Skip to content
29 changes: 18 additions & 11 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1908,13 +1908,16 @@ Argument groups
Note that any arguments not in your user-defined groups will end up back
in the usual "positional arguments" and "optional arguments" sections.

.. versionchanged:: 3.11
Calling :meth:`add_argument_group` on an argument group is deprecated.
This feature was never supported and does not always work correctly.
The function exists on the API by accident through inheritance and
will be removed in the future.
.. deprecated:: 3.11
Calling :meth:`add_argument_group` on an argument group is deprecated.
This feature was never supported and does not always work correctly.
The function exists on the API by accident through inheritance and
will be removed in the future.

.. deprecated:: 3.14
.. versionchanged:: 3.14
Calling :meth:`add_argument_group` on an argument group has been removed.

.. deprecated-removed:: 3.14
Passing prefix_chars_ to :meth:`add_argument_group`
is now deprecated.

Expand Down Expand Up @@ -1975,11 +1978,15 @@ Mutual exclusion
--foo FOO foo help
--bar BAR bar help

.. versionchanged:: 3.11
Calling :meth:`add_argument_group` or :meth:`add_mutually_exclusive_group`
on a mutually exclusive group is deprecated. These features were never
supported and do not always work correctly. The functions exist on the
API by accident through inheritance and will be removed in the future.
.. deprecated:: 3.11
Calling :meth:`add_argument_group` or :meth:`add_mutually_exclusive_group`
on a mutually exclusive group is deprecated. These features were never
supported and do not always work correctly. The functions exist on the
API by accident through inheritance and will be removed in the future.

.. deprecated-removed:: 3.14
Calling :meth:`add_argument_group` or :meth:`add_mutually_exclusive_group`
on a mutually exclusive group has been removed.


Parser defaults
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,14 @@ argparse
of :class:`!argparse.BooleanOptionalAction`.
They were deprecated since 3.12.

* Calling :meth:`add_argument_group` on an argument group, and calling
:meth:`add_argument_group` or :meth:`add_mutually_exclusive_group` on
a mutually exclusive group have been removed. This nesting was never
supported, did not always work correctly, and existed in the API by
accident through inheritance. This functionality has been deprecated
since Python 3.11.
(Contributed by Savannah Ostrowski in :gh:`xxxxxxx`.)

ast
---

Expand Down
20 changes: 3 additions & 17 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1709,14 +1709,7 @@ def _remove_action(self, action):
self._group_actions.remove(action)

def add_argument_group(self, *args, **kwargs):
import warnings
warnings.warn(
"Nesting argument groups is deprecated.",
category=DeprecationWarning,
stacklevel=2
)
return super().add_argument_group(*args, **kwargs)

raise ValueError('nested argument groups are not supported')

class _MutuallyExclusiveGroup(_ArgumentGroup):

Expand All @@ -1737,15 +1730,8 @@ def _remove_action(self, action):
self._container._remove_action(action)
self._group_actions.remove(action)

def add_mutually_exclusive_group(self, *args, **kwargs):
import warnings
warnings.warn(
"Nesting mutually exclusive groups is deprecated.",
category=DeprecationWarning,
stacklevel=2
)
return super().add_mutually_exclusive_group(*args, **kwargs)

def add_mutually_exclusive_group(self, **kwargs):
raise ValueError('nested mutually exclusive groups are not supported')

def _prog_name(prog=None):
if prog is not None:
Expand Down
83 changes: 15 additions & 68 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2954,6 +2954,13 @@ def test_group_prefix_chars_default(self):
self.assertEqual(msg, str(cm.warning))
self.assertEqual(cm.filename, __file__)

def test_nested_argument_group(self):
parser = argparse.ArgumentParser()
g = parser.add_argument_group()
self.assertRaisesRegex(ValueError,
'nested argument groups are not supported',
g.add_argument_group)

# ===================
# Parent parser tests
# ===================
Expand Down Expand Up @@ -3254,6 +3261,14 @@ def test_empty_group(self):
with self.assertRaises(ValueError):
parser.parse_args(['-h'])

def test_nested_mutex_groups(self):
parser = argparse.ArgumentParser(prog='PROG')
g = parser.add_mutually_exclusive_group()
g.add_argument("--spam")
self.assertRaisesRegex(ValueError,
'nested mutually exclusive groups are not supported',
g.add_mutually_exclusive_group)

class MEMixin(object):

def test_failures_when_not_required(self):
Expand Down Expand Up @@ -3621,55 +3636,6 @@ def get_parser(self, required):
-c c help
'''

class TestMutuallyExclusiveNested(MEMixin, TestCase):

# Nesting mutually exclusive groups is an undocumented feature
# that came about by accident through inheritance and has been
# the source of many bugs. It is deprecated and this test should
# eventually be removed along with it.

def get_parser(self, required):
parser = ErrorRaisingArgumentParser(prog='PROG')
group = parser.add_mutually_exclusive_group(required=required)
group.add_argument('-a')
group.add_argument('-b')
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
group2 = group.add_mutually_exclusive_group(required=required)
group2.add_argument('-c')
group2.add_argument('-d')
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
group3 = group2.add_mutually_exclusive_group(required=required)
group3.add_argument('-e')
group3.add_argument('-f')
return parser

usage_when_not_required = '''\
usage: PROG [-h] [-a A | -b B | [-c C | -d D | [-e E | -f F]]]
'''
usage_when_required = '''\
usage: PROG [-h] (-a A | -b B | (-c C | -d D | (-e E | -f F)))
'''

help = '''\

options:
-h, --help show this help message and exit
-a A
-b B
-c C
-d D
-e E
-f F
'''

# We are only interested in testing the behavior of format_usage().
test_failures_when_not_required = None
test_failures_when_required = None
test_successes_when_not_required = None
test_successes_when_required = None


class TestMutuallyExclusiveOptionalOptional(MEMixin, TestCase):
def get_parser(self, required=None):
Expand Down Expand Up @@ -4840,25 +4806,6 @@ def test_all_suppressed_mutex_with_optional_nargs(self):
usage = 'usage: PROG [-h]\n'
self.assertEqual(parser.format_usage(), usage)

def test_nested_mutex_groups(self):
parser = argparse.ArgumentParser(prog='PROG')
g = parser.add_mutually_exclusive_group()
g.add_argument("--spam")
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
gg = g.add_mutually_exclusive_group()
gg.add_argument("--hax")
gg.add_argument("--hox", help=argparse.SUPPRESS)
gg.add_argument("--hex")
g.add_argument("--eggs")
parser.add_argument("--num")

usage = textwrap.dedent('''\
usage: PROG [-h] [--spam SPAM | [--hax HAX | --hex HEX] | --eggs EGGS]
[--num NUM]
''')
self.assertEqual(parser.format_usage(), usage)

def test_long_mutex_groups_wrap(self):
parser = argparse.ArgumentParser(prog='PROG')
g = parser.add_mutually_exclusive_group()
Expand Down
Loading