Чтобы узнать, какое имя опции использовалось для выбора конкретной опции, я предлагаю вам обезопасить патч парсера опций с помощью некоторых пользовательских классов. Это решение заканчивается наследованием от click.Option
и 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
Использование пользовательских классов:
Сначала добавьте cls
параметр к @click.command
, например:
@click.command(cls=DeprecatedOptionsCommand)
Затем для каждой опции, которая имеет устаревшие значения, добавьте значения cls
и deprecated
, например:
@click.option('--old1', '--new1', cls=DeprecatedOption, deprecated=['--old1'])
И при желании вы можете добавить preferred
значение, например:
@click.option('--old2', '-x', '--new2', cls=DeprecatedOption,
deprecated=['--old2'], preferred='-x')
Как это работает?
Здесь есть два пользовательских класса, они происходят от двух click
классов. Кастом click.Command
и click.Option
.
Это работает, потому что click - это хорошо разработанная OO-инфраструктура Декоратор @click.command()
обычно создает экземпляр объекта click.Command
, но позволяет переопределить это поведение параметром cls
. @click.option()
работает аналогично. Так что относительно легко унаследовать от click.Command
и click.Option
в наших собственных классах и переопределить нужные методы.
В случае пользовательского click.Option
: DeprecatedOption
мы добавляем два новых атрибута ключевого слова: deprecated
и preferred
. deprecated
является обязательным и представляет собой список имен команд, о которых будут предупреждены. preferred
является необязательным и указывает рекомендуемое имя команды. Это строка, которая по умолчанию будет соответствовать имени последней команды в строке параметров.
В случае пользовательского click.Command
: DeprecatedOptionsCommand
мы переопределяем метод make_parser()
. Это позволяет нам монтировать патчи экземпляров параметров в экземпляре анализатора. Парсер на самом деле не предназначен для расширения, как Command
и Option
, поэтому нам нужно стать немного более креативным.
В этом случае вся обработка опций в синтаксическом анализаторе выполняется методом process()
. Здесь мы обезьяны исправляем этот метод, а в исправленном методе мы ищем один уровень в кадре стека, чтобы найти переменную opt
, которая является именем, используемым для поиска опции. Затем, если это значение находится в списке deprecated
, мы выдаем предупреждение.
Этот код проникает в некоторые частные структуры в синтаксическом анализаторе, но это вряд ли является проблемой. Этот код парсера последний раз изменялся 4 года назад. Код парсера вряд ли подвергнется существенным изменениям.
Тестовый код:
@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.