Skip to content

Commit 6dc3151

Browse files
committed
Store defaults in namespace
1 parent d460c8e commit 6dc3151

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
@@ -1233,7 +1233,9 @@ def __call__(self, parser, namespace, values, option_string=None):
12331233
# namespace for the relevant parts.
12341234
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
12351235
for key, value in vars(subnamespace).items():
1236-
setattr(namespace, key, value)
1236+
if key != '__defaults__':
1237+
setattr(namespace, key, value)
1238+
namespace.__defaults__.update(subnamespace.__defaults__)
12371239

12381240
if arg_strings:
12391241
vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
@@ -1308,21 +1310,36 @@ def __repr__(self):
13081310
class Namespace(_AttributeHolder):
13091311
"""Simple object for storing attributes.
13101312
1313+
Default values are stored in a dict name `__defaults__` so they won't mess
1314+
with the given values.
1315+
13111316
Implements equality by attribute names and values, and provides a simple
13121317
string representation.
13131318
"""
13141319

13151320
def __init__(self, **kwargs):
13161321
for name in kwargs:
13171322
setattr(self, name, kwargs[name])
1323+
self.__defaults__ = {}
1324+
1325+
def _get_kwargs(self):
1326+
kwargs = self.__defaults__ | self.__dict__
1327+
kwargs.pop('__defaults__', None)
1328+
return list(kwargs.items())
1329+
1330+
def __getattr__(self, name):
1331+
try:
1332+
return self.__defaults__[name]
1333+
except KeyError:
1334+
raise AttributeError(name)
13181335

13191336
def __eq__(self, other):
13201337
if not isinstance(other, Namespace):
13211338
return NotImplemented
1322-
return vars(self) == vars(other)
1339+
return dict(self._get_kwargs()) == dict(other._get_kwargs())
13231340

13241341
def __contains__(self, key):
1325-
return key in self.__dict__
1342+
return key in self.__dict__ or key in self.__defaults__
13261343

13271344

13281345
class _ActionsContainer(object):
@@ -1880,14 +1897,13 @@ def parse_known_args(self, args=None, namespace=None):
18801897
# add any action defaults that aren't present
18811898
for action in self._actions:
18821899
if action.dest is not SUPPRESS:
1883-
if not hasattr(namespace, action.dest):
1884-
if action.default is not SUPPRESS:
1885-
setattr(namespace, action.dest, action.default)
1900+
if action.default is not SUPPRESS and action.dest not in namespace:
1901+
namespace.__defaults__[action.dest] = action.default
18861902

18871903
# add any parser defaults that aren't present
18881904
for dest in self._defaults:
1889-
if not hasattr(namespace, dest):
1890-
setattr(namespace, dest, self._defaults[dest])
1905+
if dest not in namespace:
1906+
namespace.__defaults__[dest] = self._defaults[dest]
18911907

18921908
# parse the arguments and exit if there are any errors
18931909
if self.exit_on_error:
@@ -2124,12 +2140,11 @@ def consume_positionals(start_index):
21242140
# parsing arguments to avoid calling convert functions
21252141
# twice (which may fail) if the argument was given, but
21262142
# only if it was defined already in the namespace
2127-
if (action.default is not None and
2128-
isinstance(action.default, str) and
2129-
hasattr(namespace, action.dest) and
2130-
action.default is getattr(namespace, action.dest)):
2131-
setattr(namespace, action.dest,
2132-
self._get_value(action, action.default))
2143+
if (isinstance(action.default, str) and
2144+
action.dest in namespace and
2145+
getattr(namespace, action.dest) is action.default):
2146+
namespace.__defaults__[action.dest] = self._get_value(
2147+
action, action.default)
21332148

21342149
if required_actions:
21352150
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):
@@ -2083,6 +2072,16 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
20832072
# return the main parser
20842073
return parser
20852074

2075+
def _get_parser_with_shared_option(self):
2076+
parser = ErrorRaisingArgumentParser(prog='PROG', description='main description')
2077+
parser.add_argument('-f', '--foo', default='0')
2078+
subparsers = parser.add_subparsers()
2079+
parser1 = subparsers.add_parser('1')
2080+
parser1.add_argument('-f', '--foo', default='1')
2081+
parser2 = subparsers.add_parser('2')
2082+
parser2.add_argument('-f', '--foo', default='2')
2083+
return parser
2084+
20862085
def setUp(self):
20872086
super().setUp()
20882087
self.parser = self._get_parser()
@@ -2435,6 +2434,14 @@ def test_alias_help(self):
24352434
3 3 help
24362435
"""))
24372436

2437+
def test_subparsers_with_shared_option(self):
2438+
parser = self._get_parser_with_shared_option()
2439+
self.assertEqual(parser.parse_args([]), NS(foo='0'))
2440+
self.assertEqual(parser.parse_args(['1']), NS(foo='1'))
2441+
self.assertEqual(parser.parse_args(['2']), NS(foo='2'))
2442+
self.assertEqual(parser.parse_args(['-f', '10', '1', '-f', '42']), NS(foo='42'))
2443+
self.assertEqual(parser.parse_args(['1'], NS(foo='42')), NS(foo='42'))
2444+
24382445
# ============
24392446
# Groups tests
24402447
# ============
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)