Python в командной строке: учебник по Click

Введение

Выходные я посвятил одному из своих любимых занятий - написанию кода на Python, и нашел способ сгенерировать 3D QR-код моего пароля WIFI. В процессе я получил несколько интересных прозрений, главным образом то, что интерфейсы командной строки (CLI) и веб-приложения имеют некоторые поразительные общие черты:

CLI и веб-приложения - это не более чем текстовые конечные точки для произвольного кода!

Чтобы показать это в деталях, я использую базу кода из моего проекта 3D-модели QR-кода и создам интерфейс командной строки из функций, которые я использовал.

Построение интерфейса командной строки (CLI)

CLI позволяет вам получить доступ к программе из командной строки, например, из оболочки bash в Linux/macOS или командной строки Windows. CLI позволяет запускать скрипты. Например, CLI позволяет нам программно генерировать столько QR-кодов, сколько мы хотим, с помощью одной команды.

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

Обзор: Подготовка к созданию CLI

Во имя "хорошей программной инженерии" мы сначала немного упорядочим наши функции и подготовимся к тому, чтобы собрать их в пакет Python, который можно будет легко распространять. Окончательная структура каталогов, на которую мы ориентируемся, выглядит следующим образом:

├── environment.yml
├── qrwifi
│   ├── __init__.py
│   ├── app.py
│   ├── cli.py
│   ├── functions.py **
│   └── templates
│       ├── index.html.j2
│       ├── qr.html.j2
│       └── template.html.j2
└── setup.py

(С этого момента я буду выделять файл, который мы будем редактировать, двойной звездочкой (**)).

Библиотека функций

Для начала создадим functions.py. В нем должны находиться функции, которые мы можем импортировать и вызывать.

import numpy as np
import pyqrcode as pq


def wifi_qr(ssid: str, security: str, password: str):
    """
    Creates the WiFi QR code object.
    """
    qr = pq.create(f'WIFI:S:{ssid};T:{security};P:{password};;')
    return qr


def qr2array(qr):
    """
    Convert a QR code object into its array representation.
    """
    arr = []
    for line in qr.text().split('\n'):
        if line:
            arr.append(list(map(int, line)))
    return np.vstack(arr)


def png_b64(qr, scale: int = 10):
    """
    Return the base64 encoded PNG of the QR code.
    """
    return qr.png_data_uri(scale=scale)

МодульCLI

Для создания интерфейса командной строки мы будем использовать пакет Python под названием Click. Kite также содержит зеркало документации, которая подается в ваш текстовый редактор, когда вы используете Kite). Вы можете установить его, используя:
$ pip install click
Что дает click, так это чистый и композитный способ создания интерфейсов командной строки для вашего Python-кода.

$ tree
.
├── environment.yml
├── qrwifi
│   ├── __init__.py
│   ├── cli.py **
│   └── functions.py
└── setup.py

Теперь создадим cli.py. Он будет содержать модуль командной строки нашего пакета. Мы создадим его так, чтобы пользователь мог использовать его следующим образом:
$ qrwifi --ssid '<SSID_NAME>' \
--security '<SECURITY>' \
--password '<PASSWORD>' \
[terminal|png --filename '<FILEPATH>']

Чтобы пояснить, мы заменяем все <...> соответствующими строками, без символа $, без {} скобок.

Я буду развивать вашу интуицию бит за битом, а в конце мы сможем посмотреть на все вместе. Вы всегда можете обратиться к полному сценарию cli.py в конце этого раздела.

import numpy as np

import pyqrcode as pq

import click

from .functions import wifi_qr, qr2array


@click.group()
@click.option('--ssid', help='WiFi network name.')
@click.option('--security', type=click.Choice(['WEP', 'WPA', '']))
@click.option('--password', help='WiFi password.')
@click.pass_context
def main(ctx, ssid: str, security: str = '', password: str = ''):
    qr = wifi_qr(ssid=ssid, security=security, password=password)
    ctx.obj['qr'] = qr

Начнем с импорта необходимых пакетов, и начнем с функции main(). Согласно сигнатуре функции, функция main() принимает объект ctx (ctx - сокращение от "контекст", подробнее об этом позже), а также аргументы ключевых слов, которые нам нужны для создания QR-кода WiFi.

В теле main() мы вызываем функцию wifi_qr(), определенную в functions.py, а затем присваиваем полученный объект qr в ctx.obj (словарь объектов контекста). Если вам все еще интересно, что это за объект "контекст", держитесь крепче - скоро я до него доберусь.

Кроме определения функции, вы заметите, что мы украсили ее функциями click. Именно здесь вступает в игру магия click. Украсив main() функциями @click.group(), мы теперь можем раскрыть main() в командной строке и вызвать ее оттуда! Чтобы вывести его опции в командную строку, мы должны добавить один или несколько декораторов @click.option() с соответствующими флагами.

Вы также заметите, что есть декоратор @click.pass_context. Возможно, сейчас самое время представить объект "контекст".

Простейший способ архитектоники нашего CLI для вывода в терминал или PNG-файл - это иметь "дочернюю" функцию main(), которая знает о том, что было установлено в main(). Для того чтобы это было возможно, @click.pass_context создает функцию, принимающую в качестве первого аргумента объект "context", дочерний атрибут которого .obj представляет собой прославленный словарь. Используя этот шаблон программирования, "дочерние" функции могут действовать на объект контекста и делать все, что им нужно. По сути, это похоже на передачу состояния от родительской функции к дочерней.

Перейдем к построению "дочерних" функций, которые называются terminal() и png().

@main.command()
@click.pass_context
def terminal(ctx):
    """Print QR code to the terminal."""
    print(ctx.obj['qr'].terminal())


@main.command()
@click.option('--filename', help='full path to the png file')
@click.pass_context
def png(ctx, filename, scale: int = 10):
    """Create a PNG file of the QR code."""
    ctx.obj['qr'].png(filename, scale)

Обе наши функции украшены @main.command(), что указывает click на то, что это "дочерняя" команда функции main(). Украшение функций с помощью @somecommand.command() позволяет нам вложить команды друг в друга и разделить логику, делая наш код понятным.

terminal() не имеет никаких опций, потому что мы хотим, чтобы он выводился непосредственно на терминал.

Мы хотим, чтобы команда png() была сохранена на диск по некоторому заранее указанному пути. Таким образом, к ней прилагается еще одна команда @click.option().

def start():
    main(obj={})


if __name__ == '__main__':
    start()

Наконец, у нас есть функция start(), которая передает пустой словарь в main(). Функция start() не имеет аргументов, поэтому ее необходимо добавить в setup.py в качестве точки входа (об этом позже).

cli.py в полном объеме

Как и было обещано, вот полный cli.py, который вы можете скопировать/вставить.

import numpy as np

import pyqrcode as pq

import click

from .functions import wifi_qr, qr2array


@click.group()
@click.option('--ssid', help='WiFi network name.')
@click.option('--security', type=click.Choice(['WEP', 'WPA', '']))
@click.option('--password', help='WiFi password.')
@click.pass_context
def main(ctx, ssid: str, security: str = '', password: str = ''):
    qr = wifi_qr(ssid=ssid, security=security, password=password)
    ctx.obj['qr'] = qr
    ctx.obj['ssid'] = ssid
    ctx.obj['security'] = security
    ctx.obj['password'] = password


@main.command()
@click.pass_context
def terminal(ctx):
    print(ctx.obj['qr'].terminal())


@main.command()
@click.option('--filename', help='full path to the png file')
@click.pass_context
def png(ctx, filename, scale: int = 10):
    ctx.obj['qr'].png(filename, scale)


def start():
    main(obj={})


if __name__ == '__main__':
    start()

Настройка пользовательского интерфейса CLI

qrwifi в командной строке

Как это выглядит в командной строке? Давайте посмотрим:

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

Options:
  --ssid TEXT            WiFi network name.
  --security [WEP|WPA|]
  --password TEXT        WiFi password.
  --help                 Show this message and exit.

Commands:
  png
  terminal

Посмотрите на это!!! Нам не пришлось делать никаких argparse трюков, чтобы заставить этот великолепный вывод появиться! Мы даже получили "меню помощи" бесплатно, в комплекте с текстом "help", который мы указали в командной строке.

Вы заметите, что здесь есть раздел Options со всеми опциями, прикрепленными к функции main(), а также раздел Commands, в котором доступны дочерние функции (png() и terminal()). Имя функции в точности соответствует имени команды в CLI.

Однако мы еще не закончили, потому что этот cli.py доступен, только если мы знаем, где находится файл. Если мы будем распространять его как пакет, то в идеале мы хотели бы абстрагироваться от местоположения cli.py, вместо этого заставив конечного пользователя обращаться к запоминающемуся имени, скажем, qrwifi.

Создать setup.py

Для этого нам нужен другой файл, setup.py файл.

$tree
.
├── environment.yml
├── qrwifi
│   ├── __init__.py
│   ├── cli.py
│   └── functions.py
└── setup.py **

Давайте рассмотрим структуру файла setup.py. (Вы также можете скопировать/вставить его полностью.)

from setuptools import setup, find_packages

setup(
      # mandatory
      name='qrwifi',
      # mandatory
      version='0.1',
      # mandatory
      author_email='username@email.address',
      packages=['qrwifi'],
      package_data={},
      install_requires=['pyqrcode', 'SolidPython', 'numpy', 'Flask', 'click'],
      entry_points={
        'console_scripts': ['qrwifi = qrwifi.cli:start']
      }
)

Здесь мы указываем пакет name, version и author_email (что я считаю самой основной информацией, которая нам необходима).

В разделе packages мы указываем списком строк каталоги, в которых содержится наш пакет Python. В данном случае это простой пакет, содержащий только один каталог qrwifi. Нет никаких других дополнительных наборов данных, которые нужно упаковать вместе, поэтому мы можем оставить его как пустой словарь.

В разделе install_requires мы указываем пакеты, которые нужны нашему пакету Python. При установке Python установит эти пакеты и их указанные зависимости.

Последним магическим заклинанием, которое у нас есть, является ключевое слово entry_points. Здесь мы указываем, что хотим получить доступ к qrwifi на терминале с помощью команды qrwifi. Таким образом, мы передаем словарь, в котором ключ console_scripts сопоставлен со списком команд, ограниченных "=". Здесь мы отображаем строку qrwifi на qrwifi.cli:start (шаблон package.name:function).

Если мы сохраним setup.py на диск, мы сможем установить пакет из нашего текущего каталога:

$ python setup.py develop

Я выбрал develop вместо install, потому что в режиме разработки мы можем редактировать исходный текст непосредственно в том же каталоге и сразу же тестировать изменения. При использовании режима install файлы в каталоге qrwifi будут скопированы в каталог вашего пакета Python. Вы можете прочитать больше о режиме разработки здесь.

qrwifi в командной строке: Конечный продукт!

Теперь мы можем получить доступ к приложению из командной строки, нужно только вызвать qrwifi.

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

Options:
  --ssid TEXT            WiFi network name.
  --security [WEP|WPA|]
  --password TEXT        WiFi password.
  --help                 Show this message and exit.

Commands:
  png
  terminal

Пример использования этого приложения CLI для отображения QR-кода на терминале будет следующим:

$ qrwifi --ssid "Kite Guest Network" \
           --security "WPA" \
           --password "vrilhkjasdf" terminal

А чтобы сохранить PNG файл на диск:

$ qrwifi --ssid "Kite Guest Network" \
           --security "WPA" \
           --password "vrilhkjasdf" \
           png --filename ./kiteguest.png

Выводы

Надеемся, что эта статья в блоге показала вам один полезный пример того, как создать приложение командной строки с помощью Click. Click является мощным и простым в использовании, что является редким достижением хорошего дизайна программного обеспечения! Большое спасибо группе pocoo, которая создает Click, за такую замечательную работу с этим пакетом.

Это также, надеюсь, иллюстрирует точку зрения, которая заставила меня написать этот пост в первую очередь: CLI и веб-приложения - это не более чем текстовые конечные точки для произвольного кода.

Вернуться на верх