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 и веб-приложения - это не более чем текстовые конечные точки для произвольного кода.
Вернуться на верх