Skip to content

Commit bb57ffd

Browse files
gh-83648: Support deprecation of options, arguments and subcommands in argparse (GH-114086)
1 parent c32bae5 commit bb57ffd

File tree

5 files changed

+262
-27
lines changed

5 files changed

+262
-27
lines changed

Doc/library/argparse.rst

+46-1
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,8 @@ The add_argument() method
777777
* dest_ - The name of the attribute to be added to the object returned by
778778
:meth:`parse_args`.
779779

780+
* deprecated_ - Whether or not use of the argument is deprecated.
781+
780782
The following sections describe how each of these are used.
781783

782784

@@ -1439,6 +1441,34 @@ behavior::
14391441
>>> parser.parse_args('--foo XXX'.split())
14401442
Namespace(bar='XXX')
14411443

1444+
1445+
.. _deprecated:
1446+
1447+
deprecated
1448+
^^^^^^^^^^
1449+
1450+
During a project's lifetime, some arguments may need to be removed from the
1451+
command line. Before removing them, you should inform
1452+
your users that the arguments are deprecated and will be removed.
1453+
The ``deprecated`` keyword argument of
1454+
:meth:`~ArgumentParser.add_argument`, which defaults to ``False``,
1455+
specifies if the argument is deprecated and will be removed
1456+
in the future.
1457+
For arguments, if ``deprecated`` is ``True``, then a warning will be
1458+
printed to standard error when the argument is used::
1459+
1460+
>>> import argparse
1461+
>>> parser = argparse.ArgumentParser(prog='snake.py')
1462+
>>> parser.add_argument('--legs', default=0, type=int, deprecated=True)
1463+
>>> parser.parse_args([])
1464+
Namespace(legs=0)
1465+
>>> parser.parse_args(['--legs', '4']) # doctest: +SKIP
1466+
snake.py: warning: option '--legs' is deprecated
1467+
Namespace(legs=4)
1468+
1469+
.. versionchanged:: 3.13
1470+
1471+
14421472
Action classes
14431473
^^^^^^^^^^^^^^
14441474

@@ -1842,7 +1872,8 @@ Sub-commands
18421872

18431873
{foo,bar} additional help
18441874

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

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

1887+
:meth:`~_SubParsersAction.add_parser` supports also an additional
1888+
*deprecated* argument, which allows to deprecate the subparser.
1889+
1890+
>>> import argparse
1891+
>>> parser = argparse.ArgumentParser(prog='chicken.py')
1892+
>>> subparsers = parser.add_subparsers()
1893+
>>> run = subparsers.add_parser('run')
1894+
>>> fly = subparsers.add_parser('fly', deprecated=True)
1895+
>>> parser.parse_args(['fly']) # doctest: +SKIP
1896+
chicken.py: warning: command 'fly' is deprecated
1897+
Namespace()
1898+
1899+
.. versionadded:: 3.13
1900+
18561901
One particularly effective way of handling sub-commands is to combine the use
18571902
of the :meth:`add_subparsers` method with calls to :meth:`set_defaults` so
18581903
that each subparser knows which Python function it should execute. For

Doc/whatsnew/3.13.rst

+9
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ New Modules
169169
Improved Modules
170170
================
171171

172+
argparse
173+
--------
174+
175+
* Add parameter *deprecated* in methods
176+
:meth:`~argparse.ArgumentParser.add_argument` and :meth:`!add_parser`
177+
which allows to deprecate command-line options, positional arguments and
178+
subcommands.
179+
(Contributed by Serhiy Storchaka in :gh:`83648`).
180+
172181
array
173182
-----
174183

Lib/argparse.py

+68-24
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,8 @@ def __init__(self,
843843
choices=None,
844844
required=False,
845845
help=None,
846-
metavar=None):
846+
metavar=None,
847+
deprecated=False):
847848
self.option_strings = option_strings
848849
self.dest = dest
849850
self.nargs = nargs
@@ -854,6 +855,7 @@ def __init__(self,
854855
self.required = required
855856
self.help = help
856857
self.metavar = metavar
858+
self.deprecated = deprecated
857859

858860
def _get_kwargs(self):
859861
names = [
@@ -867,6 +869,7 @@ def _get_kwargs(self):
867869
'required',
868870
'help',
869871
'metavar',
872+
'deprecated',
870873
]
871874
return [(name, getattr(self, name)) for name in names]
872875

@@ -889,7 +892,8 @@ def __init__(self,
889892
choices=_deprecated_default,
890893
required=False,
891894
help=None,
892-
metavar=_deprecated_default):
895+
metavar=_deprecated_default,
896+
deprecated=False):
893897

894898
_option_strings = []
895899
for option_string in option_strings:
@@ -927,7 +931,8 @@ def __init__(self,
927931
choices=choices,
928932
required=required,
929933
help=help,
930-
metavar=metavar)
934+
metavar=metavar,
935+
deprecated=deprecated)
931936

932937

933938
def __call__(self, parser, namespace, values, option_string=None):
@@ -950,7 +955,8 @@ def __init__(self,
950955
choices=None,
951956
required=False,
952957
help=None,
953-
metavar=None):
958+
metavar=None,
959+
deprecated=False):
954960
if nargs == 0:
955961
raise ValueError('nargs for store actions must be != 0; if you '
956962
'have nothing to store, actions such as store '
@@ -967,7 +973,8 @@ def __init__(self,
967973
choices=choices,
968974
required=required,
969975
help=help,
970-
metavar=metavar)
976+
metavar=metavar,
977+
deprecated=deprecated)
971978

972979
def __call__(self, parser, namespace, values, option_string=None):
973980
setattr(namespace, self.dest, values)
@@ -982,15 +989,17 @@ def __init__(self,
982989
default=None,
983990
required=False,
984991
help=None,
985-
metavar=None):
992+
metavar=None,
993+
deprecated=False):
986994
super(_StoreConstAction, self).__init__(
987995
option_strings=option_strings,
988996
dest=dest,
989997
nargs=0,
990998
const=const,
991999
default=default,
9921000
required=required,
993-
help=help)
1001+
help=help,
1002+
deprecated=deprecated)
9941003

9951004
def __call__(self, parser, namespace, values, option_string=None):
9961005
setattr(namespace, self.dest, self.const)
@@ -1003,14 +1012,16 @@ def __init__(self,
10031012
dest,
10041013
default=False,
10051014
required=False,
1006-
help=None):
1015+
help=None,
1016+
deprecated=False):
10071017
super(_StoreTrueAction, self).__init__(
10081018
option_strings=option_strings,
10091019
dest=dest,
10101020
const=True,
1011-
default=default,
1021+
deprecated=deprecated,
10121022
required=required,
1013-
help=help)
1023+
help=help,
1024+
default=default)
10141025

10151026

10161027
class _StoreFalseAction(_StoreConstAction):
@@ -1020,14 +1031,16 @@ def __init__(self,
10201031
dest,
10211032
default=True,
10221033
required=False,
1023-
help=None):
1034+
help=None,
1035+
deprecated=False):
10241036
super(_StoreFalseAction, self).__init__(
10251037
option_strings=option_strings,
10261038
dest=dest,
10271039
const=False,
10281040
default=default,
10291041
required=required,
1030-
help=help)
1042+
help=help,
1043+
deprecated=deprecated)
10311044

10321045

10331046
class _AppendAction(Action):
@@ -1042,7 +1055,8 @@ def __init__(self,
10421055
choices=None,
10431056
required=False,
10441057
help=None,
1045-
metavar=None):
1058+
metavar=None,
1059+
deprecated=False):
10461060
if nargs == 0:
10471061
raise ValueError('nargs for append actions must be != 0; if arg '
10481062
'strings are not supplying the value to append, '
@@ -1059,7 +1073,8 @@ def __init__(self,
10591073
choices=choices,
10601074
required=required,
10611075
help=help,
1062-
metavar=metavar)
1076+
metavar=metavar,
1077+
deprecated=deprecated)
10631078

10641079
def __call__(self, parser, namespace, values, option_string=None):
10651080
items = getattr(namespace, self.dest, None)
@@ -1077,7 +1092,8 @@ def __init__(self,
10771092
default=None,
10781093
required=False,
10791094
help=None,
1080-
metavar=None):
1095+
metavar=None,
1096+
deprecated=False):
10811097
super(_AppendConstAction, self).__init__(
10821098
option_strings=option_strings,
10831099
dest=dest,
@@ -1086,7 +1102,8 @@ def __init__(self,
10861102
default=default,
10871103
required=required,
10881104
help=help,
1089-
metavar=metavar)
1105+
metavar=metavar,
1106+
deprecated=deprecated)
10901107

10911108
def __call__(self, parser, namespace, values, option_string=None):
10921109
items = getattr(namespace, self.dest, None)
@@ -1102,14 +1119,16 @@ def __init__(self,
11021119
dest,
11031120
default=None,
11041121
required=False,
1105-
help=None):
1122+
help=None,
1123+
deprecated=False):
11061124
super(_CountAction, self).__init__(
11071125
option_strings=option_strings,
11081126
dest=dest,
11091127
nargs=0,
11101128
default=default,
11111129
required=required,
1112-
help=help)
1130+
help=help,
1131+
deprecated=deprecated)
11131132

11141133
def __call__(self, parser, namespace, values, option_string=None):
11151134
count = getattr(namespace, self.dest, None)
@@ -1124,13 +1143,15 @@ def __init__(self,
11241143
option_strings,
11251144
dest=SUPPRESS,
11261145
default=SUPPRESS,
1127-
help=None):
1146+
help=None,
1147+
deprecated=False):
11281148
super(_HelpAction, self).__init__(
11291149
option_strings=option_strings,
11301150
dest=dest,
11311151
default=default,
11321152
nargs=0,
1133-
help=help)
1153+
help=help,
1154+
deprecated=deprecated)
11341155

11351156
def __call__(self, parser, namespace, values, option_string=None):
11361157
parser.print_help()
@@ -1144,7 +1165,8 @@ def __init__(self,
11441165
version=None,
11451166
dest=SUPPRESS,
11461167
default=SUPPRESS,
1147-
help="show program's version number and exit"):
1168+
help="show program's version number and exit",
1169+
deprecated=False):
11481170
super(_VersionAction, self).__init__(
11491171
option_strings=option_strings,
11501172
dest=dest,
@@ -1188,6 +1210,7 @@ def __init__(self,
11881210
self._parser_class = parser_class
11891211
self._name_parser_map = {}
11901212
self._choices_actions = []
1213+
self._deprecated = set()
11911214

11921215
super(_SubParsersAction, self).__init__(
11931216
option_strings=option_strings,
@@ -1198,7 +1221,7 @@ def __init__(self,
11981221
help=help,
11991222
metavar=metavar)
12001223

1201-
def add_parser(self, name, **kwargs):
1224+
def add_parser(self, name, *, deprecated=False, **kwargs):
12021225
# set prog from the existing prefix
12031226
if kwargs.get('prog') is None:
12041227
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
@@ -1226,6 +1249,10 @@ def add_parser(self, name, **kwargs):
12261249
for alias in aliases:
12271250
self._name_parser_map[alias] = parser
12281251

1252+
if deprecated:
1253+
self._deprecated.add(name)
1254+
self._deprecated.update(aliases)
1255+
12291256
return parser
12301257

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

12421269
# select the parser
12431270
try:
1244-
parser = self._name_parser_map[parser_name]
1271+
subparser = self._name_parser_map[parser_name]
12451272
except KeyError:
12461273
args = {'parser_name': parser_name,
12471274
'choices': ', '.join(self._name_parser_map)}
12481275
msg = _('unknown parser %(parser_name)r (choices: %(choices)s)') % args
12491276
raise ArgumentError(self, msg)
12501277

1278+
if parser_name in self._deprecated:
1279+
parser._warning(_("command '%(parser_name)s' is deprecated") %
1280+
{'parser_name': parser_name})
1281+
12511282
# parse all the remaining options into the namespace
12521283
# store any unrecognized options on the object, so that the top
12531284
# level parser can decide what to do with them
12541285

12551286
# In case this subparser defines new defaults, we parse them
12561287
# in a new namespace object and then update the original
12571288
# namespace for the relevant parts.
1258-
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
1289+
subnamespace, arg_strings = subparser.parse_known_args(arg_strings, None)
12591290
for key, value in vars(subnamespace).items():
12601291
setattr(namespace, key, value)
12611292

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

19792011
def take_action(action, argument_strings, option_string=None):
19802012
seen_actions.add(action)
@@ -2070,6 +2102,10 @@ def consume_optional(start_index):
20702102
# the Optional's string args stopped
20712103
assert action_tuples
20722104
for action, args, option_string in action_tuples:
2105+
if action.deprecated and option_string not in warned:
2106+
self._warning(_("option '%(option)s' is deprecated") %
2107+
{'option': option_string})
2108+
warned.add(option_string)
20732109
take_action(action, args, option_string)
20742110
return stop
20752111

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

20942134
# slice off the Positionals that we just parsed and return the
@@ -2650,3 +2690,7 @@ def error(self, message):
26502690
self.print_usage(_sys.stderr)
26512691
args = {'prog': self.prog, 'message': message}
26522692
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
2693+
2694+
def _warning(self, message):
2695+
args = {'prog': self.prog, 'message': message}
2696+
self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr)

0 commit comments

Comments
 (0)