nested_dict_action.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import argparse
  2. import copy
  3. import yaml
  4. class NestedDictAction(argparse.Action):
  5. """Action class to append items to dict object.
  6. Examples:
  7. >>> parser = argparse.ArgumentParser()
  8. >>> _ = parser.add_argument('--conf', action=NestedDictAction,
  9. ... default={'a': 4})
  10. >>> parser.parse_args(['--conf', 'a=3', '--conf', 'c=4'])
  11. Namespace(conf={'a': 3, 'c': 4})
  12. >>> parser.parse_args(['--conf', 'c.d=4'])
  13. Namespace(conf={'a': 4, 'c': {'d': 4}})
  14. >>> parser.parse_args(['--conf', 'c.d=4', '--conf', 'c=2'])
  15. Namespace(conf={'a': 4, 'c': 2})
  16. >>> parser.parse_args(['--conf', '{d: 5, e: 9}'])
  17. Namespace(conf={'d': 5, 'e': 9})
  18. """
  19. _syntax = """Syntax:
  20. {op} <key>=<yaml-string>
  21. {op} <key>.<key2>=<yaml-string>
  22. {op} <python-dict>
  23. {op} <yaml-string>
  24. e.g.
  25. {op} a=4
  26. {op} a.b={{c: true}}
  27. {op} {{"c": True}}
  28. {op} {{a: 34.5}}
  29. """
  30. def __init__(
  31. self,
  32. option_strings,
  33. dest,
  34. nargs=None,
  35. default=None,
  36. choices=None,
  37. required=False,
  38. help=None,
  39. metavar=None,
  40. ):
  41. super().__init__(
  42. option_strings=option_strings,
  43. dest=dest,
  44. nargs=nargs,
  45. default=copy.deepcopy(default),
  46. type=None,
  47. choices=choices,
  48. required=required,
  49. help=help,
  50. metavar=metavar,
  51. )
  52. def __call__(self, parser, namespace, values, option_strings=None):
  53. # --{option} a.b=3 -> {'a': {'b': 3}}
  54. if "=" in values:
  55. indict = copy.deepcopy(getattr(namespace, self.dest, {}))
  56. key, value = values.split("=", maxsplit=1)
  57. if not value.strip() == "":
  58. value = yaml.load(value, Loader=yaml.Loader)
  59. if not isinstance(indict, dict):
  60. indict = {}
  61. keys = key.split(".")
  62. d = indict
  63. for idx, k in enumerate(keys):
  64. if idx == len(keys) - 1:
  65. d[k] = value
  66. else:
  67. if not isinstance(d.setdefault(k, {}), dict):
  68. # Remove the existing value and recreates as empty dict
  69. d[k] = {}
  70. d = d[k]
  71. # Update the value
  72. setattr(namespace, self.dest, indict)
  73. else:
  74. try:
  75. # At the first, try eval(), i.e. Python syntax dict.
  76. # e.g. --{option} "{'a': 3}" -> {'a': 3}
  77. # This is workaround for internal behaviour of configargparse.
  78. value = eval(values, {}, {})
  79. if not isinstance(value, dict):
  80. syntax = self._syntax.format(op=option_strings)
  81. mes = f"must be interpreted as dict: but got {values}\n{syntax}"
  82. raise argparse.ArgumentTypeError(self, mes)
  83. except Exception:
  84. # and the second, try yaml.load
  85. value = yaml.load(values, Loader=yaml.Loader)
  86. if not isinstance(value, dict):
  87. syntax = self._syntax.format(op=option_strings)
  88. mes = f"must be interpreted as dict: but got {values}\n{syntax}"
  89. raise argparse.ArgumentError(self, mes)
  90. d = getattr(namespace, self.dest, None)
  91. if isinstance(d, dict):
  92. d.update(value)
  93. else:
  94. # Remove existing params, and overwrite
  95. setattr(namespace, self.dest, value)