Coverage for python / lsst / utils / argparsing.py: 24%
26 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 08:31 +0000
1# This file is part of utils.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
11#
13"""Utilities to help with argument parsing in command line interfaces."""
15from __future__ import annotations
17__all__ = ["AppendDict"]
19import argparse
20import copy
21from collections.abc import Mapping
22from typing import Any
25class AppendDict(argparse.Action):
26 """An action analogous to the built-in 'append' that appends to a `dict`
27 instead of a `list`.
29 Inputs are assumed to be strings in the form "key=value"; any input that
30 does not contain exactly one "=" character is invalid. If the default value
31 is non-empty, the default key-value pairs may be overwritten by values from
32 the command line.
33 """
35 def __init__(
36 self,
37 option_strings: str | list[str],
38 dest: str,
39 nargs: int | str | None = None,
40 const: Any | None = None,
41 default: Any | None = None,
42 type: type | None = None,
43 choices: Any | None = None,
44 required: bool = False,
45 help: str | None = None,
46 metavar: str | None = None,
47 ):
48 if default is None:
49 default = {}
50 if not isinstance(default, Mapping):
51 argname = option_strings if option_strings else metavar if metavar else dest
52 raise TypeError(f"Default for {argname} must be a mapping or None, got {default!r}.")
53 super().__init__(option_strings, dest, nargs, const, default, type, choices, required, help, metavar)
55 def __call__(
56 self, parser: argparse.ArgumentParser, namespace: Any, values: Any, option_string: str | None = None
57 ) -> None:
58 # argparse doesn't make defensive copies, so namespace.dest may be
59 # the same object as self.default. Do the copy ourselves and avoid
60 # modifying the object previously in namespace.dest.
61 mapping = copy.copy(getattr(namespace, self.dest))
63 # Sometimes values is a copy of default instead of an input???
64 if isinstance(values, Mapping):
65 mapping.update(values)
66 else:
67 # values may be either a string or list of strings, depending on
68 # nargs. Unsafe to test for Sequence, because a scalar string
69 # passes.
70 if not isinstance(values, list):
71 values = [values]
72 for value in values:
73 vars = value.split("=")
74 if len(vars) != 2:
75 raise ValueError(f"Argument {value!r} does not match format 'key=value'.")
76 mapping[vars[0]] = vars[1]
78 # Other half of the defensive copy.
79 setattr(namespace, self.dest, mapping)