Spec-Zone.ru › click

Команды и группы

Наиболее важной особенностью Click является возможность произвольного вложения утилит командной строки. Это реализуется через Command и Group (на самом деле MultiCommand).

Вызов обратного вызова

Для обычной команды обратный вызов выполняется всякий раз, когда запускается команда. Если скрипт является единственной командой, он всегда будет срабатывать (если обратный вызов параметра этого не предотвращает. Например, это происходит, если кто-то передает --help скрипту).

Для групп и многокомпонентных команд ситуация выглядит иначе. В этом случае обратный вызов срабатывает всякий раз, когда срабатывает подкоманда (если это поведение не изменено). Практически это означает, что внешняя команда запускается, когда запускается внутренняя команда:

@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
    click.echo(f"Debug mode is {'on' if debug else 'off'}")

@cli.command()  # @cli, not @click!
def sync():
    click.echo('Syncing')

Вот как это выглядит:

$ tool.py
Usage: tool.py [OPTIONS] COMMAND [ARGS]...

Options:
  --debug / --no-debug
  --help                Show this message and exit.

Commands:
  sync

$ tool.py --debug sync
Debug mode is on
Syncing

Передача параметров

Click строго разделяет параметры между командами и подкомандами. Это означает, что опции и аргументы для конкретной команды должны быть указаны после имени самой команды, но перед любыми другими именами команд.

Это поведение уже можно наблюдать с предопределенной --help опцией. Предположим, у нас есть программа под названием tool.py, содержащая подкоманду под названием sub.

  • tool.py --help вернёт справку по всей программе (список подкоманд).
  • tool.py sub --help вернёт справку по подкоманде sub.
  • Но tool.py --help sub будет рассматривать --help как аргумент для основной программы. Click затем вызывает обратный вызов для --help, который выводит справку и прерывает программу, прежде чем Click сможет обработать подкоманду.

Вложенная обработка и контексты

Как вы можете видеть из предыдущего примера, базовая группа команд принимает аргумент отладки, который передаётся её обратному вызову, но не самой команде синхронизации. Команда синхронизации принимает только свои собственные аргументы.

Это позволяет инструментам действовать совершенно независимо друг от друга, но как одна команда может взаимодействовать с вложенной? Ответом на это является Context.

Каждый раз, когда вызывается команда, создаётся новый контекст и связывается с родительским контекстом. Обычно вы не видите этих контекстов, но они есть. Контексты передаются обратному вызову параметра вместе со значением автоматически. Команды также могут запросить передачу контекста, пометив себя декоратором pass_context(). В этом случае контекст передаётся в качестве первого аргумента.

Контекст также может содержать заданный программой объект, который можно использовать для целей программы. Это означает, что вы можете создать скрипт такого вида:

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    # ensure that ctx.obj exists and is a dict (in case `cli()` is called
    # by means other than the `if` block below)
    ctx.ensure_object(dict)

    ctx.obj['DEBUG'] = debug

@cli.command()
@click.pass_context
def sync(ctx):
    click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")

if __name__ == '__main__':
    cli(obj={})

Если объект предоставлен, каждый контекст будет передавать объект своим дочерним элементам, но на любом уровне объект контекста может быть перезаписан. Чтобы обратиться к родителю, можно использовать context.parent.

Помимо этого, вместо передачи объекта вниз, ничего не мешает приложению изменять глобальное состояние. Например, можно просто переключить глобальную DEBUG переменную и всё.

Декорирование команд

Как вы видели в предыдущем примере, декоратор может изменить способ вызова команды. Фактически, обратные вызовы всегда вызываются через метод Context.invoke(), который автоматически правильно вызывает команду (передавая или не передавая контекст).

Это очень полезно, когда вы хотите написать пользовательские декораторы. Например, распространённым шаблоном было бы настройка объекта, представляющего состояние, а затем сохранение его в контексте, а затем использование пользовательского декоратора для поиска самого последнего объекта такого типа и передачи его в качестве первого аргумента.

Например, декоратор pass_obj() можно реализовать так:

from functools import update_wrapper

def pass_obj(f):
    @click.pass_context
    def new_func(ctx, *args, **kwargs):
        return ctx.invoke(f, ctx.obj, *args, **kwargs)
    return update_wrapper(new_func, f)

Команда Context.invoke() автоматически вызовет функцию правильным образом, поэтому функция будет вызвана с f(ctx, obj) или f(obj) в зависимости от того, декорирована ли она декоратором pass_context().

Это очень мощная концепция, которая может использоваться для создания очень сложных вложенных приложений; см. Сложные приложения для получения дополнительной информации.

Вызов группы без команды

По умолчанию группа или многокомпонентная команда не вызывается, если не передаётся подкоманда. Фактически, отсутствие команды по умолчанию передаёт --help. Это поведение можно изменить, передав invoke_without_command=True группе. В этом случае обратный вызов всегда вызывается вместо отображения страницы справки. Объект контекста также содержит информацию о том, будет ли вызов направлен на подкоманду или нет.

Пример:

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        click.echo('I was invoked without subcommand')
    else:
        click.echo(f"I am about to invoke {ctx.invoked_subcommand}")

@cli.command()
def sync():
    click.echo('The subcommand')

И как это работает на практике:

$ tool
I was invoked without subcommand
$ tool sync
I am about to invoke sync
The subcommand

Пользовательские многокомпонентные команды

Помимо использования click.group(), вы также можете создать свои собственные пользовательские многокомпонентные команды. Это полезно, когда вы хотите поддерживать команды, загружаемые лениво из плагинов.

Пользовательская многокомпонентная команда просто должна реализовать методы списка и загрузки:

import click
import os

plugin_folder = os.path.join(os.path.dirname(__file__), 'commands')

class MyCLI(click.MultiCommand):

    def list_commands(self, ctx):
        rv = []
        for filename in os.listdir(plugin_folder):
            if filename.endswith('.py') and filename != '__init__.py':
                rv.append(filename[:-3])
        rv.sort()
        return rv

    def get_command(self, ctx, name):
        ns = {}
        fn = os.path.join(plugin_folder, name + '.py')
        with open(fn) as f:
            code = compile(f.read(), fn, 'exec')
            eval(code, ns, ns)
        return ns['cli']

cli = MyCLI(help='This tool\'s subcommands are loaded from a '
            'plugin folder dynamically.')

if __name__ == '__main__':
    cli()

Эти пользовательские классы также могут использоваться с декораторами:

@click.command(cls=MyCLI)
def cli():
    pass

Объединение многокомпонентных команд

Помимо реализации пользовательских многокомпонентных команд, может быть интересно объединить несколько команд в один скрипт. Хотя это, как правило, не рекомендуется, так как оно вкладывает одну под другую, подход к объединению может быть полезен в некоторых ситуациях для более приятного взаимодействия с оболочкой.

Стандартная реализация такой системы объединения — класс CommandCollection. Он принимает список других многокомпонентных команд и делает команды доступными на одном уровне.

Пример использования:

import click

@click.group()
def cli1():
    pass

@cli1.command()
def cmd1():
    """Command on cli1"""

@click.group()
def cli2():
    pass

@cli2.command()
def cmd2():
    """Command on cli2"""

cli = click.CommandCollection(sources=[cli1, cli2])

if __name__ == '__main__':
    cli()

И как это выглядит:

$ cli --help
Usage: cli [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  cmd1  Command on cli1
  cmd2  Command on cli2

Если команда существует в нескольких источниках, приоритет имеет первый источник.

Цепочки многокомпонентных команд

Изменения

Введено в версии 3.0.

Иногда бывает полезно иметь возможность вызывать сразу несколько подкоманд. Например, если вы установили пакет setuptools, вы можете быть знакомы с цепочкой команд setup.py sdist bdist_wheel upload, которая вызывает sdist перед bdist_wheel перед upload. Начиная с Click 3.0 это очень легко реализовать. Всё, что вам нужно сделать, это передать chain=True вашей многокомпонентной команде:

@click.group(chain=True)
def cli():
    pass


@cli.command('sdist')
def sdist():
    click.echo('sdist called')


@cli.command('bdist_wheel')
def bdist_wheel():
    click.echo('bdist_wheel called')

Теперь вы можете вызвать её так:

$ setup.py sdist bdist_wheel
sdist called
bdist_wheel called

При использовании цепочки многокомпонентных команд только одна команда (последняя) может использовать nargs=-1 на аргументе. Также нельзя вкладывать многокомпонентные команды в цепочки многокомпонентных команд. Помимо этого, нет ограничений на их работу. Они могут принимать опции и аргументы как обычно. Порядок между опциями и аргументами ограничен для цепочек команд. В настоящее время разрешается только порядок --options argument.

Ещё один момент: атрибут Context.invoked_subcommand немного бесполезен для многокомпонентных команд, так как он возвращает значение '*' при вызове более одной команды. Это необходимо, потому что обработка подкоманд происходит одна за другой, поэтому точные подкоманды, которые будут обработаны, ещё не доступны, когда срабатывает обратный вызов.

Примечание

В настоящее время нельзя вкладывать команды цепочки. Это будет исправлено в будущих версиях Click.

Многокомандные конвейеры

Журнал изменений

Новое в версии 3.0.

Очень распространённое применение многокомандных цепочек — обработка результата предыдущей команды. Существует несколько способов реализации. Самый очевидный — сохранение значения в объекте контекста и его обработка из функции в функцию. Для этого функция декорируется с помощью pass_context(), после чего объект контекста предоставляется, и подкоманда может сохранить свои данные в нём.

Другой способ — создание конвейеров путём возврата функций обработки. Представьте это так: при вызове подкоманды она обрабатывает все свои параметры и определяет план обработки. На этом этапе она возвращает функцию обработки и возвращается.

Куда попадают возвращаемые функции? Многокомандная цепочка может зарегистрировать обратный вызов с помощью MultiCommand.result_callback(), который проходит по всем этим функциям и вызывает их.

Для лучшего понимания рассмотрим пример:

@click.group(chain=True, invoke_without_command=True)
@click.option('-i', '--input', type=click.File('r'))
def cli(input):
    pass

@cli.result_callback()
def process_pipeline(processors, input):
    iterator = (x.rstrip('\r\n') for x in input)
    for processor in processors:
        iterator = processor(iterator)
    for item in iterator:
        click.echo(item)

@cli.command('uppercase')
def make_uppercase():
    def processor(iterator):
        for line in iterator:
            yield line.upper()
    return processor

@cli.command('lowercase')
def make_lowercase():
    def processor(iterator):
        for line in iterator:
            yield line.lower()
    return processor

@cli.command('strip')
def make_strip():
    def processor(iterator):
        for line in iterator:
            yield line.strip()
    return processor

Это много информации сразу, поэтому давайте разберём её пошагово.

  1. Сначала необходимо создать цепочку команд с помощью group(). Кроме того, мы указываем Click вызывать команду даже при отсутствии подкоманд. Если этого не сделать, вызов пустого конвейера будет возвращать страницу справки вместо запуска обратных вызовов.
  2. Затем мы регистрируем обратный вызов результата для нашей группы. Этот обратный вызов будет вызываться с аргументом, который представляет собой список всех возвращаемых значений всех подкоманд, а также с теми же именованными параметрами, что и у самой группы. Это означает, что мы можем легко получить доступ к входному файлу, не прибегая к использованию объекта контекста.
  3. В этом обратном вызове мы создаём итератор всех строк во входном файле, затем пропускаем этот итератор через все возвращаемые обратные вызовы от всех подкоманд и, наконец, выводим все строки в стандартный вывод.

После этого мы можем зарегистрировать любое количество подкоманд, и каждая из них может вернуть функцию процессора для изменения потока строк.

Важно отметить, что Click закрывает контекст после выполнения каждого обратного вызова. Это означает, что, например, типы файлов недоступны в processor функциях, так как файлы уже закрыты. Это ограничение вряд ли изменится, потому что это усложнило бы обработку ресурсов. В таких случаях рекомендуется не использовать тип файла и вручную открывать файл с помощью open_file().

Для более сложного примера, который также улучшает обработку конвейеров, ознакомьтесь с демонстрацией многокомандной цепочки imagepipe в репозитории Click. Она реализует инструмент редактирования изображений на основе конвейеров с хорошей внутренней структурой для конвейеров.

Переопределение значений по умолчанию

По умолчанию значение параметра по умолчанию извлекается из флага default , предоставленного при его определении, но значения по умолчанию могут загружаться и из других источников. Другой источник — словарь Context.default_map в контексте. Это позволяет загружать значения по умолчанию из файла конфигурации для переопределения стандартных значений по умолчанию.

Это полезно, если вы подключаете команды из другого пакета, но не удовлетворены значениями по умолчанию.

Карта значений по умолчанию может быть вложенной для каждой подкоманды:

default_map = {
    "debug": True,  # default for a top level option
    "runserver": {"port": 5000}  # default for a subcommand
}

Карту значений по умолчанию можно указать при вызове скрипта или переопределить на любом этапе командами. Например, команда верхнего уровня может загрузить значения по умолчанию из файла конфигурации.

Пример использования:

import click

@click.group()
def cli():
    pass

@cli.command()
@click.option('--port', default=8000)
def runserver(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")

if __name__ == '__main__':
    cli(default_map={
        'runserver': {
            'port': 5000
        }
    })

И в действии:

$ cli runserver
Serving on http://127.0.0.1:5000/

Значения по умолчанию контекста

Журнал изменений

Новое в версии 2.0.

Начиная с Click 2.0, вы можете переопределять значения по умолчанию для контекстов не только при вызове скрипта, но и в декораторе, объявляющем команду. Например, в предыдущем примере, определяющем пользовательский default_map , это также можно сделать сейчас в декораторе.

Этот пример делает то же, что и предыдущий пример:

import click

CONTEXT_SETTINGS = dict(
    default_map={'runserver': {'port': 5000}}
)

@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
    pass

@cli.command()
@click.option('--port', default=8000)
def runserver(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")

if __name__ == '__main__':
    cli()

И снова пример в действии:

$ cli runserver
Serving on http://127.0.0.1:5000/

Возвращаемые значения команд

Журнал изменений

Новое в версии 3.0.

Одно из новых дополнений в Click 3.0 — полная поддержка возвращаемых значений от обратных вызовов команд. Это открывает целый ряд ранее трудно реализуемых функций.

По сути, любой обратный вызов команды теперь может возвращать значение. Это возвращаемое значение передаётся определённым получателям. Один из случаев использования уже показан в примере многокомандных цепочек, где показано, что у цепочек многокоманд могут быть обратные вызовы, обрабатывающие все возвращаемые значения.

При работе с возвращаемыми значениями команд в Click вам нужно знать следующее:

  • Возвращаемое значение обратного вызова команды обычно возвращается методом BaseCommand.invoke(). Исключение из этого правила связано с Group:

    • В группе возвращаемое значение, как правило, является возвращаемым значением вызываемой подкоманды. Единственное исключение — возвращаемое значение является возвращаемым значением обратного вызова группы, если он вызван без аргументов и invoke_without_command включён.
    • Если группа настроена на цепочку, возвращаемое значение представляет собой список результатов всех подкоманд.
    • Возвращаемые значения групп могут обрабатываться через MultiCommand.result_callback. Он вызывается со списком всех возвращаемых значений в режиме цепочки или с единственным возвращаемым значением в случае нецепных команд.
  • Возвращаемое значение передаётся через методы Context.invoke() и Context.forward(). Это полезно в ситуациях, когда вы хотите внутренне вызвать другую команду.
  • Click не имеет жёстких требований к возвращаемым значениям и не использует их сам. Это позволяет использовать возвращаемые значения для пользовательских декораторов или рабочих процессов (как в примере многокомандной цепочки).
  • При вызове скрипта Click как командной строки приложения (через BaseCommand.main()) возвращаемое значение игнорируется, если standalone_mode отключен, в противном случае оно передаётся дальше.

© 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/commands/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API