Skip to content
73 changes: 69 additions & 4 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1716,8 +1716,9 @@ Subcommands
:meth:`!add_subparsers` method. The :meth:`!add_subparsers` method is normally
called with no arguments and returns a special action object. This object
has a single method, :meth:`~_SubParsersAction.add_parser`, which takes a
command name and any :class:`!ArgumentParser` constructor arguments, and
returns an :class:`!ArgumentParser` object that can be modified as usual.
command name, optional deprecated_ and subnamespace_ flags, any
:class:`!ArgumentParser` constructor arguments, and returns an
:class:`!ArgumentParser` object that can be modified as usual.

Description of parameters:

Expand Down Expand Up @@ -1775,7 +1776,9 @@ Subcommands
command line (and not any other subparsers). So in the example above, when
the ``a`` command is specified, only the ``foo`` and ``bar`` attributes are
present, and when the ``b`` command is specified, only the ``foo`` and
``baz`` attributes are present.
``baz`` attributes are present. If one wishes to store the the subparser's
attributes separate from the main parser's attributes, see the subnamespace_
option of :meth:`~_SubParsersAction.add_parser`.

Similarly, when a help message is requested from a subparser, only the help
for that particular parser will be printed. The help message will not
Expand Down Expand Up @@ -1896,7 +1899,8 @@ Subcommands


.. method:: _SubParsersAction.add_parser(name, *, help=None, aliases=None, \
deprecated=False, **kwargs)
deprecated=False, subnamespace=False, \
**kwargs)

Create and return a new :class:`ArgumentParser` object for the
subcommand *name*.
Expand Down Expand Up @@ -1925,12 +1929,73 @@ Subcommands
chicken.py: warning: command 'fly' is deprecated
Namespace()

.. _subnamespace:

The *subnamespace* flag, if ``True``, tells the parent parser to
store the subparser's parsed arguments contained in
their own :class:`Namespace`, nested within the parent's :class:`!Namespace`.
The attribute name in the parent's namespace at which the
subparser's subnamespace is stored is the subparser's *name*,
but with underscores ``_`` replacing hyphens ``-``
similar to dest_ in :meth:`ArgumentParser.add_argument`.

This is useful for receiving parsed arguments hierarchically, mirroring the
hierarchical relation between a parser and its subparsers. For example::

>>> inet = argparse.ArgumentParser(add_help=False)
>>> inet.add_argument("address")
>>> inet.add_argument("port", type=int)
>>>
>>> unix = argparse.ArgumentParser(add_help=False)
>>> unix.add_argument("path")
>>>
>>> parser = argparse.ArgumentParser(prog='my-socat')
>>> action = parser.add_subparsers(required=True, dest="action")
>>>
>>> parser_bind = action.add_parser("bind", subnamespace=True)
>>> parser_bind.add_argument("--fork", action="store_true")
>>> bind_family = parser_bind.add_subparsers(required=True, dest="family")
>>>
>>> parser_bind_inet = bind_family.add_parser("inet", subnamespace=True, parents=[inet])
>>> parser_bind_unix = bind_family.add_parser("unix", subnamespace=True, parents=[unix])
>>>
>>> parser_connect = action.add_parser("connect", subnamespace=True)
>>> connect_family = parser_connect.add_subparsers(required=True, dest="family")
>>>
>>> parser_connect_inet = connect_family.add_parser("inet", subnamespace=True, parents=[inet])
>>> parser_connect_unix = connect_family.add_parser("unix", subnamespace=True, parents=[unix])
>>>
>>> args = parser.parse_args(["bind", "unix", "/foo/bar/socket"])
>>> args
Namespace(action='bind', bind=Namespace(fork=False, family='unix', unix=Namespace(path='/foo/bar/socket')))

This is also very useful when one has arguments in subparsers whose
``dest`` conflict with those of the parent parser's arguments, and one
wishes to faithfully distinguish between the two. For example::

>>> parser = argparse.ArgumentParser(prog='restaurant.py')
>>> parser.add_argument('-f', help='fast-tracked order', action='store_true')
>>> meals = parser.add_subparsers(dest='meal')
>>>
>>> parser_nuggets = meals.add_parser('chicken-nuggets', subnamespace=True)
>>> parser_nuggets.add_argument('-f', help='with fries', action='store_true')
>>>
>>> parser_salad = meals.add_parser('caesar-salad', subnamespace=True)
>>> parser_salad.add_argument('-f', help='fresh', action='store_true')
>>>
>>> args = parser.parse_args(['chicken-nuggets', '-f'])
>>> args
Namespace(f=False, meal='chicken-nuggets', chicken_nuggets=Namespace(f=True))

All other keyword arguments are passed directly to the
:class:`!ArgumentParser` constructor.

.. versionadded:: 3.13
Added the *deprecated* parameter.

.. versionadded:: next
Added the *subnamespace* parameter.


FileType objects
^^^^^^^^^^^^^^^^
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,12 @@ argparse
double backticks (RST inline-literal style).
(Contributed by Hugo van Kemenade in :gh:`149375`.)

* Added ``subnamespace`` keyword-only flag to
:meth:`argparse._SubParsersAction.add_parser` to allow nested
:class:`argparse.Namespace`\ s, which correspond with the hierarchical
nature of subparsers. By default ``subnamespace`` is ``False`` for
backwards compatibility.


array
-----
Expand Down
26 changes: 23 additions & 3 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,7 +1317,7 @@ def __init__(self,
help=help,
metavar=metavar)

def add_parser(self, name, *, deprecated=False, **kwargs):
def add_parser(self, name, *, deprecated=False, subnamespace=False, **kwargs):
# set prog from the existing prefix
if kwargs.get('prog') is None:
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
Expand Down Expand Up @@ -1348,6 +1348,11 @@ def add_parser(self, name, *, deprecated=False, **kwargs):
parser._check_help(choice_action)
self._name_parser_map[name] = parser

# set the subnamespace attribute on the parser to determine
# whether parsed arguments should be stored in their own
# nested namespace or added to the parent parser's namespace
setattr(parser, 'subnamespace', subnamespace)

# make parser available under aliases also
for alias in aliases:
self._name_parser_map[alias] = parser
Expand Down Expand Up @@ -1390,8 +1395,23 @@ def __call__(self, parser, namespace, values, option_string=None):
# in a new namespace object and then update the original
# 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 the subparser's 'subnamespace' attribute is ``True``
# then store the subparser's parsed arguments contained in
# their own namespace, nested within the parent namespace.
# The attribute name in the parent namespace at which the
# subparser's subnamespace is stored is the subparser's name,
# specified when using '.add_parser()', but with '-' replaced with '_'
# similar to how options are stored.
#
# Otherwise if 'subnamespace' is ``False`` then update
# the parent namespace with the values from the subnamespace.
if subparser.subnamespace:
subnamespace_name = parser_name.replace('-', '_')
setattr(namespace, subnamespace_name, subnamespace)
else:
for key, value in vars(subnamespace).items():
setattr(namespace, key, value)

if arg_strings:
if not hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
Expand Down
205 changes: 205 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -6164,6 +6164,211 @@ def test_equality_returns_notimplemented(self):
self.assertIs(ns.__ne__(None), NotImplemented)


# ==================
# Subnamespace tests
# ==================

class TestSubnamespace(TestCase):

def test_single_subnamespace(self):
parser = argparse.ArgumentParser()
action = parser.add_subparsers(required=False, dest="action")

parser_add = action.add_parser("add", subnamespace=True)
parser_add.add_argument("--to")

parser_remove = action.add_parser("remove", subnamespace=True)
parser_remove.add_argument("--from")

# a root parser should not have 'subnamespace' attribute,
# as that attribute should only be set when using
# `_SubParsersAction.add_parser()`
self.assertNotHasAttr(parser, "subnamespace")

# check subparser has 'subnamespace' attribute,
# that was set when calling `action.add_parser()`
self.assertHasAttr(parser_add, "subnamespace")

# 'subnamespace' attribute is a bool
self.assertIsInstance(parser_add.subnamespace, bool)

# check nesting of Namspaces works
args = parser.parse_args(["add"])
self.assertEqual(args, argparse.Namespace(
action="add", add=argparse.Namespace(
to=None
)))

# test accessing of subnamespaces and args via `x.y` works
self.assertEqual(args.add.to, None)

# check 'required=False' allows no subnamespaces to be created
args = parser.parse_args([])
self.assertEqual(args, argparse.Namespace(action=None))

def test_double_subnamespace(self):
inet = argparse.ArgumentParser(add_help=False)
inet.add_argument("address")
inet.add_argument("port", type=int)
inet.add_argument("--use-proxy", action="store_true")

unix = argparse.ArgumentParser(add_help=False)
unix.add_argument("path")

parser = argparse.ArgumentParser(prog="my-socat")
parser.add_argument("--key-file")
action = parser.add_subparsers(required=True, dest="action")

parser_bind = action.add_parser("bind", subnamespace=True)
parser_bind.add_argument("--fork", action="store_true")
bind_family = parser_bind.add_subparsers(required=True, dest="family")

parser_bind_inet = bind_family.add_parser("inet", subnamespace=True, parents=[inet])
parser_bind_unix = bind_family.add_parser("unix", subnamespace=True, parents=[unix])

parser_connect = action.add_parser("connect", subnamespace=True)
connect_family = parser_connect.add_subparsers(required=True, dest="family")

parser_connect_inet = connect_family.add_parser("inet", subnamespace=True, parents=[inet])
parser_connect_unix = connect_family.add_parser("unix", subnamespace=True, parents=[unix])

# check doubly-nested Namespaces work
# we assume if this test passes that we don't need to write
# redundant triply-nested etc Namespaces tests
args = parser.parse_args(["bind", "unix", "/foo/bar/socket"])
self.assertEqual(args, argparse.Namespace(
key_file=None,
action="bind", bind=argparse.Namespace(
fork=False,
family="unix", unix=argparse.Namespace(
path="/foo/bar/socket"
))))

# we test with nested args with '-' in their name
# '--key-file' -> 'key_file' and '--use-proxy' -> 'use_proxy'
args = parser.parse_args(["--key-file", "/etc/key", "connect",
"inet", "127.0.0.1", "8000"])
self.assertEqual(args, argparse.Namespace(
key_file="/etc/key",
action="connect", connect=argparse.Namespace(
family="inet", inet=argparse.Namespace(
address="127.0.0.1", port=8000, use_proxy=False
))))

# test accessing of nested args works when they have '_' in them
self.assertEqual(args.connect.inet.use_proxy, False)

def test_mixed_some_subnamespace_some_not(self):
parser = argparse.ArgumentParser()
action = parser.add_subparsers(required=True, dest="action")

parser_add = action.add_parser("add", subnamespace=True)
spec = parser_add.add_subparsers(required=True, dest="spec")

parser_add_country = spec.add_parser("country", subnamespace=False)
parser_add_country.add_argument("country_name")

parser_add_color = spec.add_parser("color", subnamespace=True)
parser_add_color.add_argument("name")

# test that non-subnamespace parser arguments get parented to
# their parent Namespace, not the root Namespace
# ie make sure 'country_name' gets put under the 'add' Namespace
# not the root Namespace
args = parser.parse_args(["add", "country", "france"])
self.assertEqual(args, argparse.Namespace(
action="add", add=argparse.Namespace(
spec="country", country_name="france"
)))

# test accessing of double subnamespaces works
# with non-subnamespace args
self.assertEqual(args.add.country_name, "france")

# contrast above example with this one, where 'name' is
# parented under the 'add.color' Namespace
args = parser.parse_args(["add", "color", "blue"])
self.assertEqual(args, argparse.Namespace(
action="add", add=argparse.Namespace(
spec="color", color=argparse.Namespace(
name="blue"
))))

# test accessing of double subnamespaces works,
# in particular check that we're not just getting a proxy
# or some descriptor chicanery; args genuinely are stored
# hierarchically
self.assertIsInstance(args.add.color, argparse.Namespace)
self.assertEqual(args.add.color.name, "blue")

def test_exotic_subnamespace_names(self):
parser = argparse.ArgumentParser()
parser.add_argument("-f", help="fast-tracked order", action="store_true")

choice = parser.add_subparsers(required=True, dest="choice")

parser_0 = choice.add_parser("0", subnamespace=True,
help="number 0 menu item")

parser_1 = choice.add_parser("1", subnamespace=True,
help="number 1 menu item")

parser_True = choice.add_parser("True", subnamespace=True,
help="limited edition 'True' meal")
parser_True.add_argument("--deluxe", "-d", action="store_true")

parser_double_cheeseburger = choice.add_parser("double-cheeseburger",
subnamespace=True)

parser_chicken_nuggets = choice.add_parser("chicken-nuggets",
subnamespace=True)
parser_chicken_nuggets.add_argument("-f", help="with fries",
action="store_true")

# test to observe '-' being replaced with '_' in
# subnamespace attribute name
args = parser.parse_args(["double-cheeseburger"])
self.assertNotHasAttr(args, "double-cheeseburger")
self.assertHasAttr(args, "double_cheeseburger")
self.assertEqual(args, argparse.Namespace(
f=False,
choice="double-cheeseburger", double_cheeseburger=argparse.Namespace()
))

# test to observe two different `f` flags with different values
# something which is not possible without subnamespaces,
# and a key reason of why we're interested in subnamespaces
args = parser.parse_args(["chicken-nuggets", "-f"])
self.assertEqual(args, argparse.Namespace(
f=False,
choice="chicken-nuggets", chicken_nuggets=argparse.Namespace(
f=True,
)))

# test to check that the order in which the `-f` flags appear
# doesn't lead to a last-write-wins situation; the `f`s are
# genuinely being parsed individually and are not overwriting
# each other in the `_SubParsersAction.__call__` stage
args = parser.parse_args(["-f", "chicken-nuggets"])
self.assertEqual(args, argparse.Namespace(
f=True,
choice="chicken-nuggets", chicken_nuggets=argparse.Namespace(
f=False,
)))

# check that subnamespaces that aren't accessible by `x.y` notation
# are still accessible via getattr
args = parser.parse_args(["0"])
self.assertHasAttr(args, "0")
self.assertEqual(getattr(args, "0"), argparse.Namespace())

# check that subparsers whose name are a Python keyword
# are acceptable and their subnamespaces are correctly stored
args = parser.parse_args(["True"])
self.assertHasAttr(args, "True")
self.assertEqual(getattr(args, "True"), argparse.Namespace(deluxe=False))


# ===================
# File encoding tests
# ===================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add ``subnamespace`` keyword-only flag to
:meth:`argparse._SubParsersAction.add_parser` to allow nested
:class:`argparse.Namespace`\ s, which correspond with the hierarchical
nature of subparsers. By default ``subnamespace`` is ``False`` for
backwards compatibility.
Loading