I want to deprecate a parameter alias in click
(say, switch from underscores to dashes). For a while, I want both formulations to be valid, but throw a FutureWarning
when the parameter is invoked with the to-be-deprecated alias. However, I have not found a way to access the actual alias a parameter was invoked with.
In short, I want:
click.command()
click.option('--old', '--new')
def cli(*args, **kwargs):
...
to raise a warning when the option is invoked with --old
, but not when it is invoked with --new
. Is there a clean way to do this that doesn't rely too much on undocumented behavior?
I tried adding a callback to the click.option
, but it seems to be called after the option is parsed, and the arguments contain no information which alias was actually used. A solution would probably overload click.Option
or even click.Command
, but I don't know where the actual parsing takes place.
To be able to know what option name was used to select a a particular option, I suggest you monkey patch the option parser using some custom classes. This solution ends up inheriting from both click.Option
and click.Command
:
import click
import warnings
class DeprecatedOption(click.Option):
def __init__(self, *args, **kwargs):
self.deprecated = kwargs.pop('deprecated', ())
self.preferred = kwargs.pop('preferred', args[0][-1])
super(DeprecatedOption, self).__init__(*args, **kwargs)
class DeprecatedOptionsCommand(click.Command):
def make_parser(self, ctx):
"""Hook 'make_parser' and during processing check the name
used to invoke the option to see if it is preferred"""
parser = super(DeprecatedOptionsCommand, self).make_parser(ctx)
# get the parser options
options = set(parser._short_opt.values())
options |= set(parser._long_opt.values())
for option in options:
if not isinstance(option.obj, DeprecatedOption):
continue
def make_process(an_option):
""" Construct a closure to the parser option processor """
orig_process = an_option.process
deprecated = getattr(an_option.obj, 'deprecated', None)
preferred = getattr(an_option.obj, 'preferred', None)
msg = "Expected `deprecated` value for `{}`"
assert deprecated is not None, msg.format(an_option.obj.name)
def process(value, state):
"""The function above us on the stack used 'opt' to
pick option from a dict, see if it is deprecated """
# reach up the stack and get 'opt'
import inspect
frame = inspect.currentframe()
try:
opt = frame.f_back.f_locals.get('opt')
finally:
del frame
if opt in deprecated:
msg = "'{}' has been deprecated, use '{}'"
warnings.warn(msg.format(opt, preferred),
FutureWarning)
return orig_process(value, state)
return process
option.process = make_process(option)
return parser
First add a cls
parameter to @click.command
like:
@click.command(cls=DeprecatedOptionsCommand)
Then for each option which has deprecated values add the cls
and deprecated
values like:
@click.option('--old1', '--new1', cls=DeprecatedOption, deprecated=['--old1'])
And optionally you can add a preferred
value like:
@click.option('--old2', '-x', '--new2', cls=DeprecatedOption,
deprecated=['--old2'], preferred='-x')
There are two custom classes here, they derive from two click
classes. A custom click.Command
and click.Option
.
This works because click is a well designed OO framework. The @click.command()
decorator usually instantiates a click.Command
object but allows this behavior to be over-ridden with the cls
parameter. @click.option()
works similarly. So it is a relatively easy matter to inherit from click.Command
and click.Option
in our own classes and over ride the desired methods.
In the case of the custom click.Option
: DeprecatedOption
, we add two new keyword attributes: deprecated
and preferred
. deprecated
is required and is a list of command names that will be warned about. preferred
is optional, and specifies the recommended command name. It is a string and will default to the last command name in the option line.
In the case of the custom click.Command
: DeprecatedOptionsCommand
, we override the make_parser()
method. This allows us to monkey patch the option parser instances in the parser instance. The parser is not really intended for expansion like Command
and Option
so we have to get a little more creative.
In this case all option processing in the parser goes through the process()
method. Here we monkey patch that method, and in the patched method we look up one level in the stack frame to find the opt
variable which is the name used to find the option. Then, if this value is in the deprecated
list, we issue the warning.
This code reaches into some private structures in the parser, but this is unlikely to be an issue. This parser code was last changed 4 years ago. The parser code is unlikely to undergo significant revisions.
@click.command(cls=DeprecatedOptionsCommand)
@click.option('--old1', '--new1', cls=DeprecatedOption,
deprecated=['--old1'])
@click.option('--old2', '-x', '--new2', cls=DeprecatedOption,
deprecated=['--old2'], preferred='-x')
def cli(**kwargs):
click.echo("{}".format(kwargs))
if __name__ == "__main__":
commands = (
'--old1 5',
'--new1 6',
'--old2 7',
'--new2 8',
'-x 9',
'',
'--help',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
cli(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --old1 5
{'new1': '5', 'new2': None}
C:/Users/stephen/Documents/src/testcode/test.py:71: FutureWarning: '--old1' has been deprecated, use '--new1'
FutureWarning)
-----------
> --new1 6
{'new1': '6', 'new2': None}
-----------
> --old2 7
{'new2': '7', 'new1': None}
C:/Users/stephen/Documents/src/testcode/test.py:71: FutureWarning: '--old2' has been deprecated, use '-x'
FutureWarning)
-----------
> --new2 8
{'new2': '8', 'new1': None}
-----------
> -x 9
{'new2': '9', 'new1': None}
-----------
>
{'new1': None, 'new2': None}
-----------
> --help
Usage: test.py [OPTIONS]
Options:
--old1, --new1 TEXT
-x, --old2, --new2 TEXT
--help Show this message and exit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With