Coverage for python/lsst/utils/argparsing.py: 100%
26 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:40 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:40 -0700
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.
34 Parameters
35 ----------
36 option_strings : `str` | `list` [ `str` ]
37 The command-line option strings that trigger this action, e.g.
38 ``--config``.
39 dest : `str`
40 The name of the `~argparse.Namespace` attribute in which the
41 accumulated `dict` is stored.
42 nargs : `int` | `str` | `None`, optional
43 The number of command-line arguments consumed each time the option
44 appears, as accepted by `argparse.ArgumentParser.add_argument`.
45 const : `typing.Any` | `None`, optional
46 A constant value required by some ``nargs`` settings; unused by this
47 action.
48 default : `typing.Any` | `None`, optional
49 The initial value of the mapping if the option does not appear on the
50 command line. Must be a `~collections.abc.Mapping` or `None`; `None`
51 is converted to an empty `dict`.
52 type : `type` | `None`, optional
53 A callable applied by argparse to each command-line argument before
54 it is passed to this action.
55 choices : `typing.Any` | `None`, optional
56 A container of the allowable values for the argument.
57 required : `bool`, optional
58 Whether the option must appear on the command line.
59 help : `str` | `None`, optional
60 A brief description of the argument for usage messages.
61 metavar : `str` | `None`, optional
62 The name for the argument's value(s) in usage messages.
63 """
65 def __init__(
66 self,
67 option_strings: str | list[str],
68 dest: str,
69 nargs: int | str | None = None,
70 const: Any | None = None,
71 default: Any | None = None,
72 type: type | None = None,
73 choices: Any | None = None,
74 required: bool = False,
75 help: str | None = None,
76 metavar: str | None = None,
77 ):
78 if default is None:
79 default = {}
80 if not isinstance(default, Mapping):
81 argname = option_strings if option_strings else metavar if metavar else dest
82 raise TypeError(f"Default for {argname} must be a mapping or None, got {default!r}.")
83 super().__init__(option_strings, dest, nargs, const, default, type, choices, required, help, metavar)
85 def __call__(
86 self, parser: argparse.ArgumentParser, namespace: Any, values: Any, option_string: str | None = None
87 ) -> None:
88 # argparse doesn't make defensive copies, so namespace.dest may be
89 # the same object as self.default. Do the copy ourselves and avoid
90 # modifying the object previously in namespace.dest.
91 mapping = copy.copy(getattr(namespace, self.dest))
93 # Sometimes values is a copy of default instead of an input???
94 if isinstance(values, Mapping):
95 mapping.update(values)
96 else:
97 # values may be either a string or list of strings, depending on
98 # nargs. Unsafe to test for Sequence, because a scalar string
99 # passes.
100 if not isinstance(values, list):
101 values = [values]
102 for value in values:
103 vars = value.split("=")
104 if len(vars) != 2:
105 raise ValueError(f"Argument {value!r} does not match format 'key=value'.")
106 mapping[vars[0]] = vars[1]
108 # Other half of the defensive copy.
109 setattr(namespace, self.dest, mapping)