Skip to content

GH-127133: Remove ability to nest argument groups & mutually exclusive groups #127186

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

Merged
merged 12 commits into from
Nov 24, 2024
19 changes: 9 additions & 10 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1926,11 +1926,10 @@ 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-removed:: 3.11 3.14
Calling :meth:`add_argument_group` on an argument group now raises an
exception. This nesting was never supported, often failed to work
correctly, and was unintentionally exposed through inheritance.

.. deprecated:: 3.14
Passing prefix_chars_ to :meth:`add_argument_group`
Expand Down Expand Up @@ -1993,11 +1992,11 @@ 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-removed:: 3.11 3.14
Calling :meth:`add_argument_group` or :meth:`add_mutually_exclusive_group`
on a mutually exclusive group now raises an exception. This nesting was
never supported, often failed to work correctly, and was unintentionally
exposed through inheritance.


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:`~argparse.ArgumentParser.add_argument_group` on an argument
group, and calling :meth:`~argparse.ArgumentParser.add_argument_group` or
:meth:`~argparse.ArgumentParser.add_mutually_exclusive_group` on a mutually
exclusive group now raise exceptions. This nesting was never supported,
often failed to work correctly, and was unintentionally exposed through
inheritance. This functionality has been deprecated since Python 3.11.
(Contributed by Savannah Ostrowski in :gh:`127186`.)

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('argument groups cannot be nested')

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('mutually exclusive groups cannot be nested')

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 @@ -2997,6 +2997,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,
'argument groups cannot be nested',
g.add_argument_group)

# ===================
# Parent parser tests
# ===================
Expand Down Expand Up @@ -3297,6 +3304,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,
'mutually exclusive groups cannot be nested',
g.add_mutually_exclusive_group)

class MEMixin(object):

def test_failures_when_not_required(self):
Expand Down Expand Up @@ -3664,55 +3679,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 @@ -4883,25 +4849,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Calling :meth:`argparse.ArgumentParser.add_argument_group` on an argument group,
and calling :meth:`argparse.ArgumentParser.add_argument_group` or
:meth:`argparse.ArgumentParser.add_mutually_exclusive_group` on a mutually
exclusive group now raise exceptions. This nesting was never supported, often
failed to work correctly, and was unintentionally exposed through inheritance.
This functionality has been deprecated since Python 3.11.
Loading