Correct way to deprecate parameter alias in click

2024/10/8 10:53:15

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.

Answer

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:

Code:

import click
import warningsclass 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 nameused to invoke the option to see if it is preferred"""parser = super(DeprecatedOptionsCommand, self).make_parser(ctx)# get the parser optionsoptions = set(parser._short_opt.values())options |= set(parser._long_opt.values())for option in options:if not isinstance(option.obj, DeprecatedOption):continuedef make_process(an_option):""" Construct a closure to the parser option processor """orig_process = an_option.processdeprecated = 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' topick option from a dict, see if it is deprecated """# reach up the stack and get 'opt'import inspectframe = inspect.currentframe()try:opt = frame.f_back.f_locals.get('opt')finally:del frameif opt in deprecated:msg = "'{}' has been deprecated, use '{}'"warnings.warn(msg.format(opt, preferred),FutureWarning)return orig_process(value, state)return processoption.process = make_process(option)return parser

Using the Custom Classes:

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')

How does this work?

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.

Test Code:

@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, timetime.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

Results:

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.
https://en.xdnf.cn/q/70133.html

Related Q&A

How to rename a file with non-ASCII character encoding to ASCII

I have the file name, "abc枚.xlsx", containing some kind of non-ASCII character encoding and Id like to remove all non-ASCII characters to rename it to "abc.xlsx".Here is what Ive t…

draw random element in numpy

I have an array of element probabilities, lets say [0.1, 0.2, 0.5, 0.2]. The array sums up to 1.0.Using plain Python or numpy, I want to draw elements proportional to their probability: the first eleme…

Python Gevent Pywsgi server with ssl

Im trying to use gevent.pywsgi.WSGIServer to wrap a Flask app. Everything works fine, however, when I try to add a key and a certificate for ssl, its not even able to accept any clients anymore.This is…

unexpected keyword argument buffering - python client

I am receiving the error as "getresponse() got an unexpected keyword argument buffering". Complete error log is :[INFO ] Kivy v1.8.0 [INFO ] [Logger ] Record lo…

numpy and pandas timedelta error

In Python I have an array of dates generated (or read from a CSV-file) using pandas, and I want to add one year to each date. I can get it working using pandas but not using numpy. What am I doing wron…

Pandas - split large excel file

I have an excel file with about 500,000 rows and I want to split it to several excel file, each with 50,000 rows.I want to do it with pandas so it will be the quickest and easiest.any ideas how to make…

Unable to verify secret hash for client at REFRESH_TOKEN_AUTH

Problem"Unable to verify secret hash for client ..." at REFRESH_TOKEN_AUTH auth flow. {"Error": {"Code": "NotAuthorizedException","Message": "Unab…

save a dependecy graph in python

I am using in python3 the stanford dependency parser to parse a sentence, which returns a dependency graph. import pickle from nltk.parse.stanford import StanfordDependencyParserparser = StanfordDepend…

What are the specific rules for constant folding?

I just realized that CPython seems to treat constant expressions, which represent the same value, differently with respect to constant folding. For example:>>> import dis >>> dis.dis(…

installing opencv for python on mavericks

I am trying to install opencv on a Macbook Pro late 2013 with mavericks. I didnt find any binaries so I am trying to build it. I tried http://www.guidefreitas.com/installing-opencv-2-4-2-on-mac-osx-mou…