Рубрики
Без рубрики

Ultimate Python Main ()

Вы когда -нибудь создавали сценарий Python “Bin”? Это руководство, чтобы сделать его максимально защищенным и удобным. Tagged с Python, Tuperial, Learning.

Итак, вы хотите написать утилиту CLI в Python. Цель состоит в том, чтобы быть в состоянии написать:

$ ./say_something.py -w "hello, world"
hello, world

А потом, чтобы запустить ваш сценарий. Это не так сложно, и вы, вероятно, уже это сделали, но думали ли вы обо всех вариантах использования? Пойдем шаг за все, что вы можете сделать, чтобы сделать свой main () пулепроницаемый. Первые разделы будут очень простыми, и мы создадим что -то менее очевидное по мере продвижения.

Привет, мир

Давайте начнем с базового сценария, который просто говорит «Привет, мир».

print("hello, world")

Теперь это довольно просто, но все еще не будет работать, если вы называете это напрямую, как ./say_something.py . Чтобы это работало, вам нужно добавить знаменитый Шебанг в начале.

#!/usr/bin/python3

print("hello, world")

Примечание – Давайте также не забудем сделать chmod a+x say_something.py дать права на выполнение в файле

Однако что, если вы используете VirtualEnv? Позвонив /usr/bin/python3 Прямо вы заставляете путь к переводчику Python. Это может быть желательным в некоторых случаях (примером менеджеров пакетов будет стремиться к безопасности) Но если вы пишете распределяемый сценарий, проще получить его из среды.

#!/usr/bin/env python3

print("hello, world")

Примечание – Мы звоняем Python3 вместо Python Потому что в некоторых системах вы найдете это Python указывает на Python 2 и кто хочет использовать Python 2?

Импортируемость

Что-то, что вы выучите рано из руководств,-это то, что вы хотите убедиться, что если по какой-то причине кто-то импортирует ваш модуль, то не будет никакого побочного эффекта, как сюрприз print () Анкет

Из -за этого вам нужно «защитить» свой код с помощью этой специальной идиомы:

#!/usr/bin/env python3

if __name__ == "__main__":
    print("hello, world")

Вы заметите использование специальной переменной __name__ чья ценность будет __главный__ Если вы находитесь в файле «корневой», который работает Python, и имена модуля в противном случае.

Аргументы разбора

Вы заметите, что до сих пор программа говорит только «Привет, мир», но не разбирается -W аргумент, который мы описали выше. Аргументы на основе Python ничем не отличаются от C – для тех, кто имел шанс изучить его в школе – за исключением того, что это хорошо, так что у вас есть еще несколько вкусностей.

Основное отличие состоит в том, что вместо того, чтобы получать аргументы как аргумент main () Вам придется импортировать их.

#!/usr/bin/env python3
from sys import argv, stderr


def fail(msg: str):
    stderr.write(f"{msg}\n")
    exit(1)


if __name__ == "__main__":
    what = "hello, world"

    if len(argv) == 1:
        pass
    elif len(argv) == 3:
        if argv[1] != "-w":
            fail(f'Unrecognized argument "{argv[1]}"')
        what = argv[2]
    else:
        fail("Too many/few arguments")

    print(what)

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

#!/usr/bin/env python3
from argparse import ArgumentParser


if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    args = parser.parse_args()

    print(args.what)

Таким образом, вы позволяете всей тяжелой работе ArgementParser () И в качестве бонуса вы получаете автопогенерированный текст справки при звонке с -h как аргумент.

Внешний звонок

Все это круто, но что, если вы хотите вызвать инструмент CLI из другой программы Python? Опыт показал мне, что зачастую это гораздо более прагматичная вещь, а не разработать конкретный API для Python и другой API для CLI (это аргументы).

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

К счастью, есть простое решение для этого: создать C-стиль main () функция

#!/usr/bin/env python3
from argparse import ArgumentParser


def main():
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    args = parser.parse_args()

    print(args.what)


if __name__ == "__main__":
    main()

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

#!/usr/bin/env python3
from argparse import ArgumentParser
from typing import Sequence, Optional


def main(argv: Optional[Sequence[str]] = None):
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    args = parser.parse_args(argv)

    print(args.what)


if __name__ == "__main__":
    main()

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

>>> from say_something import main
>>> main(["-w", "hello, world"])
hello, world

Немного помощи

Для хорошей меры и ясности давайте выведем аргументы, анализирующие отдельную функцию.

#!/usr/bin/env python3
from argparse import ArgumentParser, Namespace
from typing import Sequence, Optional


def parse_args(argv: Optional[Sequence[str]] = None) -> Namespace:
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    return parser.parse_args(argv)


def main(argv: Optional[Sequence[str]] = None):
    args = parse_args(argv)
    print(args.what)


if __name__ == "__main__":
    main()

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

#!/usr/bin/env python3
from argparse import ArgumentParser
from typing import Sequence, Optional, NamedTuple


class Args(NamedTuple):
    what: str


def parse_args(argv: Optional[Sequence[str]] = None) -> Args:
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    return Args(**parser.parse_args(argv).__dict__)


def main(argv: Optional[Sequence[str]] = None):
    args = parse_args(argv)
    print(args.what)


if __name__ == "__main__":
    main()

Обработка сигналов

Для этой демонстрации давайте добавим в этот код немного «сна», чтобы имитировать, что он делает что -то вместо того, чтобы печатать текст сразу.

А что, если программа получает сигнал? Есть две основные вещи, с которыми вы хотите справиться:

  • Sigint – Когда пользователь делает Ctrl+c в их терминале, который поднимает Keyboardintrupt
  • Sigterm – Когда пользователь любезно просит программу умереть с Термин сигнал, с которым можно обработать (в отличие от sigkill ) Но мы увидим это позже

Начнем с обработки Ctrl+c :

#!/usr/bin/env python3
from argparse import ArgumentParser
from time import sleep
from typing import Sequence, Optional, NamedTuple
from sys import stderr


class Args(NamedTuple):
    what: str


def parse_args(argv: Optional[Sequence[str]] = None) -> Args:
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    return Args(**parser.parse_args(argv).__dict__)


def main(argv: Optional[Sequence[str]] = None):
    args = parse_args(argv)
    sleep(100)
    print(args.what)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        stderr.write("ok, bye\n")
        exit(1)

Как видите, это так же просто, как поймать Keyboardintrupt исключение. Обратите внимание, что мы делаем это за пределами main () Поскольку у знаменитого гипотетического «модуля вызывающего абонента» уже будет обработка сигналов, так что это то, что нам нужно настроить только в том случае, если мы работаем на сами.

Далее идет обработка Sigterm Анкет По умолчанию программа просто остановится, но мы не хотим этого. Примером, если мы используем следующий шаблон внутри кода:

try:
    # do something
finally:
    # cleanup

У нас не будет возможности очистить, если исключение не поднято. К счастью для нас, мы можем поднять исключение для себя.

#!/usr/bin/env python3
from argparse import ArgumentParser
from time import sleep
from typing import Sequence, Optional, NamedTuple
from sys import stderr
from signal import signal, SIGTERM


class Args(NamedTuple):
    what: str


def parse_args(argv: Optional[Sequence[str]] = None) -> Args:
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    return Args(**parser.parse_args(argv).__dict__)


def sigterm_handler(_, __):
    raise SystemExit(1)


def main(argv: Optional[Sequence[str]] = None):
    args = parse_args(argv)
    sleep(100)
    print(args.what)


if __name__ == "__main__":
    signal(SIGTERM, sigterm_handler)

    try:
        main()
    except KeyboardInterrupt:
        stderr.write("ok, bye\n")
        exit(1)

Что происходит, так это то, что мы используем сигнал () Чтобы зарегистрироваться sigterm_handler () как наш обработчик для Sigterm Анкет То, что происходит «под капотом», так это то, что обработчик сигнала будет выполнен до «следующей инструкции», которую иначе рассмотрел бы переводчик. Это дает вам шанс поднять исключение, которое будет пузыриться до нашего __main__ и выйти из программы с 1 Возврат код, запуская все Наконец и контекстные менеджеры на этом пути.

Как указывалось ранее, есть Sleep () в середине функции. Это означает, что вы можете запустить код из этого раздела, а затем нажать Ctlr+c или отправить Sigterm Чтобы увидеть, что происходит, когда вы прерываете программу.

Отчет об ошибках

Иногда – чаще всего, что вы хотели бы – ваша программа потерпела неудачу по своей собственной ошибке, либо потому, что входные данные неверны. Например, если пользователь пытается открыть файл, который не существует, вы можете сообщить об этом. И под «сообщить об этом» я имею в виду, не с резкой трассировкой стека. Держите трассировку стека для тех случаев, которые вы не ожидали таким образом, вы знаете, что что -то действительно не так.

Давайте представим, что мы хотим запретить пользователю сказать «ни». Чтобы сообщить об ошибке, мы собираемся создать определенный тип ошибки для нашей программы

class SaySomethingError(Exception):
    pass

Тогда мы собираемся справиться с этим в нашем __main__ :

    except SaySomethingError as e:
        stderr.write(f"Error: {e}")
        exit(1)

И, наконец, мы собираемся выразить ошибку из главный()

    if args.what == "ni":
        raise SaySomethingError('Saying "ni" is forbidden')

Для общего кода:

#!/usr/bin/env python3
from argparse import ArgumentParser
from typing import Sequence, Optional, NamedTuple
from sys import stderr
from signal import signal, SIGTERM


class Args(NamedTuple):
    what: str


class SaySomethingError(Exception):
    pass


def parse_args(argv: Optional[Sequence[str]] = None) -> Args:
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    return Args(**parser.parse_args(argv).__dict__)


def sigterm_handler(_, __):
    raise SystemExit(1)


def main(argv: Optional[Sequence[str]] = None):
    args = parse_args(argv)

    if args.what == "ni":
        raise SaySomethingError('Saying "ni" is forbidden')

    print(args.what)


if __name__ == "__main__":
    signal(SIGTERM, sigterm_handler)

    try:
        main()
    except KeyboardInterrupt:
        stderr.write("ok, bye\n")
        exit(1)
    except SaySomethingError as e:
        stderr.write(f"Error: {e}")
        exit(1)

С помощью этой очень простой системы вы можете выразить ошибку из своего кода, независимо от того, насколько вы глубоки. Это запустит все функции очистки от контекстных менеджеров и Наконец блоки. И, наконец, это будет поймано __main__ Чтобы отобразить правильное сообщение об ошибке.

Упаковка

Хорошая идея при предоставлении бин Скрипт внутри пакета – это вызвать его с Python’s -m вариант. Примером вместо написания пип , Я обычно пишу Python3 -m pip Чтобы быть уверенным в том, что PIP, который я работаю, действительно из моей виртуальной Env. В качестве бонуса вам не нужна среда бин каталог в вашем $ Path Анкет Вы обнаружите, что наиболее известные и не очень известные пакеты предоставляют оба способа назвать свои двоичные файлы.

Для этого вам нужно поместить свой сценарий в __main__.py файл. Давайте сделаем это (это команды UNIX, но у Windows не должно быть проблем с переводом):

mkdir say_something
touch say_something/__init__.py
mv say_something.py say_something/__main__.py

Теперь вы можете позвонить своему скрипту с python3 -m say_something Анкет

Примечание – Мы создаем пустой __init__.py Файл, чтобы показать на Python, что это правильный модуль

Здесь явно становится вопросом предпочтений, но мой личный любимый инструмент для упаковки стал Поэзия Потому что это так просто. Давайте создадим базовый pyproject.toml для нашего проекта.

[tool.poetry]
name = "say_something"
version = "0.1.0"
description = ""
authors = []
license = "WTFPL"

[tool.poetry.dependencies]
python = "^3.8"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

Давайте попробуем это, чтобы посмотреть, сработало ли это

poetry run python -m say_something

Это хорошо, но команда пошла? Не бойтесь, поэзия позволяет вам довольно легко объявить ваши команды “бина”. Единственное, что ему нужна функция, чтобы вызывать напрямую, включая все махинации обработки сигналов. Тогда давайте переместим все это в отдельную функцию:

#!/usr/bin/env python3
from argparse import ArgumentParser
from typing import Sequence, Optional, NamedTuple
from sys import stderr
from signal import signal, SIGTERM


class Args(NamedTuple):
    what: str


class SaySomethingError(Exception):
    pass


def parse_args(argv: Optional[Sequence[str]] = None) -> Args:
    parser = ArgumentParser()
    parser.add_argument("-w", "--what", default="hello, world")

    return Args(**parser.parse_args(argv).__dict__)


def sigterm_handler(_, __):
    raise SystemExit(1)


def main(argv: Optional[Sequence[str]] = None):
    args = parse_args(argv)

    if args.what == "ni":
        raise SaySomethingError('Saying "ni" is forbidden')

    print(args.what)


def __main__():
    signal(SIGTERM, sigterm_handler)

    try:
        main()
    except KeyboardInterrupt:
        stderr.write("ok, bye\n")
        exit(1)
    except SaySomethingError as e:
        stderr.write(f"Error: {e}")
        exit(1)


if __name__ == "__main__":
    __main__()

Теперь мы можем добавить следующее в pyproject.toml файл:

[tool.poetry.scripts]
say_something = "say_something.__main__:__main__"

И наконец, давайте попробуем это:

poetry run say_something -w "hello, poetry"

Таким образом, когда кто -то установит ваш пакет, у него будет Say_something команда доступна.

Вывод

Образец, представленная в этой статье, является той, которую я использую все время Анкет Странно, я никогда не делал подходящего шаблона для этого Но теперь, когда я написал эту статью, я знаю, где ее прийти и скопировать ее. Я надеюсь, что вы нашли это полезным!

Оригинал: “https://dev.to/xowap/the-ultimate-python-main-18kn”