Различие между импортированным пакетом и одним запуском в интерфейсе командной строки при использовании точек входа в Python

#python #entry-point

Вопрос:

У меня есть пакет python, который чаще всего используется в качестве инструмента CLI, но иногда я запускаю его как библиотеку для своих собственных целей. (например, превращаю его в веб-приложение или для модульного тестирования)

Например, я хотел бы sys.exit(1) при ошибке выдать конечному пользователю приятное сообщение об ошибке об исключении при использовании в качестве команды CLI, но raise об исключении, когда оно используется в качестве импортированной библиотеки.

Я использую entry_points в своем setup.py :

 entry_points={
        'console_scripts': [
            'jello=jello.cli:main'
        ]
    }
 

Это отлично работает, но я не могу легко отличить, когда пакет запускается в командной строке или был импортирован, потому __name__ что это всегда jello.cli . Это связано с тем, что точка входа в основном импортирует пакет как обычно.

Я попытался создать __main__.py файл и указать там свою точку входа, но это, похоже, ничего не изменило.

Я подумываю sys.argv[0] о том, чтобы проверить, существует ли там имя моей программы, но это, похоже, хрупкий хак. (в случае, если пользователь использует псевдоним команды или что-то в этом роде) Есть другие идеи или я просто делаю это неправильно? На данный момент я передаю as_lib аргумент своим функциям, чтобы они вели себя по-разному в зависимости от того, загружены ли они как модуль или запущены из интерфейса командной строки, но я хотел бы уйти от этого.

Комментарии:

1. Вы должны попытаться провести различие между импортируемыми/библиотечными частями вашей кодовой базы и CLI. Интерфейс командной строки должен использовать вашу библиотеку, а библиотека должна создавать исключения, которые специально обрабатываются точкой входа.

2. Если я вас правильно понимаю, это звучит так, как будто я должен реорганизовать свой код таким образом, чтобы функции, которые что-то делают, вызывали исключения, но main() функция должна быть посвящена функциональности CLI. Таким образом, я могу try/except внутри main() и предоставлять там приятные сообщения об ошибках вместо того, чтобы делать это в вызываемой функции. Это правда?

3. Да, я добавил пример

Ответ №1:

Это минимальный пример структуры пакета, который может использоваться как a cli и библиотека одновременно.

Это структура каталогов:

 egpkg/
├── setup.py
└── egpkg/
   ├── __init__.py
   ├── lib.py
   └── cli.py
 

Это entry_points вход setup.py . Он идентичен вашему:

     entry_points={
        "console_scripts": [
            "egpkg_cli=egpkg.cli:main",
        ],
    },
 

__init__.py :

 from .lib import func
 

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

 import sys
import argparse

from egpkg import func


def main():
    p = argparse.ArgumentParser()
    p.add_argument("a", type=int)
    args = vars(p.parse_args())

    try:
        result = func(**args)
    except Exception as e:
        sys.exit(str(e))

    print(f"Got result: {result}", file=sys.stdout)
 

lib.py
Здесь определяется ядро вашей библиотеки, и вы должны использовать библиотеку так, как вы ожидаете, что ее будут использовать ваши пользователи. Когда вы получаете значения/входные данные, которые не будут работать, вы можете raise это сделать.

 def func(a):
    if a == 0:
        raise ValueError("Supplied value can't be 0")
    return 10 / a
 

Затем из консоли python или скрипта вы можете:

 In [1]: from egpkg import func
In [2]: func(2)
Out[2]: 5.0
In [3]: func(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/egpkg/egpkg/lib.py", line 3, in func
    raise ValueError("Supplied value can't be 0")
ValueError: Supplied value can't be 0

Supplied value can't be 0
 

И из CLI:

 (venv) ~ egpkg_cli 2
Got result: 5.0
(venv) ~ egpkg_cli 0
Supplied value can't be 0
 

Комментарии:

1. Идеально — это именно то, что мне нужно было сделать. Не знаю, почему я сам об этом не подумал. 😀

Ответ №2:

Я полагаю, что то, что вы хотите, в Python называется «главным охранником». Модуль знает свое имя в любой момент времени. Если модуль выполняется из интерфейса командной строки, его имя таково __main__ . Итак, что вы можете сделать, так это поместить всю необходимую функциональность в какую-либо функцию на уровне модуля, а затем вы можете назвать ее иначе, чем в «главном защитнике», как вы бы назвали ее библиотекой. Вот как выглядит «главный охранник»:

 def myfunc(thing, stuff):
   if not stuff:
      raise MyExc

if __name__ == '__main__':
    try:
       myfunc(x, y)
    except MyExc:
       sys.exit(1)
 

Здесь sys.exit вызывается только при ошибке, если модуль выполняется из командной строки в виде сценария.

Комментарии:

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