Skip to content

gh-89398: Fix argparse namespace overridden by subparsers default #30219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,9 @@ def __call__(self, parser, namespace, values, option_string=None):
# namespace for the relevant parts.
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
for key, value in vars(subnamespace).items():
setattr(namespace, key, value)
if key != '__defaults__':
setattr(namespace, key, value)
namespace.__defaults__.update(subnamespace.__defaults__)

if arg_strings:
vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
Expand Down Expand Up @@ -1308,21 +1310,36 @@ 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.
"""

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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you convert it into a list if the caller (__eq__) turns it into a dict again?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __repr__ implementation of the parent class _AttributeHolder also calls this. But this change can be considered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, I should have checked that. Better to just leave it as is then.


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__


class _ActionsContainer(object):
Expand Down Expand Up @@ -1880,14 +1897,13 @@ def parse_known_args(self, args=None, namespace=None):
# 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 action.dest not in namespace:
namespace.__defaults__[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])
if dest not in namespace:
namespace.__defaults__[dest] = self._defaults[dest]

# parse the arguments and exit if there are any errors
if self.exit_on_error:
Expand Down Expand Up @@ -2124,12 +2140,11 @@ def consume_positionals(start_index):
# parsing arguments to avoid calling convert functions
# twice (which may fail) if the argument was given, but
# only if it was defined already in the namespace
if (action.default is not None and
isinstance(action.default, str) and
hasattr(namespace, action.dest) and
action.default is getattr(namespace, action.dest)):
setattr(namespace, action.dest,
self._get_value(action, action.default))
if (isinstance(action.default, str) and
action.dest in namespace and
getattr(namespace, action.dest) is action.default):
namespace.__defaults__[action.dest] = self._get_value(
action, action.default)

if required_actions:
self.error(_('the following arguments are required: %s') %
Expand Down
31 changes: 19 additions & 12 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,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):
Expand Down Expand Up @@ -2083,6 +2072,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()
Expand Down Expand Up @@ -2435,6 +2434,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
# ============
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix an issue that subparsers defaults override the existing values in the argparse Namespace.