Coverage for python / lsst / utils / argparsing.py: 24%

26 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 08:37 +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# 

12 

13"""Utilities to help with argument parsing in command line interfaces.""" 

14 

15from __future__ import annotations 

16 

17__all__ = ["AppendDict"] 

18 

19import argparse 

20import copy 

21from collections.abc import Mapping 

22from typing import Any 

23 

24 

25class AppendDict(argparse.Action): 

26 """An action analogous to the built-in 'append' that appends to a `dict` 

27 instead of a `list`. 

28 

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 """ 

34 

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) 

54 

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)) 

62 

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] 

77 

78 # Other half of the defensive copy. 

79 setattr(namespace, self.dest, mapping)