Skip to content

Commit ac449af

Browse files
gh-101503: Add public names for argparse's group and subparsers-action types
add_subparsers(), add_argument_group(), and add_mutually_exclusive_group() return instances of _SubParsersAction, _ArgumentGroup, and _MutuallyExclusiveGroup respectively -- underscore-prefixed, undocumented classes with no supported way to reference them for type hints or isinstance checks, even though the documentation itself was already forced to describe _SubParsersAction.add_parser() under its private name. Rename these to SubParsersAction, ArgumentGroup, and MutuallyExclusiveGroup, add them to __all__, and document them. The old private names are kept as aliases for backwards compatibility with existing code that imported them directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent e620377 commit ac449af

4 files changed

Lines changed: 113 additions & 21 deletions

File tree

Doc/library/argparse.rst

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ your usage messages.
211211
When a custom usage message is specified for the main parser, you may also want to
212212
consider passing the ``prog`` argument to :meth:`~ArgumentParser.add_subparsers`
213213
or the ``prog`` and the ``usage`` arguments to
214-
:meth:`~_SubParsersAction.add_parser`, to ensure consistent command prefixes and
214+
:meth:`~SubParsersAction.add_parser`, to ensure consistent command prefixes and
215215
usage information across subparsers.
216216

217217

@@ -1733,10 +1733,11 @@ Subcommands
17331733
different functions which require different kinds of command-line arguments.
17341734
:class:`ArgumentParser` supports the creation of such subcommands with the
17351735
:meth:`!add_subparsers` method. The :meth:`!add_subparsers` method is normally
1736-
called with no arguments and returns a special action object. This object
1737-
has a single method, :meth:`~_SubParsersAction.add_parser`, which takes a
1738-
command name and any :class:`!ArgumentParser` constructor arguments, and
1739-
returns an :class:`!ArgumentParser` object that can be modified as usual.
1736+
called with no arguments and returns a :class:`SubParsersAction` object.
1737+
This object has a single method, :meth:`~SubParsersAction.add_parser`, which
1738+
takes a command name and any :class:`!ArgumentParser` constructor arguments,
1739+
and returns an :class:`!ArgumentParser` object that can be modified as
1740+
usual.
17401741

17411742
Description of parameters:
17421743

@@ -1805,7 +1806,7 @@ Subcommands
18051806
for that particular parser will be printed. The help message will not
18061807
include parent parser or sibling parser messages. (A help message for each
18071808
subparser command, however, can be given by supplying the ``help=`` argument
1808-
to :meth:`~_SubParsersAction.add_parser` as above.)
1809+
to :meth:`~SubParsersAction.add_parser` as above.)
18091810

18101811
::
18111812

@@ -1919,7 +1920,15 @@ Subcommands
19191920
the main parser.
19201921

19211922

1922-
.. method:: _SubParsersAction.add_parser(name, *, help=None, aliases=None, \
1923+
.. class:: SubParsersAction
1924+
1925+
The type of the object returned by :meth:`~ArgumentParser.add_subparsers`.
1926+
1927+
.. versionadded:: 3.16
1928+
Previously this class was only available under the private,
1929+
undocumented name ``argparse._SubParsersAction``.
1930+
1931+
.. method:: SubParsersAction.add_parser(name, *, help=None, aliases=None, \
19231932
deprecated=False, **kwargs)
19241933
19251934
Create and return a new :class:`ArgumentParser` object for the
@@ -2019,8 +2028,8 @@ Argument groups
20192028
bar bar help
20202029
--foo FOO foo help
20212030

2022-
The :meth:`add_argument_group` method returns an argument group object which
2023-
has an :meth:`~ArgumentParser.add_argument` method just like a regular
2031+
The :meth:`add_argument_group` method returns an :class:`ArgumentGroup`
2032+
object which has an :meth:`~ArgumentParser.add_argument` method just like a regular
20242033
:class:`ArgumentParser`. When an argument is added to the group, the parser
20252034
treats it just like a normal argument, but displays the argument in a
20262035
separate group for help messages. The :meth:`!add_argument_group` method
@@ -2066,6 +2075,16 @@ Argument groups
20662075
is now deprecated.
20672076

20682077

2078+
.. class:: ArgumentGroup
2079+
2080+
The type of the object returned by
2081+
:meth:`~ArgumentParser.add_argument_group`.
2082+
2083+
.. versionadded:: 3.16
2084+
Previously this class was only available under the private,
2085+
undocumented name ``argparse._ArgumentGroup``.
2086+
2087+
20692088
Mutual exclusion
20702089
^^^^^^^^^^^^^^^^
20712090

@@ -2129,6 +2148,17 @@ Mutual exclusion
21292148
exposed through inheritance.
21302149

21312150

2151+
.. class:: MutuallyExclusiveGroup
2152+
2153+
The type of the object returned by
2154+
:meth:`~ArgumentParser.add_mutually_exclusive_group`. A subclass of
2155+
:class:`ArgumentGroup`.
2156+
2157+
.. versionadded:: 3.16
2158+
Previously this class was only available under the private,
2159+
undocumented name ``argparse._MutuallyExclusiveGroup``.
2160+
2161+
21322162
Parser defaults
21332163
^^^^^^^^^^^^^^^
21342164

Lib/argparse.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
'MetavarTypeHelpFormatter',
7878
'Namespace',
7979
'Action',
80+
'ArgumentGroup',
81+
'MutuallyExclusiveGroup',
82+
'SubParsersAction',
8083
'ONE_OR_MORE',
8184
'OPTIONAL',
8285
'PARSER',
@@ -1337,15 +1340,15 @@ def __call__(self, parser, namespace, values, option_string=None):
13371340
parser.exit()
13381341

13391342

1340-
class _SubParsersAction(Action):
1343+
class SubParsersAction(Action):
13411344

13421345
class _ChoicesPseudoAction(Action):
13431346

13441347
def __init__(self, name, aliases, help):
13451348
metavar = dest = name
13461349
if aliases:
13471350
metavar += ' (%s)' % ', '.join(aliases)
1348-
sup = super(_SubParsersAction._ChoicesPseudoAction, self)
1351+
sup = super(SubParsersAction._ChoicesPseudoAction, self)
13491352
sup.__init__(option_strings=[], dest=dest, help=help,
13501353
metavar=metavar)
13511354

@@ -1365,7 +1368,7 @@ def __init__(self,
13651368
self._deprecated = set()
13661369
self._color = True
13671370

1368-
super(_SubParsersAction, self).__init__(
1371+
super(SubParsersAction, self).__init__(
13691372
option_strings=option_strings,
13701373
dest=dest,
13711374
nargs=PARSER,
@@ -1455,6 +1458,10 @@ def __call__(self, parser, namespace, values, option_string=None):
14551458
setattr(namespace, _UNRECOGNIZED_ARGS_ATTR, [])
14561459
getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
14571460

1461+
# Deprecated alias, kept for backwards compatibility with code that
1462+
# imported this class under its old, undocumented private name.
1463+
_SubParsersAction = SubParsersAction
1464+
14581465
class _ExtendAction(_AppendAction):
14591466
def __call__(self, parser, namespace, values, option_string=None):
14601467
items = getattr(namespace, self.dest, None)
@@ -1580,7 +1587,7 @@ def __init__(self,
15801587
self.register('action', 'count', _CountAction)
15811588
self.register('action', 'help', _HelpAction)
15821589
self.register('action', 'version', _VersionAction)
1583-
self.register('action', 'parsers', _SubParsersAction)
1590+
self.register('action', 'parsers', SubParsersAction)
15841591
self.register('action', 'extend', _ExtendAction)
15851592

15861593
# raise an exception if the conflict handler is invalid
@@ -1699,12 +1706,12 @@ def add_argument(self, *args, **kwargs):
16991706
return self._add_action(action)
17001707

17011708
def add_argument_group(self, *args, **kwargs):
1702-
group = _ArgumentGroup(self, *args, **kwargs)
1709+
group = ArgumentGroup(self, *args, **kwargs)
17031710
self._action_groups.append(group)
17041711
return group
17051712

17061713
def add_mutually_exclusive_group(self, **kwargs):
1707-
group = _MutuallyExclusiveGroup(self, **kwargs)
1714+
group = MutuallyExclusiveGroup(self, **kwargs)
17081715
self._mutually_exclusive_groups.append(group)
17091716
return group
17101717

@@ -1892,7 +1899,7 @@ def _check_help(self, action):
18921899
raise ValueError('badly formed help string') from exc
18931900

18941901

1895-
class _ArgumentGroup(_ActionsContainer):
1902+
class ArgumentGroup(_ActionsContainer):
18961903

18971904
def __init__(self, container, title=None, description=None, **kwargs):
18981905
if 'prefix_chars' in kwargs:
@@ -1907,7 +1914,7 @@ def __init__(self, container, title=None, description=None, **kwargs):
19071914
update('conflict_handler', container.conflict_handler)
19081915
update('prefix_chars', container.prefix_chars)
19091916
update('argument_default', container.argument_default)
1910-
super_init = super(_ArgumentGroup, self).__init__
1917+
super_init = super(ArgumentGroup, self).__init__
19111918
super_init(description=description, **kwargs)
19121919

19131920
# group attributes
@@ -1924,21 +1931,25 @@ def __init__(self, container, title=None, description=None, **kwargs):
19241931
self._mutually_exclusive_groups = container._mutually_exclusive_groups
19251932

19261933
def _add_action(self, action):
1927-
action = super(_ArgumentGroup, self)._add_action(action)
1934+
action = super(ArgumentGroup, self)._add_action(action)
19281935
self._group_actions.append(action)
19291936
return action
19301937

19311938
def _remove_action(self, action):
1932-
super(_ArgumentGroup, self)._remove_action(action)
1939+
super(ArgumentGroup, self)._remove_action(action)
19331940
self._group_actions.remove(action)
19341941

19351942
def add_argument_group(self, *args, **kwargs):
19361943
raise ValueError('argument groups cannot be nested')
19371944

1938-
class _MutuallyExclusiveGroup(_ArgumentGroup):
1945+
# Deprecated alias, kept for backwards compatibility with code that
1946+
# imported this class under its old, undocumented private name.
1947+
_ArgumentGroup = ArgumentGroup
1948+
1949+
class MutuallyExclusiveGroup(ArgumentGroup):
19391950

19401951
def __init__(self, container, required=False):
1941-
super(_MutuallyExclusiveGroup, self).__init__(container)
1952+
super(MutuallyExclusiveGroup, self).__init__(container)
19421953
self.required = required
19431954
self._container = container
19441955

@@ -1957,6 +1968,10 @@ def _remove_action(self, action):
19571968
def add_mutually_exclusive_group(self, **kwargs):
19581969
raise ValueError('mutually exclusive groups cannot be nested')
19591970

1971+
# Deprecated alias, kept for backwards compatibility with code that
1972+
# imported this class under its old, undocumented private name.
1973+
_MutuallyExclusiveGroup = MutuallyExclusiveGroup
1974+
19601975
def _prog_name(prog=None):
19611976
if prog is not None:
19621977
return prog

Lib/test/test_argparse.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3175,6 +3175,44 @@ def test_alias_help(self):
31753175
3 3 help
31763176
"""))
31773177

3178+
class TestPublicClasses(TestCase):
3179+
"""Test that public names are used for objects returned by parser
3180+
methods that used to return only private, undocumented types
3181+
(see gh-101503)."""
3182+
3183+
def test_add_subparsers_return_type(self):
3184+
parser = argparse.ArgumentParser()
3185+
subparsers = parser.add_subparsers()
3186+
self.assertIsInstance(subparsers, argparse.SubParsersAction)
3187+
subparser = subparsers.add_parser('cmd')
3188+
self.assertIsInstance(subparser, argparse.ArgumentParser)
3189+
3190+
def test_add_argument_group_return_type(self):
3191+
parser = argparse.ArgumentParser()
3192+
group = parser.add_argument_group('title')
3193+
self.assertIsInstance(group, argparse.ArgumentGroup)
3194+
3195+
def test_add_mutually_exclusive_group_return_type(self):
3196+
parser = argparse.ArgumentParser()
3197+
group = parser.add_mutually_exclusive_group()
3198+
self.assertIsInstance(group, argparse.MutuallyExclusiveGroup)
3199+
self.assertIsInstance(group, argparse.ArgumentGroup)
3200+
3201+
def test_old_private_names_are_aliases(self):
3202+
# The old, undocumented private names are kept as aliases for
3203+
# backwards compatibility with code that imported them directly.
3204+
self.assertIs(argparse._SubParsersAction, argparse.SubParsersAction)
3205+
self.assertIs(argparse._ArgumentGroup, argparse.ArgumentGroup)
3206+
self.assertIs(argparse._MutuallyExclusiveGroup,
3207+
argparse.MutuallyExclusiveGroup)
3208+
3209+
def test_public_names_exported(self):
3210+
for name in ('SubParsersAction', 'ArgumentGroup',
3211+
'MutuallyExclusiveGroup'):
3212+
with self.subTest(name=name):
3213+
self.assertIn(name, argparse.__all__)
3214+
3215+
31783216
# ============
31793217
# Groups tests
31803218
# ============
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Add public names :class:`argparse.SubParsersAction`,
2+
:class:`argparse.ArgumentGroup`, and :class:`argparse.MutuallyExclusiveGroup`
3+
for the previously private, undocumented types returned by
4+
:meth:`~argparse.ArgumentParser.add_subparsers`,
5+
:meth:`~argparse.ArgumentParser.add_argument_group`, and
6+
:meth:`~argparse.ArgumentParser.add_mutually_exclusive_group` respectively,
7+
so that code (and type checkers) can reference them without relying on the
8+
old ``_SubParsersAction``, ``_ArgumentGroup``, and ``_MutuallyExclusiveGroup``
9+
names, which are kept as aliases for backwards compatibility.

0 commit comments

Comments
 (0)