Skip to content

gh-83648: Support deprecation of options, arguments and subcommands in argparse #114086

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 8 commits into from
Feb 5, 2024
Merged
47 changes: 46 additions & 1 deletion Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,8 @@ The add_argument() method
* dest_ - The name of the attribute to be added to the object returned by
:meth:`parse_args`.

* deprecated_ - Whether or not use of the argument is deprecated.

The following sections describe how each of these are used.


Expand Down Expand Up @@ -1439,6 +1441,34 @@ behavior::
>>> parser.parse_args('--foo XXX'.split())
Namespace(bar='XXX')


.. _deprecated:

deprecated
^^^^^^^^^^

During a project's lifetime, some arguments may need to be removed from the
command line. Before removing them, you should inform
your users that the arguments are deprecated and will be removed.
The ``deprecated`` keyword argument of
:meth:`~ArgumentParser.add_argument`, which defaults to ``False``,
specifies if the argument is deprecated and will be removed
in the future.
For arguments, if ``deprecated`` is ``True``, then a warning will be
printed to standard error when the argument is used::

>>> import argparse
>>> parser = argparse.ArgumentParser(prog='snake.py')
>>> parser.add_argument('--legs', default=0, type=int, deprecated=True)
>>> parser.parse_args([])
Namespace(legs=0)
>>> parser.parse_args(['--legs', '4']) # doctest: +SKIP
snake.py: warning: option '--legs' is deprecated
Namespace(legs=4)

.. versionchanged:: 3.13


Action classes
^^^^^^^^^^^^^^

Expand Down Expand Up @@ -1842,7 +1872,8 @@ Sub-commands

{foo,bar} additional help

Furthermore, ``add_parser`` supports an additional ``aliases`` argument,
Furthermore, :meth:`~_SubParsersAction.add_parser` supports an additional
*aliases* argument,
which allows multiple strings to refer to the same subparser. This example,
like ``svn``, aliases ``co`` as a shorthand for ``checkout``::

Expand All @@ -1853,6 +1884,20 @@ Sub-commands
>>> parser.parse_args(['co', 'bar'])
Namespace(foo='bar')

:meth:`~_SubParsersAction.add_parser` supports also an additional
*deprecated* argument, which allows to deprecate the subparser.

>>> import argparse
>>> parser = argparse.ArgumentParser(prog='chicken.py')
>>> subparsers = parser.add_subparsers()
>>> run = subparsers.add_parser('run')
>>> fly = subparsers.add_parser('fly', deprecated=True)
>>> parser.parse_args(['fly']) # doctest: +SKIP
chicken.py: warning: command 'fly' is deprecated
Namespace()

.. versionadded:: 3.13

One particularly effective way of handling sub-commands is to combine the use
of the :meth:`add_subparsers` method with calls to :meth:`set_defaults` so
that each subparser knows which Python function it should execute. For
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ New Modules
Improved Modules
================

argparse
--------

* Add parameter *deprecated* in methods
:meth:`~argparse.ArgumentParser.add_argument` and :meth:`!add_parser`
which allows to deprecate command-line options, positional arguments and
subcommands.
(Contributed by Serhiy Storchaka in :gh:`83648`).

array
-----

Expand Down
92 changes: 68 additions & 24 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,8 @@ def __init__(self,
choices=None,
required=False,
help=None,
metavar=None):
metavar=None,
deprecated=False):
self.option_strings = option_strings
self.dest = dest
self.nargs = nargs
Expand All @@ -854,6 +855,7 @@ def __init__(self,
self.required = required
self.help = help
self.metavar = metavar
self.deprecated = deprecated

def _get_kwargs(self):
names = [
Expand All @@ -867,6 +869,7 @@ def _get_kwargs(self):
'required',
'help',
'metavar',
'deprecated',
]
return [(name, getattr(self, name)) for name in names]

Expand All @@ -889,7 +892,8 @@ def __init__(self,
choices=_deprecated_default,
required=False,
help=None,
metavar=_deprecated_default):
metavar=_deprecated_default,
deprecated=False):

_option_strings = []
for option_string in option_strings:
Expand Down Expand Up @@ -927,7 +931,8 @@ def __init__(self,
choices=choices,
required=required,
help=help,
metavar=metavar)
metavar=metavar,
deprecated=deprecated)


def __call__(self, parser, namespace, values, option_string=None):
Expand All @@ -950,7 +955,8 @@ def __init__(self,
choices=None,
required=False,
help=None,
metavar=None):
metavar=None,
deprecated=False):
if nargs == 0:
raise ValueError('nargs for store actions must be != 0; if you '
'have nothing to store, actions such as store '
Expand All @@ -967,7 +973,8 @@ def __init__(self,
choices=choices,
required=required,
help=help,
metavar=metavar)
metavar=metavar,
deprecated=deprecated)

def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
Expand All @@ -982,15 +989,17 @@ def __init__(self,
default=None,
required=False,
help=None,
metavar=None):
metavar=None,
deprecated=False):
super(_StoreConstAction, self).__init__(
option_strings=option_strings,
dest=dest,
nargs=0,
const=const,
default=default,
required=required,
help=help)
help=help,
deprecated=deprecated)

def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, self.const)
Expand All @@ -1003,14 +1012,16 @@ def __init__(self,
dest,
default=False,
required=False,
help=None):
help=None,
deprecated=False):
super(_StoreTrueAction, self).__init__(
option_strings=option_strings,
dest=dest,
const=True,
default=default,
deprecated=deprecated,
required=required,
help=help)
help=help,
default=default)


class _StoreFalseAction(_StoreConstAction):
Expand All @@ -1020,14 +1031,16 @@ def __init__(self,
dest,
default=True,
required=False,
help=None):
help=None,
deprecated=False):
super(_StoreFalseAction, self).__init__(
option_strings=option_strings,
dest=dest,
const=False,
default=default,
required=required,
help=help)
help=help,
deprecated=deprecated)


class _AppendAction(Action):
Expand All @@ -1042,7 +1055,8 @@ def __init__(self,
choices=None,
required=False,
help=None,
metavar=None):
metavar=None,
deprecated=False):
if nargs == 0:
raise ValueError('nargs for append actions must be != 0; if arg '
'strings are not supplying the value to append, '
Expand All @@ -1059,7 +1073,8 @@ def __init__(self,
choices=choices,
required=required,
help=help,
metavar=metavar)
metavar=metavar,
deprecated=deprecated)

def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest, None)
Expand All @@ -1077,7 +1092,8 @@ def __init__(self,
default=None,
required=False,
help=None,
metavar=None):
metavar=None,
deprecated=False):
super(_AppendConstAction, self).__init__(
option_strings=option_strings,
dest=dest,
Expand All @@ -1086,7 +1102,8 @@ def __init__(self,
default=default,
required=required,
help=help,
metavar=metavar)
metavar=metavar,
deprecated=deprecated)

def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest, None)
Expand All @@ -1102,14 +1119,16 @@ def __init__(self,
dest,
default=None,
required=False,
help=None):
help=None,
deprecated=False):
super(_CountAction, self).__init__(
option_strings=option_strings,
dest=dest,
nargs=0,
default=default,
required=required,
help=help)
help=help,
deprecated=deprecated)

def __call__(self, parser, namespace, values, option_string=None):
count = getattr(namespace, self.dest, None)
Expand All @@ -1124,13 +1143,15 @@ def __init__(self,
option_strings,
dest=SUPPRESS,
default=SUPPRESS,
help=None):
help=None,
deprecated=False):
super(_HelpAction, self).__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs=0,
help=help)
help=help,
deprecated=deprecated)

def __call__(self, parser, namespace, values, option_string=None):
parser.print_help()
Expand All @@ -1144,7 +1165,8 @@ def __init__(self,
version=None,
dest=SUPPRESS,
default=SUPPRESS,
help="show program's version number and exit"):
help="show program's version number and exit",
deprecated=False):
super(_VersionAction, self).__init__(
option_strings=option_strings,
dest=dest,
Expand Down Expand Up @@ -1188,6 +1210,7 @@ def __init__(self,
self._parser_class = parser_class
self._name_parser_map = {}
self._choices_actions = []
self._deprecated = set()

super(_SubParsersAction, self).__init__(
option_strings=option_strings,
Expand All @@ -1198,7 +1221,7 @@ def __init__(self,
help=help,
metavar=metavar)

def add_parser(self, name, **kwargs):
def add_parser(self, name, *, deprecated=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 @@ -1226,6 +1249,10 @@ def add_parser(self, name, **kwargs):
for alias in aliases:
self._name_parser_map[alias] = parser

if deprecated:
self._deprecated.add(name)
self._deprecated.update(aliases)

return parser

def _get_subactions(self):
Expand All @@ -1241,21 +1268,25 @@ def __call__(self, parser, namespace, values, option_string=None):

# select the parser
try:
parser = self._name_parser_map[parser_name]
subparser = self._name_parser_map[parser_name]
except KeyError:
args = {'parser_name': parser_name,
'choices': ', '.join(self._name_parser_map)}
msg = _('unknown parser %(parser_name)r (choices: %(choices)s)') % args
raise ArgumentError(self, msg)

if parser_name in self._deprecated:
parser._warning(_("command '%(parser_name)s' is deprecated") %
{'parser_name': parser_name})

# parse all the remaining options into the namespace
# store any unrecognized options on the object, so that the top
# level parser can decide what to do with them

# In case this subparser defines new defaults, we parse them
# in a new namespace object and then update the original
# namespace for the relevant parts.
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
subnamespace, arg_strings = subparser.parse_known_args(arg_strings, None)
for key, value in vars(subnamespace).items():
setattr(namespace, key, value)

Expand Down Expand Up @@ -1975,6 +2006,7 @@ def _parse_known_args(self, arg_strings, namespace):
# converts arg strings to the appropriate and then takes the action
seen_actions = set()
seen_non_default_actions = set()
warned = set()

def take_action(action, argument_strings, option_string=None):
seen_actions.add(action)
Expand Down Expand Up @@ -2070,6 +2102,10 @@ def consume_optional(start_index):
# the Optional's string args stopped
assert action_tuples
for action, args, option_string in action_tuples:
if action.deprecated and option_string not in warned:
self._warning(_("option '%(option)s' is deprecated") %
{'option': option_string})
warned.add(option_string)
take_action(action, args, option_string)
return stop

Expand All @@ -2089,6 +2125,10 @@ def consume_positionals(start_index):
for action, arg_count in zip(positionals, arg_counts):
args = arg_strings[start_index: start_index + arg_count]
start_index += arg_count
if args and action.deprecated and action.dest not in warned:
self._warning(_("argument '%(argument_name)s' is deprecated") %
{'argument_name': action.dest})
warned.add(action.dest)
take_action(action, args)

# slice off the Positionals that we just parsed and return the
Expand Down Expand Up @@ -2650,3 +2690,7 @@ def error(self, message):
self.print_usage(_sys.stderr)
args = {'prog': self.prog, 'message': message}
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)

def _warning(self, message):
args = {'prog': self.prog, 'message': message}
self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr)
Loading