Skip to content

Commit

Permalink
gh-83648: Support deprecation of options, arguments and subcommands i…
Browse files Browse the repository at this point in the history
…n argparse (GH-114086)
  • Loading branch information
serhiy-storchaka committed Feb 5, 2024
1 parent c32bae5 commit bb57ffd
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 27 deletions.
47 changes: 46 additions & 1 deletion Doc/library/argparse.rst
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
Expand Up @@ -169,6 +169,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
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)

0 comments on commit bb57ffd

Please sign in to comment.