Skip to content

Commit 4a63f50

Browse files
committed
Store defaults in namespace
1 parent f33e2c8 commit 4a63f50

File tree

3 files changed

+49
-26
lines changed

3 files changed

+49
-26
lines changed

Lib/argparse.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,7 +1215,9 @@ def __call__(self, parser, namespace, values, option_string=None):
12151215
# namespace for the relevant parts.
12161216
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
12171217
for key, value in vars(subnamespace).items():
1218-
setattr(namespace, key, value)
1218+
if key != '__defaults__':
1219+
setattr(namespace, key, value)
1220+
namespace.__defaults__.update(subnamespace.__defaults__)
12191221

12201222
if arg_strings:
12211223
vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
@@ -1290,21 +1292,36 @@ def __repr__(self):
12901292
class Namespace(_AttributeHolder):
12911293
"""Simple object for storing attributes.
12921294
1295+
Default values are stored in a dict name `__defaults__` so they won't mess
1296+
with the given values.
1297+
12931298
Implements equality by attribute names and values, and provides a simple
12941299
string representation.
12951300
"""
12961301

12971302
def __init__(self, **kwargs):
12981303
for name in kwargs:
12991304
setattr(self, name, kwargs[name])
1305+
self.__defaults__ = {}
1306+
1307+
def _get_kwargs(self):
1308+
kwargs = self.__defaults__ | self.__dict__
1309+
kwargs.pop('__defaults__', None)
1310+
return list(kwargs.items())
1311+
1312+
def __getattr__(self, name):
1313+
try:
1314+
return self.__defaults__[name]
1315+
except KeyError:
1316+
raise AttributeError(name)
13001317

13011318
def __eq__(self, other):
13021319
if not isinstance(other, Namespace):
13031320
return NotImplemented
1304-
return vars(self) == vars(other)
1321+
return dict(self._get_kwargs()) == dict(other._get_kwargs())
13051322

13061323
def __contains__(self, key):
1307-
return key in self.__dict__
1324+
return key in self.__dict__ or key in self.__defaults__
13081325

13091326

13101327
class _ActionsContainer(object):
@@ -1862,14 +1879,13 @@ def parse_known_args(self, args=None, namespace=None):
18621879
# add any action defaults that aren't present
18631880
for action in self._actions:
18641881
if action.dest is not SUPPRESS:
1865-
if not hasattr(namespace, action.dest):
1866-
if action.default is not SUPPRESS:
1867-
setattr(namespace, action.dest, action.default)
1882+
if action.default is not SUPPRESS and action.dest not in namespace:
1883+
namespace.__defaults__[action.dest] = action.default
18681884

18691885
# add any parser defaults that aren't present
18701886
for dest in self._defaults:
1871-
if not hasattr(namespace, dest):
1872-
setattr(namespace, dest, self._defaults[dest])
1887+
if dest not in namespace:
1888+
namespace.__defaults__[dest] = self._defaults[dest]
18731889

18741890
# parse the arguments and exit if there are any errors
18751891
if self.exit_on_error:
@@ -2102,12 +2118,11 @@ def consume_positionals(start_index):
21022118
# parsing arguments to avoid calling convert functions
21032119
# twice (which may fail) if the argument was given, but
21042120
# only if it was defined already in the namespace
2105-
if (action.default is not None and
2106-
isinstance(action.default, str) and
2107-
hasattr(namespace, action.dest) and
2108-
action.default is getattr(namespace, action.dest)):
2109-
setattr(namespace, action.dest,
2110-
self._get_value(action, action.default))
2121+
if (isinstance(action.default, str) and
2122+
action.dest in namespace and
2123+
getattr(namespace, action.dest) is action.default):
2124+
namespace.__defaults__[action.dest] = self._get_value(
2125+
action, action.default)
21112126

21122127
if required_actions:
21132128
self.error(_('the following arguments are required: %s') %

Lib/test/test_argparse.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,7 @@ def __init__(self, *args, **kwargs):
7676
self.kwargs = kwargs
7777

7878

79-
class NS(object):
80-
81-
def __init__(self, **kwargs):
82-
self.__dict__.update(kwargs)
83-
84-
def __repr__(self):
85-
sorted_items = sorted(self.__dict__.items())
86-
kwarg_str = ', '.join(['%s=%r' % tup for tup in sorted_items])
87-
return '%s(%s)' % (type(self).__name__, kwarg_str)
88-
89-
def __eq__(self, other):
90-
return vars(self) == vars(other)
79+
NS = argparse.Namespace
9180

9281

9382
class ArgumentParserError(Exception):
@@ -2075,6 +2064,16 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
20752064
# return the main parser
20762065
return parser
20772066

2067+
def _get_parser_with_shared_option(self):
2068+
parser = ErrorRaisingArgumentParser(prog='PROG', description='main description')
2069+
parser.add_argument('-f', '--foo', default='0')
2070+
subparsers = parser.add_subparsers()
2071+
parser1 = subparsers.add_parser('1')
2072+
parser1.add_argument('-f', '--foo', default='1')
2073+
parser2 = subparsers.add_parser('2')
2074+
parser2.add_argument('-f', '--foo', default='2')
2075+
return parser
2076+
20782077
def setUp(self):
20792078
super().setUp()
20802079
self.parser = self._get_parser()
@@ -2429,6 +2428,14 @@ def test_alias_help(self):
24292428
3 3 help
24302429
"""))
24312430

2431+
def test_subparsers_with_shared_option(self):
2432+
parser = self._get_parser_with_shared_option()
2433+
self.assertEqual(parser.parse_args([]), NS(foo='0'))
2434+
self.assertEqual(parser.parse_args(['1']), NS(foo='1'))
2435+
self.assertEqual(parser.parse_args(['2']), NS(foo='2'))
2436+
self.assertEqual(parser.parse_args(['-f', '10', '1', '-f', '42']), NS(foo='42'))
2437+
self.assertEqual(parser.parse_args(['1'], NS(foo='42')), NS(foo='42'))
2438+
24322439
# ============
24332440
# Groups tests
24342441
# ============
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix an issue that subparsers defaults override the existing values in the argparse Namespace.

0 commit comments

Comments
 (0)