diff --git a/Lib/argparse.py b/Lib/argparse.py index 2144c81886ad19..3b0fbc5354f281 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1316,7 +1316,10 @@ def __call__(self, parser, namespace, values, option_string=None): # namespace for the relevant parts. subnamespace, arg_strings = subparser.parse_known_args(arg_strings, None) for key, value in vars(subnamespace).items(): - setattr(namespace, key, value) + if key != '__defaults__': + setattr(namespace, key, value) + if hasattr(namespace, '__defaults__'): + namespace.__defaults__.update(subnamespace.__defaults__) if arg_strings: if not hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): @@ -1398,6 +1401,9 @@ def __repr__(self): class Namespace(_AttributeHolder): """Simple object for storing attributes. + Default values are stored in a dict named `__defaults__` so they won't + override the given values. + Implements equality by attribute names and values, and provides a simple string representation. """ @@ -1405,16 +1411,34 @@ class Namespace(_AttributeHolder): def __init__(self, **kwargs): for name in kwargs: setattr(self, name, kwargs[name]) + self.__defaults__ = {} + + def _get_kwargs(self): + kwargs = self.__defaults__ | self.__dict__ + kwargs.pop('__defaults__', None) + return list(kwargs.items()) + + def __getattr__(self, name): + try: + return self.__defaults__[name] + except KeyError: + raise AttributeError(name) def __eq__(self, other): if not isinstance(other, Namespace): return NotImplemented - return vars(self) == vars(other) + return dict(self._get_kwargs()) == dict(other._get_kwargs()) def __contains__(self, key): - return key in self.__dict__ + return key in self.__dict__ or key in self.__defaults__ +def _set_default(namespace, dest, value): + if not hasattr(namespace, '__defaults__'): + setattr(namespace, dest, value) + else: + namespace.__defaults__[dest] = value + class _ActionsContainer(object): def __init__(self, @@ -2025,14 +2049,13 @@ def _parse_known_args2(self, args, namespace, intermixed): # add any action defaults that aren't present for action in self._actions: if action.dest is not SUPPRESS: - if not hasattr(namespace, action.dest): - if action.default is not SUPPRESS: - setattr(namespace, action.dest, action.default) + if action.default is not SUPPRESS and not hasattr(namespace, action.dest): + _set_default(namespace, action.dest, action.default) # add any parser defaults that aren't present for dest in self._defaults: if not hasattr(namespace, dest): - setattr(namespace, dest, self._defaults[dest]) + _set_default(namespace, dest, self._defaults[dest]) # parse the arguments and exit if there are any errors if self.exit_on_error: @@ -2330,7 +2353,7 @@ def consume_positionals(start_index): isinstance(action.default, str) and hasattr(namespace, action.dest) and action.default is getattr(namespace, action.dest)): - setattr(namespace, action.dest, + _set_default(namespace, action.dest, self._get_value(action, action.default)) if required_actions: diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index fc73174d98cd6f..9c5756dca51304 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -121,18 +121,7 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs -class NS(object): - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __repr__(self): - sorted_items = sorted(self.__dict__.items()) - kwarg_str = ', '.join(['%s=%r' % tup for tup in sorted_items]) - return '%s(%s)' % (type(self).__name__, kwarg_str) - - def __eq__(self, other): - return vars(self) == vars(other) +NS = argparse.Namespace class ArgumentParserError(Exception): @@ -2474,6 +2463,16 @@ def _get_parser(self, subparser_help=False, prefix_chars=None, # return the main parser return parser + def _get_parser_with_shared_option(self): + parser = ErrorRaisingArgumentParser(prog='PROG', description='main description') + parser.add_argument('-f', '--foo', default='0') + subparsers = parser.add_subparsers() + parser1 = subparsers.add_parser('1') + parser1.add_argument('-f', '--foo', default='1') + parser2 = subparsers.add_parser('2') + parser2.add_argument('-f', '--foo', default='2') + return parser + def setUp(self): super().setUp() self.parser = self._get_parser() @@ -2940,6 +2939,14 @@ def test_alias_help(self): 3 3 help """)) + def test_subparsers_with_shared_option(self): + parser = self._get_parser_with_shared_option() + self.assertEqual(parser.parse_args([]), NS(foo='0')) + self.assertEqual(parser.parse_args(['1']), NS(foo='1')) + self.assertEqual(parser.parse_args(['2']), NS(foo='2')) + self.assertEqual(parser.parse_args(['-f', '10', '1', '-f', '42']), NS(foo='42')) + self.assertEqual(parser.parse_args(['1'], NS(foo='42')), NS(foo='42')) + # ============ # Groups tests # ============ diff --git a/Misc/NEWS.d/next/Library/2021-12-21-08-34-36.bpo-45235.OV9_9i.rst b/Misc/NEWS.d/next/Library/2021-12-21-08-34-36.bpo-45235.OV9_9i.rst new file mode 100644 index 00000000000000..06c70cb3480948 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-12-21-08-34-36.bpo-45235.OV9_9i.rst @@ -0,0 +1 @@ +Fix an issue that subparsers defaults override the existing values in the argparse Namespace.