Пишем инструменты командной строки на Python с помощью Click

click

Python — невероятно гибкий язык программирования, который хорошо интегрируется с существующими программами. Немало Python-кода написано в виде скриптов и интерфейсов командной строки (CLI).

Инструменты и интерфейсы командной строки — эффективная вещь, так как они позволяют автоматизировать практически всё что угодно. Как следствие, эти интерфейсы с течением времени могут стать довольно сложными.

Обычно всё начинается с простого скрипта на Python, который что-то делает. Например, получает доступ к веб-API и выводит результат в консоль:

# print_user_agent.py
import requests

json = requests.get('http://httpbin.org/user-agent').json()
print(json['user-agent'])

Вы можете запустить этот скрипт с помощью команды python3 print_user_agent.py, и он выведет имя user-agent, использованного для вызова API.

Как и было сказано, довольно простой скрипт.

Но что делать, когда подобная программа растёт и становится всё более сложной?

Решением этого вопроса мы сегодня и займёмся. Вы узнаете об основах написания интерфейсов командной строки на Python и о том, как click позволяет упростить этот процесс.

Используя эти знания, мы шаг за шагом перейдём от простого скрипта к интерфейсу командной строки с аргументами, опциями и полезными инструкциями по использованию. Всё это мы сделаем с помощью библиотеки click.

К концу этой статьи вы будете знать:

  • Почему click — лучшая альтернатива argparse и optparse;
  • Как с его помощью создать простой CLI;
  • Как добавить обязательные аргументы командной строки в ваши скрипты;
  • Как парсить флаги и опции командной строки;
  • Как сделать ваши консольные приложения более удобными, добавив справочный текст.

Вы увидите, как сделать всё это с минимальным количеством шаблонного кода.

Примечание переводчика Код в данной статье написан на Python 3.6, работоспособность на более ранних версиях не гарантируется.

Итак, начнём!

Зачем вам писать скрипты и инструменты для командной строки на Python?

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

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

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

Основы интерфейса командной строки

Интерфейс командной строки (CLI) начинается с имени исполняемого файла. Вы вводите имя в консоль и получаете доступ к главной точке входа скрипта, такого как pip.

В зависимости от сложности CLI обычно есть определённые параметры, которые вы можете передавать скрипту:

  1. Аргумент, который является обязательным параметром. Если его не передать, то CLI вернёт ошибку. Например, в следующей команде click является аргументом: pip install click.
  2. Опция – необязательный параметр, который объединяет имя и значение, например --cache-dir ./my-cache. Вы говорите CLI, что значение ./my-cache должно использоваться как директория для кэша.
  3. Флаг, который включает или выключает определённый сценарий. Вероятно, самым частым является --help. Вы только указываете имя, а CLI самостоятельно интерпретирует значение.

С более сложными CLI, такими как pip или Heroku CLI, вы получаете доступ к набору функций, которые собраны под главной точкой входа. Они обычно называются командами или подкомандами.

Возможно, вы уже использовали CLI, когда устанавливали Python-библиотеку с помощью команды pip install <имя пакета>. Команда install говорит CLI, что вы хотите использовать функцию установки пакета, и даёт вам доступ к параметрам, характерным для этой функции.

Пакеты для работы с командной строкой, доступные в стандартной библиотеке Python 3.x

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

Два наиболее известных пакета для этого — optparse и argparse. Они являются частью стандартной библиотеки Python и добавлены туда по принципу «всё включено».

По большей части они делают одно и то же и работают схожим образом. Главное отличие заключается в том, что optparse не используется начиная с Python 3.2, и argparse считается стандартом для создания CLI в Python.

Вы можете узнать о них больше в документации Python, но, чтобы иметь представление, как выглядит скрипт с argparse, посмотрите на пример ниже:

import argparse

parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
                    help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
                    const=sum, default=max,
                    help='sum the integers (default: find the max)')

args = parser.parse_args()
print(args.accumulate(args.integers))

click против argparse: лучшая альтернатива?

Вероятно, вы смотрите на этот код и думаете: «Что это всё значит?» И это является одной из проблем argparse: код с ним неинтуитивен и сложночитаем.

Поэтому вам может понравиться click.

Click решает ту же проблему, что и optparse и argparse, но немного иначе. Он использует декораторы, поэтому ваши команды должны быть функциями, которые можно обернуть этими декораторами.

С click легко создавать многофункциональный CLI с небольшим количеством кода. И этот код будет легко читаться, даже когда ваш CLI вырастет и станет более сложным.

Пишем простой CLI на Python с помощью click

Вдоволь поговорив о CLI и библиотеках, давайте взглянем на пример, чтобы понять, как написать простой CLI с click. Как и в первом примере, мы создаём простой CLI, который выводит результат в консоль. Это несложно:

# cli.py
import click

@click.command()
def main():
    print("I'm a beautiful CLI ✨")

if __name__ == "__main__":
    main()

Не пугайтесь последних двух строк: это то, как Python запускает функцию main при исполнении файла как скрипта.

Как вы видите, всё, что нам нужно сделать – создать функцию и добавить к ней декоратор @click.command(). Он превращает функцию в команду, которая является главной точкой входа нашего скрипта. Теперь вы можете запустить скрипт через командную строку и увидеть что-то вроде этого:

$ python3 cli.py
I'm a beautiful CLI ✨

Что в click здорово, так это то, что мы получаем некоторые дополнительные возможности просто так. Мы не реализовывали справочную функцию, однако вы можете добавить флаг --help и увидеть базовое сообщение:

$ python3 cli.py --help
Usage: cli.py [OPTIONS]

Options:
  --help  Show this message and exit.

Более реалистичный пример CLI на Python с использованием click

Теперь, когда вы знаете, как click упрощает написание CLI, давайте взглянем на более реалистичный пример. Мы напишем программу, которая позволяет нам взаимодействовать с веб-API.

API, который мы дальше будем использовать, — OpenWeatherMap API. Он предоставляет информацию о текущей погоде, а также прогноз на пять дней для определённого местоположения. Мы начнём с тестового API, который возвращает текущую погоду для места.

Прежде чем мы начнём писать код, давайте познакомимся с API. Для этого можно использовать сервис HTTPie, включая онлайн-терминал.

Давайте посмотрим, что случится, когда мы обратимся к API с Лондоном в качестве местоположения:

$ http --body GET http://samples.openweathermap.org/data/2.5/weather \
  q==London \
  appid==b1b15e88fa797225412429c1c50c122a1
{
    "base": "stations",
    "clouds": {
        "all": 90
    },
    "cod": 200,
    "coord": {
        "lat": 51.51,
        "lon": -0.13
    },
    "dt": 1485789600,
    "id": 2643743,
    "main": {
        "humidity": 81,
        "pressure": 1012,
        "temp": 280.32,
        "temp_max": 281.15,
        "temp_min": 279.15
    },
    "name": "London",
    "sys": {
        "country": "GB",
        "id": 5091,
        "message": 0.0103,
        "sunrise": 1485762037,
        "sunset": 1485794875,
        "type": 1
    },
    "visibility": 10000,
    "weather": [
        {
            "description": "light intensity drizzle",
            "icon": "09d",
            "id": 300,
            "main": "Drizzle"
        }
    ],
    "wind": {
        "deg": 80,
        "speed": 4.1
    }
}

Если вы смущены наличием API-ключа в примере сверху, не переживайте, это тестовый API-ключ, предоставляемый сервисом.

Более важное наблюдение заключается в том, что мы отправляем два параметра (обозначаемые == при использовании HTTPie), чтобы узнать текущую погоду:

  • q — место, в котором мы хотим узнать погоду;
  • appid — наш API-ключ.

Это позволяет нам создать простую реализацию на Python с использованием библиотеки requests (опустим обработку ошибок и неудачных запросов для простоты):

import requests

SAMPLE_API_KEY = 'b1b15e88fa797225412429c1c50c122a1'

def current_weather(location, api_key=SAMPLE_API_KEY):
    url = 'http://samples.openweathermap.org/data/2.5/weather'

    query_params = {
        'q': location,
        'appid': api_key,
    }

    response = requests.get(url, params=query_params)

    return response.json()['weather'][0]['description']

Эта функция делает простой запрос к API, используя два параметра. В качестве обязательного аргумента она принимает location (местоположение), которое должно быть строкой. Также мы можем указать API-ключ, передавая параметр api_key при вызове функции. Это необязательно, так как по умолчанию используется тестовый ключ.

И вот мы видим текущую погоду в Python REPL:

>>> current_weather('London')
'light intensity drizzle'  # впрочем, ничего нового ?

Парсим обязательные параметры с click

Простая функция current_weather позволяет нам создать CLI с местоположением, указанным пользователем. Это должно работать примерно так:

$ python3 cli.py London
The weather in London right now: light intensity drizzle.

Как вы, возможно, догадались, местоположение — это аргумент, поскольку оно является обязательным параметром для нашего погодного CLI.

Как нам сделать это при помощи click? Всё довольно просто, мы используем декоратор под названием argument. Кто бы мог подумать?

Давайте возьмём наш предыдущий пример и слегка изменим его, добавив аргумент location:

@click.command()
@click.argument('location')
def main(location):
    weather = current_weather(location)
    print(f"The weather in {location} right now: {weather}.")

Если этот print выглядит для вас странно, не волнуйтесь — это новый способ форматирования строк в Python 3.6+, который называется f-форматированием.

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

Примечание переводчика Имя аргумента, переданное click, должно совпадать с именем аргумента в объявлении функции.

В нашем случае значение аргумента командной строки location будет передано функции main в качестве аргумента location. Логично, не так ли?

Также вы можете использовать тире в именах, например api-key, которые click переведёт в snake case для имени аргумента в функции, например main(api_key).

Реализация main просто использует нашу функцию current_weather для получения погоды в указанном месте. И затем мы с помощью print выводим полученную информацию.

Готово!

Парсим опциональные параметры с click

Как вы, возможно, догадались, тестовый API ограничивает нас в возможностях. Поэтому, прежде чем мы продолжим, зарегистрируйтесь и получите настоящий API-ключ.

Первое, что нам нужно изменить, — URL, откуда берутся данные о текущей погоде. Это можно сделать, изменив значение переменной url в функции current_weather на URL, указанный в документации OpenWeatherMap:

def current_weather(location, api_key=SAMPLE_API_KEY):
    url = 'https://api.openweathermap.org/data/2.5/weather'

    # дальше всё остаётся как было
    ...

Это изменение приведёт к неработоспособности нашего CLI, так как указанный API-ключ не работает с реальным API. Поэтому давайте добавим новый параметр в наш CLI, который позволит нам указывать API-ключ. Но сначала мы должны решить, будет ли этот параметр аргументом или опцией. Мы сделаем его опцией, так как добавление параметра вроде --api-key делает его более явным и говорящим за себя.
Мы хотим, чтобы наша программа запускалась таким образом:

$ python3 cli.py --api-key  London
The weather in London right now: light intensity drizzle.

Проще простого. Посмотрим, как добавить опцию к нашей существующей команде:

@click.command()
@click.argument('location')
@click.option('--api-key', '-a')
def main(location, api_key):
    weather = current_weather(location, api_key)
    print(f"The weather in {location} right now: {weather}.")

И снова мы добавляем декоратор к нашей функции main. В этот раз мы используем декоратор с говорящим именем @click.option и указываем имя для нашей опции, начинающееся с двух тире. Как вы видите, мы также можем указать сокращение для нашей опции с одним тире, чтобы сэкономить пользователю немного времени.

Как было сказано ранее, click создаёт аргумент для передачи в функцию main из длинного варианта имени. В случае с опцией он убирает впередистоящие тире и переводит её в snake case. Таким образом, --api-key становится api_key.

Чтобы всё заработало, осталось лишь передать API-ключ в функцию current_weather.

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

$ python3 cli.py --api-key  Canmore
The weather in Canmore right now: broken clouds.

Добавляем автоматически генерируемые инструкции по использованию

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

Сначала давайте проверим, что выведет флаг --help после всех сделанных изменений. Довольно неплохо, учитывая что мы не приложили к этому никаких усилий:

$ python3 cli.py --help
Usage: cli.py [OPTIONS] LOCATION

Options:
  -a, --api-key TEXT
  --help              Show this message and exit.

Первое, что нужно исправить, это добавить описание для нашей опции с API-ключом. Всё, что нам для этого нужно сделать, – добавить справочный текст в декоратор @click.option:

@click.command()
@click.argument('location')
@click.option(
    '--api-key', '-a',
    help='your API key for the OpenWeatherMap API',
)
def main(location, api_key):
    ...

Второе (и последнее), что мы сделаем, — добавим документацию для всей click-команды. Самый простой и самый питонический способ сделать это — добавить строку документации в нашу функцию main. Да, нам в любом случае нужно сделать это, поэтому это не лишняя работа:

...
def main(location, api_key):
    """
    A little weather tool that shows you the current weather in a LOCATION of
    your choice. Provide the city name and optionally a two-digit country code.
    Here are two examples:

    1. London,UK

    2. Canmore

    You need a valid API key from OpenWeatherMap for the tool to work. You can
    sign up for a free account at https://openweathermap.org/appid.
    """
    ...

Сложив всё вместе, мы получаем хороший вывод для нашего инструмента:

$ python3 cli.py --help
Usage: cli.py [OPTIONS] LOCATION

  A little weather tool that shows you the current weather in a LOCATION of
  your choice. Provide the city name and optionally a two-digit country
  code. Here are two examples:

  1. London,UK

  2. Canmore

  You need a valid API key from OpenWeatherMap for the tool to work. You can
  sign up for a free account at https://openweathermap.org/appid.

Options:
  -a, --api-key TEXT  your API key for the OpenWeatherMap API
  --help              Show this message and exit.

Подводим итоги

Итак, в этом уроке мы рассмотрели много всего. Можете гордиться собой, вы написали свой собственный CLI, и всё это с минимальным количеством шаблонного кода! Исходный код ниже доказывает это. Не стесняйтесь использовать его для собственных экспериментов:

import click
import requests

SAMPLE_API_KEY = 'b1b15e88fa797225412429c1c50c122a1'


def current_weather(location, api_key=SAMPLE_API_KEY):
    url = 'https://api.openweathermap.org/data/2.5/weather'

    query_params = {
        'q': location,
        'appid': api_key,
    }

    response = requests.get(url, params=query_params)

    return response.json()['weather'][0]['description']


@click.command()
@click.argument('location')
@click.option(
    '--api-key', '-a',
    help='your API key for the OpenWeatherMap API',
)
def main(location, api_key):
    """
    A little weather tool that shows you the current weather in a LOCATION of
    your choice. Provide the city name and optionally a two-digit country code.
    Here are two examples:
    1. London,UK
    2. Canmore
    You need a valid API key from OpenWeatherMap for the tool to work. You can
    sign up for a free account at https://openweathermap.org/appid.
    """
    weather = current_weather(location, api_key)
    print(f"The weather in {location} right now: {weather}.")


if __name__ == "__main__":
    main()

Перевод статьи «Writing Python Command-Line Tools With Click»

Наши тесты для вас:
Тест на знание сленга веб-разработчиков.
Что вы знаете о работе мозга?
А вы точно программист?