Расширенные шаблоны
Помимо стандартных функций, реализованных в самой библиотеке, существует множество шаблонов, которые можно реализовать, расширяя Click. Эта страница должна дать представление о том, чего можно достичь.
Псевдонимы команд
Многие инструменты поддерживают псевдонимы команд (см. Пример псевдонима команды). Например, можно настроить git для приема git ci в качестве псевдонима для git commit. Другие инструменты также поддерживают автоматическое обнаружение псевдонимов, автоматически сокращая их.
Click не поддерживает это «из коробки», но очень легко настроить Group или любой другой MultiCommand для предоставления этой функциональности.
Как объясняется в Настраиваемые многофункциональные команды, многофункциональная команда может предоставлять два метода: list_commands() и get_command(). В этом конкретном случае вам нужно переопределить только последний, так как обычно вы не хотите перечислять псевдонимы на странице справки, чтобы избежать путаницы.
Следующий пример реализует подкласс Group, который принимает префикс для команды. Если была команда push, она бы принимала pus в качестве псевдонима (при условии, что он уникален):
class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name):
rv = click.Group.get_command(self, ctx, cmd_name)
if rv is not None:
return rv
matches = [x for x in self.list_commands(ctx)
if x.startswith(cmd_name)]
if not matches:
return None
elif len(matches) == 1:
return click.Group.get_command(self, ctx, matches[0])
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
def resolve_command(self, ctx, args):
# always return the full command name
_, cmd, args = super().resolve_command(ctx, args)
return cmd.name, cmd, args
И ее можно использовать следующим образом:
@click.command(cls=AliasedGroup)
def cli():
pass
@cli.command()
def push():
pass
@cli.command()
def pop():
pass
Модификации параметров
Параметры (опции и аргументы) передаются в обратные вызовы команд, как вы уже видели. Один из распространенных способов предотвращения передачи параметра в обратный вызов — аргумент expose_value для параметра, который полностью скрывает параметр. Это работает так, что объект Context имеет атрибут params, который представляет собой словарь всех параметров. Всё, что находится в этом словаре, передается обратным вызовам.
Это можно использовать для создания дополнительных параметров. Этот шаблон, как правило, не рекомендуется, но в некоторых случаях он может быть полезен. По крайней мере, хорошо знать, что система работает таким образом.
import urllib
def open_url(ctx, param, value):
if value is not None:
ctx.params['fp'] = urllib.urlopen(value)
return value
@click.command()
@click.option('--url', callback=open_url)
def cli(url, fp=None):
if fp is not None:
click.echo(f"{url}: {fp.code}")
В этом случае обратный вызов возвращает URL без изменений, но также передает второе значение fp обратным вызовам. Однако предпочтительнее передавать информацию в оболочке:
import urllib
class URL(object):
def __init__(self, url, fp):
self.url = url
self.fp = fp
def open_url(ctx, param, value):
if value is not None:
return URL(value, urllib.urlopen(value))
@click.command()
@click.option('--url', callback=open_url)
def cli(url):
if url is not None:
click.echo(f"{url.url}: {url.fp.code}")
Нормализация маркеров
Журнал изменений
Введено в версии 2.0.
Начиная с Click 2.0, можно указать функцию, используемую для нормализации маркеров. Маркеры — это имена опций, значения выбора или значения команд. Это можно использовать для реализации регистронезависимых опций, например.
Для использования этой функции контексту необходимо передать функцию, которая выполняет нормализацию маркера. Например, можно иметь функцию, которая преобразует маркер в нижний регистр:
CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower())
@click.command(context_settings=CONTEXT_SETTINGS)
@click.option('--name', default='Pete')
def cli(name):
click.echo(f"Name: {name}")
И как это работает в командной строке:
$ cli --NAME=Pete Name: Pete
Вызов других команд
Иногда может быть интересно вызвать одну команду из другой команды. Этот шаблон, как правило, не рекомендуется в Click, но тем не менее возможен. Для этого можно использовать методы Context.invoke() или Context.forward().
Они работают аналогично, но разница в том, что Context.invoke() просто вызывает другую команду с аргументами, которые вы предоставляете как вызывающая сторона, в то время как Context.forward() заполняет аргументы из текущей команды. Оба принимают команду в качестве первого аргумента, а всё остальное передаётся так, как вы ожидаете.
Пример:
cli = click.Group()
@cli.command()
@click.option('--count', default=1)
def test(count):
click.echo(f'Count: {count}')
@cli.command()
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
ctx.forward(test)
ctx.invoke(test, count=42)
И как это выглядит:
$ cli dist Count: 1 Count: 42
Порядок вычисления обратных вызовов
Click работает немного по-другому, чем некоторые другие парсеры командной строки, поскольку он пытается согласовать порядок аргументов, определенный программистом, с порядком аргументов, определенным пользователем, прежде чем вызывать какие-либо обратные вызовы.
Это важная концепция для понимания при переносе сложных шаблонов в Click из optparse или других систем. Вызов обратного вызова параметра в optparse происходит как часть шага разбора, а вызов обратного вызова в Click происходит после разбора.
Основное различие заключается в том, что в optparse обратные вызовы вызываются со значением в сыром виде, как оно происходит, в то время как обратный вызов в Click вызывается после полного преобразования значения.
Как правило, порядок вызова определяется порядком, в котором пользователь предоставляет аргументы скрипту; если есть опция с именем --foo и опция с именем --bar, и пользователь её вызовет как --bar
--foo, тогда обратный вызов для bar будет вызван перед обратным вызовом для foo.
Существует три исключения из этого правила, которые важно знать:
- Жадный режим:
-
Опция может быть установлена в «жадный» режим. Все жадные параметры вычисляются перед всеми нежадными параметрами, но опять же в том порядке, в котором они были предоставлены в командной строке пользователем.
Это важно для параметров, которые выполняют и завершают работу, например,
--helpи--version. Оба являются жадными параметрами, но какой параметр первым указан в командной строке, тот и выиграет, завершив выполнение программы. - Повторные параметры:
-
Если опция или аргумент разделены в командной строке на несколько мест, потому что она повторяется — например,
--exclude foo --include baz --exclude bar— обратный вызов будет вызван в зависимости от позиции первой опции. В этом случае обратный вызов дляexcludeбудет вызван, и ему будут переданы обе опции (fooиbar). Затем обратный вызов дляincludeбудет вызван со значением толькоbaz.Обратите внимание, что даже если параметр не допускает нескольких версий, Click всё равно примет позицию первой, но проигнорирует все значения, кроме последнего. Причина этого заключается в возможности композиции через алиасы оболочки, устанавливающие значения по умолчанию.
- Отсутствующие параметры:
-
Если параметр не определён в командной строке, обратный вызов всё равно будет вызван. Это отличается от того, как это работает в optparse, где неопределённые значения не вызывают обратный вызов. Обратные вызовы для отсутствующих параметров вызываются в самом конце, что позволяет им принимать значения по умолчанию из параметра, который пришёл раньше.
В большинстве случаев вам не нужно беспокоиться ни об одном из этих моментов, но важно знать, как это работает в некоторых сложных случаях.
Передача неизвестных параметров
В некоторых ситуациях полезно принимать все неизвестные параметры для дальнейшей обработки вручную. Click, начиная с версии 4.0, обычно это поддерживает, но имеет некоторые ограничения, связанные с природой проблемы. Поддержка обеспечивается флагом парсера, называемым ignore_unknown_options, который укажет парсеру собирать все неизвестные параметры и помещать их в оставшиеся аргументы вместо выдачи ошибки парсинга.
Это можно активировать двумя способами:
- Это можно включить в пользовательских подклассах
Command, изменив атрибутignore_unknown_options. - Это можно включить, изменив атрибут с тем же именем в классе контекста (
Context.ignore_unknown_options). Лучше всего это изменить через словарьcontext_settingsкоманды.
Для большинства ситуаций наиболее простым решением является второй способ. После изменения поведения необходимо обработать оставшиеся параметры (которые на данном этапе считаются аргументами). Для этого у вас есть два варианта:
- Можно использовать
pass_context()для получения контекста. Это сработает только в том случае, если, помимоignore_unknown_options, вы также установитеallow_extra_args, иначе команда завершится с ошибкой о наличии оставшихся аргументов. Если вы используете этот способ, дополнительные аргументы будут собраны вContext.args. - Можно добавить аргумент
argument()с параметромnargsустановленным в значение-1, который соберет все оставшиеся аргументы. В этом случае рекомендуется установитьtypeвUNPROCESSEDдля предотвращения обработки этих аргументов, поскольку в противном случае они автоматически преобразуются в строковые значения Unicode, что часто не нужно.
В итоге вы получите что-то вроде этого:
import sys
from subprocess import call
@click.command(context_settings=dict(
ignore_unknown_options=True,
))
@click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode')
@click.argument('timeit_args', nargs=-1, type=click.UNPROCESSED)
def cli(verbose, timeit_args):
"""A fake wrapper around Python's timeit."""
cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
if verbose:
click.echo(f"Invoking: {' '.join(cmdline)}")
call(cmdline)
И вот как это выглядит:
$ cli --help Usage: cli [OPTIONS] [TIMEIT_ARGS]... A fake wrapper around Python's timeit. Options: -v, --verbose Enables verbose mode --help Show this message and exit. $ cli -n 100 'a = 1; b = 2; a * b' python -mtimeit -n 100 a = 1; b = 2; a * b $ cli -v 'a = 1; b = 2; a * b' Invoking: echo python -mtimeit a = 1; b = 2; a * b python -mtimeit a = 1; b = 2; a * b
Как видите, флаг verbose обрабатывается Click, все остальное попадает в переменную timeit_args для дальнейшей обработки, что, например, позволяет вызывать дочерний процесс. Несколько важных моментов о том, как происходит игнорирование необработанных флагов:
- Неизвестные длинные параметры обычно игнорируются и не обрабатываются вообще. Например, если переданы
--foo=barили--foo bar, они обычно остаются в таком виде. Обратите внимание, что, поскольку парсер не может знать, будет ли параметр принимать аргумент, частьbarможет обрабатываться как аргумент. - Неизвестные короткие параметры могут частично обрабатываться и, при необходимости, собираться заново. Например, в приведенном выше примере есть параметр
-v, который включает режим verbose. Если команда будет проигнорирована с-va, то часть-vбудет обработана Click (поскольку она известна), а-aпопадет в оставшиеся параметры для дальнейшей обработки. - В зависимости от ваших планов, вы можете добиться успеха, отключив вложенные аргументы (
allow_interspersed_args), что указывает парсеру не разрешать смешивание аргументов и параметров. В зависимости от ситуации это может улучшить ваши результаты.
Однако, в целом, совместная обработка параметров и аргументов из ваших собственных команд и команд из другого приложения не рекомендуется. Если можно этого избежать, то следует. Гораздо лучше отправить все параметры подкоманды в другое приложение, чем обрабатывать некоторые аргументы самостоятельно.
Доступ к глобальному контексту
Изменения
Введено в версии 5.0.
Начиная с Click 5.0, можно получить доступ к текущему контексту из любой точки в том же потоке с помощью функции get_current_context(), которая возвращает его. Это в первую очередь полезно для доступа к объекту, связанному с контекстом, а также к некоторым флагам, хранящимся в нем для настройки поведения во время выполнения. Например, функция echo() делает это для вывода значения по умолчанию для флага color.
Пример использования:
def get_current_command_name():
return click.get_current_context().info_name
Следует отметить, что это работает только в рамках текущего потока. Если вы запускаете дополнительные потоки, эти потоки не смогут обратиться к текущему контексту. Если вы хотите предоставить другому потоку возможность обратиться к этому контексту, вы должны использовать контекст в потоке как менеджер контекста:
def spawn_thread(ctx, func):
def wrapper():
with ctx:
func()
t = threading.Thread(target=wrapper)
t.start()
return t
Теперь функция потока может получить доступ к контексту так же, как и основной поток. Однако если вы используете это для многопоточности, вам нужно быть очень осторожным, поскольку подавляющее большинство контекста не является потокобезопасным! Вам разрешено только читать из контекста, но не выполнять какие-либо изменения в нем.
Определение источника параметра
В некоторых ситуациях полезно понимать, пришел ли параметр или опция с командной строки, из среды, из значения по умолчанию или Context.default_map. Метод Context.get_parameter_source() может быть использован для этого. Он вернёт член перечисления ParameterSource.
@click.command()
@click.argument('port', nargs=1, default=8080, envvar="PORT")
@click.pass_context
def cli(ctx, port):
source = ctx.get_parameter_source("port")
click.echo(f"Port came from {source.name}")
$ cli 8080 Port came from COMMANDLINE $ export PORT=8080 $ cli Port came from ENVIRONMENT $ cli Port came from DEFAULT
Управление ресурсами
Полезно открыть ресурс в группе, чтобы сделать его доступным подкомандам. Многие типы ресурсов должны быть закрыты или очищены после использования. Стандартный способ сделать это в Python — использовать менеджер контекста с оператором with.
Например, класс Repo из раздела Сложные приложения может быть определен как менеджер контекста:
class Repo:
def __init__(self, home=None):
self.home = os.path.abspath(home or ".")
self.db = None
def __enter__(self):
path = os.path.join(self.home, "repo.db")
self.db = open_database(path)
return self
def __exit__(self, exc_type, exc_value, tb):
self.db.close()
Обычно он используется с оператором with:
with Repo() as repo:
repo.db.query(...)
Однако, блок with в группе завершит работу и закроет базу данных прежде, чем она будет использована подкомандой.
Вместо этого используйте метод контекста with_resource() для входа в менеджер контекста и получения ресурса. Когда группа и все подкоманды завершатся, ресурсы контекста будут очищены.
@click.group()
@click.option("--repo-home", default=".repo")
@click.pass_context
def cli(ctx, repo_home):
ctx.obj = ctx.with_resource(Repo(repo_home))
@cli.command()
@click.pass_obj
def log(obj):
# obj is the repo opened in the cli group
for entry in obj.db.query(...):
click.echo(entry)
Если ресурс не является менеджером контекста, его обычно можно обернуть в менеджер с помощью инструмента из contextlib. Если это невозможно, используйте метод контекста call_on_close() для регистрации функции очистки.
@click.group()
@click.option("--name", default="repo.db")
@click.pass_context
def cli(ctx, repo_home):
ctx.obj = db = open_db(repo_home)
@ctx.call_on_close
def close_db():
db.record_use()
db.save()
db.close()
© Copyright 2014 Pallets.
Licensed under the BSD 3-Clause License.
We are not supported nor endorsed by Pallets.
https://click.palletsprojects.com/en/8.1.x/advanced/