Импорт модуля запускает работу: отделяем CLI от функций Python
Нужно повторно использовать функцию из скрипта в тесте или другом приложении. Добавляем import, и внезапно начинается обработка файлов либо парсер требует обязательный аргумент командной строки. Функция здесь ни при чём: Python исполняет верхний уровень модуля при его первоначальном импорте. Код запуска смешался с определениями, которые хотелось использовать отдельно.
Полезная граница проходит между функцией, выполняющей предметную работу, и точкой входа, которая читает argv и выбирает код завершения. Точку входа оформим как main(argv=None). Такой аргумент позволяет тесту передать список напрямую, не изменяя sys.argv всего процесса. Обычный запуск оставит None, и argparse прочитает аргументы команды.
Точка входа main(argv)
Защитный if с __name__ нужен только в самом низу файла. Он не превращает весь модуль в функцию и не скрывает определения от импорта. При запуске файла имя равно __main__, при импорте используется имя модуля. Поэтому работа начинается по явному решению точки входа.
import argparse
from collections.abc import Sequence
def greet(name: str) -> str:
return f"Hello, {name}"
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("name")
args = parser.parse_args(argv)
print(greet(args.name))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Импорт и запуск по отдельности
Сохраните код как greet.py. Команда python greet.py Ann напечатает Hello, Ann и завершится с нулевым кодом. Команда python -c "import greet" не должна печатать приветствие или требовать имя. Проверить логику можно вызовом main(["Ann"]) из теста с захватом stdout. Отдельный тест отсутствующего аргумента проверяет поведение парсера, а не самой функции greet.
Не помещайте создание сетевого клиента или чтение обязательных переменных окружения на верхний уровень рядом с константами. main должен собрать зависимости после разбора аргументов, передать их рабочей функции и закрыть ресурсы при завершении. Тогда импорт остаётся дешёвым, а тесты не требуют секретов и доступных внешних сервисов только для обнаружения функций.
Код завершения тоже часть интерфейса команды. Возвращаем int и передаём его в SystemExit. Если вернуть строку и передать её sys.exit, Python выведет эту строку как сообщение ошибки и завершится с ненулевым статусом. Сообщение успешной операции нужно печатать отдельно, а код статуса оставить числом.
После разделения появляется возможность подключить console script пакета к той же main или вызывать программу через python -m. При этом предметная функция не знает, из какой оболочки пришёл запрос. Проверьте обе формы использования: импорт без действий и явный вызов с результатом. Это небольшое изменение часто снимает сразу несколько причин нестабильного сбора тестов.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
