#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__
.