Skip to content

Commit 7c1d2be

Browse files
committed
bpo-39467: allow user to deprecate CLI arguments
1 parent b9783d2 commit 7c1d2be

File tree

4 files changed

+197
-2
lines changed

4 files changed

+197
-2
lines changed

Doc/library/argparse.rst

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,15 @@ The add_argument() method
698698
* default_ - The value produced if the argument is absent from the
699699
command line.
700700

701+
* deprecated_ - Define if the argument is deprecated.
702+
703+
* deprecated_reason_ - Custome deprecation warning message to display if
704+
the argument is deprecated.
705+
706+
* deprecated_pending_ - Define if the deprecation is pending. The argument
707+
is obsolete and expected to be deprecated in the future, but is not
708+
deprecated at the moment.
709+
701710
* type_ - The type to which the command-line argument should be converted.
702711

703712
* choices_ - A container of the allowable values for the argument.
@@ -1052,6 +1061,72 @@ command-line argument was not present::
10521061
Namespace(foo='1')
10531062

10541063

1064+
deprecated
1065+
^^^^^^^^^^
1066+
1067+
During projects lifecycle some arguments could be removed from the
1068+
command line, before removing these arguments definitively you would inform
1069+
your user that arguments are deprecated and will be removed.
1070+
The ``deprecated`` keyword argument of
1071+
:meth:`~ArgumentParser.add_argument`, whose value default to ``False``,
1072+
specifies if the argument is deprecated and will be removed
1073+
from the command-line available arguments in the future.
1074+
For arguments, if ``deprecated`` is ``True`` then a warning (``DeprecationWarning``) will be
1075+
emitted if the argument is given by user in the command line parameters::
1076+
1077+
>>> import argparse
1078+
>>> parser = argparse.ArgumentParser()
1079+
>>> parser.add_argument('bar', default=1)
1080+
>>> parser.add_argument('--foo', default=2, deprecated=True)
1081+
>>> parser.parse_args(['test'])
1082+
Namespace(bar='test', foo='2')
1083+
>>> parser.parse_args(['test', '--foo', '4'])
1084+
/home/cpython/Lib/argparse.py:1979: DeprecationWarning: Usage of parameter foo are deprecated
1085+
Namespace(bar='test', foo='4')
1086+
1087+
1088+
deprecated_reason
1089+
^^^^^^^^^^^^^^^^^
1090+
1091+
Custome deprecation warning message to display if the argument is deprecated.
1092+
If not given then a standard message will be displayed.
1093+
The ``deprecated_reason`` keyword argument of
1094+
:meth:`~ArgumentParser.add_argument`, allow to define a custome message to
1095+
display if the argument is deprecated and given by user in the command line parameters::
1096+
1097+
>>> import argparse
1098+
>>> parser = argparse.ArgumentParser()
1099+
>>> parser.add_argument('bar', default=1)
1100+
>>> parser.add_argument('--foo', default=2, deprecated=True, deprecated_reason='my custom message')
1101+
>>> parser.parse_args(['test'])
1102+
Namespace(bar='test', foo='2')
1103+
>>> parser.parse_args(['test', '--foo', '4'])
1104+
/home/cpython/Lib/argparse.py:1979: DeprecationWarning: my custome message
1105+
Namespace(bar='test', foo='4')
1106+
1107+
deprecated_pending
1108+
^^^^^^^^^^^^^^^^^^
1109+
1110+
Define if the deprecation is pending. Could be used to define that an argument
1111+
is obsolete and expected to be deprecated in the future, but is not
1112+
deprecated at the moment.
1113+
The ``deprecated_pending`` keyword argument of
1114+
:meth:`~ArgumentParser.add_argument`, whose value default to ``False``,
1115+
specifies if the argument is obsolete and expected to be deprecated in the future.
1116+
For arguments, if ``deprecated_pending`` is ``True`` then a warning
1117+
(``PendingDeprecationWarning``) will be emitted if the argument is given by
1118+
user in the command line parameters::
1119+
1120+
>>> import argparse
1121+
>>> parser = argparse.ArgumentParser()
1122+
>>> parser.add_argument('bar', default=1)
1123+
>>> parser.add_argument('--foo', default=2, deprecated=True, deprecated_pending=True)
1124+
>>> parser.parse_args(['test'])
1125+
Namespace(bar='test', foo='2')
1126+
>>> parser.parse_args(['test', '--foo', '4'])
1127+
/home/cpython/Lib/argparse.py:1979: PendingDeprecationWarning: The argument foo is obsolete and expected to be deprecated in the future
1128+
Namespace(bar='test', foo='4')
1129+
10551130
type
10561131
^^^^
10571132

Lib/argparse.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
import os as _os
8989
import re as _re
9090
import sys as _sys
91+
import warnings
9192

9293
from gettext import gettext as _, ngettext
9394

@@ -817,6 +818,9 @@ def __init__(self,
817818
nargs=None,
818819
const=None,
819820
default=None,
821+
deprecated=False,
822+
deprecated_reason=None,
823+
deprecated_pending=False,
820824
type=None,
821825
choices=None,
822826
required=False,
@@ -827,6 +831,14 @@ def __init__(self,
827831
self.nargs = nargs
828832
self.const = const
829833
self.default = default
834+
self.deprecated = deprecated
835+
if not deprecated_reason:
836+
deprecated_reason = f"Usage of parameter {dest} are deprecated"
837+
if deprecated_pending:
838+
deprecated_reason = f"The argument {dest} is obsolete and " \
839+
"expected to be deprecated in the future"
840+
self.deprecated_reason = deprecated_reason
841+
self.deprecated_pending=deprecated_pending
830842
self.type = type
831843
self.choices = choices
832844
self.required = required
@@ -840,6 +852,9 @@ def _get_kwargs(self):
840852
'nargs',
841853
'const',
842854
'default',
855+
'deprecated',
856+
'deprecated_reason',
857+
'deprecated_pending',
843858
'type',
844859
'choices',
845860
'help',
@@ -859,6 +874,9 @@ def __init__(self,
859874
dest,
860875
const=None,
861876
default=None,
877+
deprecated=False,
878+
deprecated_reason=None,
879+
deprecated_pending=False,
862880
type=None,
863881
choices=None,
864882
required=False,
@@ -881,6 +899,9 @@ def __init__(self,
881899
dest=dest,
882900
nargs=0,
883901
default=default,
902+
deprecated=deprecated,
903+
deprecated_reason=deprecated_reason,
904+
deprecated_pending=deprecated_pending,
884905
type=type,
885906
choices=choices,
886907
required=required,
@@ -903,6 +924,9 @@ def __init__(self,
903924
nargs=None,
904925
const=None,
905926
default=None,
927+
deprecated=False,
928+
deprecated_reason=None,
929+
deprecated_pending=False,
906930
type=None,
907931
choices=None,
908932
required=False,
@@ -920,6 +944,9 @@ def __init__(self,
920944
nargs=nargs,
921945
const=const,
922946
default=default,
947+
deprecated=deprecated,
948+
deprecated_reason=deprecated_reason,
949+
deprecated_pending=deprecated_pending,
923950
type=type,
924951
choices=choices,
925952
required=required,
@@ -937,6 +964,9 @@ def __init__(self,
937964
dest,
938965
const,
939966
default=None,
967+
deprecated=False,
968+
deprecated_reason=None,
969+
deprecated_pending=False,
940970
required=False,
941971
help=None,
942972
metavar=None):
@@ -946,6 +976,9 @@ def __init__(self,
946976
nargs=0,
947977
const=const,
948978
default=default,
979+
deprecated=deprecated,
980+
deprecated_reason=deprecated_reason,
981+
deprecated_pending=deprecated_pending,
949982
required=required,
950983
help=help)
951984

@@ -959,13 +992,19 @@ def __init__(self,
959992
option_strings,
960993
dest,
961994
default=False,
995+
deprecated=False,
996+
deprecated_reason=None,
997+
deprecated_pending=False,
962998
required=False,
963999
help=None):
9641000
super(_StoreTrueAction, self).__init__(
9651001
option_strings=option_strings,
9661002
dest=dest,
9671003
const=True,
9681004
default=default,
1005+
deprecated=deprecated,
1006+
deprecated_reason=deprecated_reason,
1007+
deprecated_pending=deprecated_pending,
9691008
required=required,
9701009
help=help)
9711010

@@ -976,13 +1015,19 @@ def __init__(self,
9761015
option_strings,
9771016
dest,
9781017
default=True,
1018+
deprecated=False,
1019+
deprecated_reason=None,
1020+
deprecated_pending=False,
9791021
required=False,
9801022
help=None):
9811023
super(_StoreFalseAction, self).__init__(
9821024
option_strings=option_strings,
9831025
dest=dest,
9841026
const=False,
9851027
default=default,
1028+
deprecated=deprecated,
1029+
deprecated_reason=deprecated_reason,
1030+
deprecated_pending=deprecated_pending,
9861031
required=required,
9871032
help=help)
9881033

@@ -995,6 +1040,9 @@ def __init__(self,
9951040
nargs=None,
9961041
const=None,
9971042
default=None,
1043+
deprecated=False,
1044+
deprecated_reason=None,
1045+
deprecated_pending=False,
9981046
type=None,
9991047
choices=None,
10001048
required=False,
@@ -1012,6 +1060,9 @@ def __init__(self,
10121060
nargs=nargs,
10131061
const=const,
10141062
default=default,
1063+
deprecated=deprecated,
1064+
deprecated_reason=deprecated_reason,
1065+
deprecated_pending=deprecated_pending,
10151066
type=type,
10161067
choices=choices,
10171068
required=required,
@@ -1032,6 +1083,9 @@ def __init__(self,
10321083
dest,
10331084
const,
10341085
default=None,
1086+
deprecated=False,
1087+
deprecated_reason=None,
1088+
deprecated_pending=False,
10351089
required=False,
10361090
help=None,
10371091
metavar=None):
@@ -1041,6 +1095,9 @@ def __init__(self,
10411095
nargs=0,
10421096
const=const,
10431097
default=default,
1098+
deprecated=deprecated,
1099+
deprecated_reason=deprecated_reason,
1100+
deprecated_pending=deprecated_pending,
10441101
required=required,
10451102
help=help,
10461103
metavar=metavar)
@@ -1058,13 +1115,19 @@ def __init__(self,
10581115
option_strings,
10591116
dest,
10601117
default=None,
1118+
deprecated=False,
1119+
deprecated_reason=None,
1120+
deprecated_pending=False,
10611121
required=False,
10621122
help=None):
10631123
super(_CountAction, self).__init__(
10641124
option_strings=option_strings,
10651125
dest=dest,
10661126
nargs=0,
10671127
default=default,
1128+
deprecated=deprecated,
1129+
deprecated_reason=deprecated_reason,
1130+
deprecated_pending=deprecated_pending,
10681131
required=required,
10691132
help=help)
10701133

@@ -1901,6 +1964,18 @@ def _parse_known_args(self, arg_strings, namespace):
19011964
pattern = 'O'
19021965
arg_string_pattern_parts.append(pattern)
19031966

1967+
# check if the arg is deprecated
1968+
# and warn only if it's given in the CLI paramters
1969+
for action in self._actions:
1970+
if arg_string.replace("-", "") == action.dest:
1971+
if not action.deprecated:
1972+
continue
1973+
warnings.warn(
1974+
action.deprecated_reason,
1975+
PendingDeprecationWarning \
1976+
if action.deprecated_pending \
1977+
else DeprecationWarning)
1978+
19041979
# join the pieces together to form the pattern
19051980
arg_strings_pattern = ''.join(arg_string_pattern_parts)
19061981

Lib/test/test_argparse.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4660,12 +4660,17 @@ def test_optional(self):
46604660
type='int',
46614661
nargs='+',
46624662
default=42,
4663+
deprecated=False,
4664+
deprecated_reason='foo bar',
4665+
deprecated_pending=False,
46634666
choices=[1, 2, 3],
46644667
help='HELP',
46654668
metavar='METAVAR')
46664669
string = (
46674670
"Action(option_strings=['--foo', '-a', '-b'], dest='b', "
4668-
"nargs='+', const=None, default=42, type='int', "
4671+
"nargs='+', const=None, default=42, deprecated=False, "
4672+
"deprecated_reason='foo bar', "
4673+
"deprecated_pending=False, type='int', "
46694674
"choices=[1, 2, 3], help='HELP', metavar='METAVAR')")
46704675
self.assertStringEqual(option, string)
46714676

@@ -4676,12 +4681,18 @@ def test_argument(self):
46764681
type=float,
46774682
nargs='?',
46784683
default=2.5,
4684+
deprecated=False,
4685+
deprecated_reason='foo bar',
4686+
deprecated_pending=False,
46794687
choices=[0.5, 1.5, 2.5],
46804688
help='H HH H',
46814689
metavar='MV MV MV')
46824690
string = (
46834691
"Action(option_strings=[], dest='x', nargs='?', "
4684-
"const=None, default=2.5, type=%r, choices=[0.5, 1.5, 2.5], "
4692+
"const=None, default=2.5, deprecated=False, "
4693+
"deprecated_reason='foo bar', "
4694+
"deprecated_pending=False, "
4695+
"type=%r, choices=[0.5, 1.5, 2.5], "
46854696
"help='H HH H', metavar='MV MV MV')" % float)
46864697
self.assertStringEqual(argument, string)
46874698

@@ -4874,6 +4885,39 @@ def spam(string_to_convert):
48744885
args = parser.parse_args('--foo spam!'.split())
48754886
self.assertEqual(NS(foo='foo_converted'), args)
48764887

4888+
4889+
# =============================================
4890+
# Check that deprecated arguments raise warning
4891+
# =============================================
4892+
4893+
class TestTypeFunctionCallWithDeprecated(TestCase):
4894+
4895+
def test_type_function_call_with_deprecated(self):
4896+
parser = argparse.ArgumentParser()
4897+
parser.add_argument('--foo', deprecated=True, default='bar')
4898+
with support.captured_stderr() as stderr:
4899+
parser.parse_args(['--foo', 'spam'])
4900+
stderr = stderr.getvalue()
4901+
self.assertIn("DeprecationWarning", stderr)
4902+
4903+
def test_type_function_call_with_pending_deprecated(self):
4904+
parser = argparse.ArgumentParser()
4905+
parser.add_argument('--foo', deprecated=True,
4906+
deprecated_pending=True, default='bar')
4907+
with support.captured_stderr() as stderr:
4908+
parser.parse_args(['--foo', 'spam'])
4909+
stderr = stderr.getvalue()
4910+
self.assertIn("PendingDeprecationWarning", stderr)
4911+
4912+
def test_type_function_call_with_deprecated_custome_msg(self):
4913+
parser = argparse.ArgumentParser()
4914+
parser.add_argument('--foo', deprecated=True,
4915+
deprecated_reason="foo bar", default='bar')
4916+
with support.captured_stderr() as stderr:
4917+
parser.parse_args(['--foo', 'spam'])
4918+
stderr = stderr.getvalue()
4919+
self.assertIn("DeprecationWarning: foo bar", stderr)
4920+
48774921
# ==================================================================
48784922
# Check semantics regarding the default argument and type conversion
48794923
# ==================================================================
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow to deprecate CLI arguments with argparse

0 commit comments

Comments
 (0)