Это история о том, как потерпеть фиаско, имея хорошо написанный и протестированный в боевых условиях работающий код и даже написанную документацию. Изначально я собирался делать анонс своей библиотеки, но что-то пошло не так. Поэтому начнём за здравие -- постановка и формализация задачи, описание возможностей и батареек. А закончим за упокой -- вопросами, как всё это теперь тестировать? Начну с формализации проблемы. Современное asyncio приложение состоит из множества компонентов типа Redis, БД, брокера и прочих. Эти компоненты необходимо инициализовать и останавливать в правильном порядке. Также очень хочется из коробки работающий reload. Вишенкой же на торте будет точно так же инициализорованный REPL, а также простота написания одноразовых скриптов для разрешения косяков на проде. Читать статью я рекомендую, глядя одним глазом в документацию. Согласитесь, нет смысла её просто переводить, вместо этого её стоит расширить Когда точка входа одна, добавление компонентов происходит постепенно и незаметно. Когда появляется вторая точка входа, будь то ещё один микросервис, REPL или скрипт -- вам приходится решать задачу запуска заново. Это особенно смешно в случае скрипта и остро напоминает проблему банана и гориллы -- вы хотели всего лишь банан, но для этого вам пришлось написать код для инициализации гориллы и всех джунглей, а после ещё для остановки Я с ходу настаиваю на добавлении REPL -- ещё одной точки входа Я предлагаю смотреть на приложение как на конструктор компонентов. Идея в том, что каждый компонент может требовать для своего запуска и остановки исполнения некоторого кода. Этот некоторый код хорошо изолируется в собственные классы-миксины. Итоговое приложение является комбинацией этих миксинов. Создание новой точки входа сводится к импорту написанных ранее компонентов наследованию от них нового класса Я выделил 6 основных этапов жизненного цикла приложения: on_init -- код исполняется единожды только в момент инициализации; pre_startup -- код исполняется до запуска event loop в том числе во время reload; on_startup -- код исполняется сразу после инициализации event loop; on_shutdown -- код исполняется, когда приложение приняло решение остановиться, но event loop ещё работает; post_shutdown -- код исполняется сразу после остановки / очистки event loop. В случае reload следующим этапом будет pre_startup; on_exit -- код исполняется единожды только в момент остановки приложения. Я выделил 3 основных сценария: Ведомое приложение, запускаемое каким-то фреймворком; "Сам себе хозяин" -- демоны, скрипты и REPL; Ведущее приложение или фреймворк, запускающий клиентское приложение. Есть необходимость выполнить код прямо перед или прямо после какого-то существующего этапа жизненного цикла? Легко Допустим, есть необходимость дважды вклиниться прямо перед on_startup. Для этого необходимо создать интерфейс: После от этого интерфейса наследоваться. В результате методы будут выполнены в следующем порядке: before_2_on_startup -> before_on_startup -> on_startup Естественно, вклинивать этапы можно пока не надоест Хороший пример кастомных этапов жизненного цикла рассмотрен в документации Казалось бы, что может пойти не так, когда батарейки в комплекте? Пройдёмся по болевым точкам и откроем стандартный asyncio REPL: Номинально работает, но: меня бесит некорректная обработка Ctrl+C А что с автодополнениями по Tab? Почему не работают? Окей, как теперь инициализировать окружение? Правильно, ручками Ничего сверхестественного: Корректная обработка Ctrl+C Автодополнения должны работать, если есть readline Окружение должно инициализироваться компонентно и так же останавливаться Добавление собственных глобальных переменных (импортированных модулей) в неймспейс Всё это тоже работает. Больше примеров в документации. В процессе пришлось через ctypes лезть в недра readline и заниматься знаменитым брутфорс-программированием. Питонячий readline -- это стыдоба Согласитесь, тестировать своим продом не совсем правильно. Вопрос необходимо формализовать. С учётом возможностей, простейшая с виду задача превращается в рак мозга. Есть непроверенный функционал -- метод reload, вызов которого должен запустить процесс остановки и последующего перезапуска. Очевидно, он должен вести себя одинаково во всех сценариях, а все нюансы -- половые трудности уже systempy. А различий есть у меня! В REPL вместо штатного reload необходимо выполнять reload_threadsafe -- и это совсем не одно и то же. У меня ломается мозг, как проверить работу метода reload в других сценариях работы В то же время есть казалось бы простые моменты, которые протестировать и можно. Например, регистрируется ли Target, и даже в каким порядке выполняются методы, где systempy кидает исключения, и так далее. Смех в том, что там отсутствуют оси изменений А ещё очень неприятно ломается REPL, и в случае ошибок забирает за собой и терминал -- спасает только закрытие и открытие нового Если ли у хабра идеи? Или воспользоваться запасным сценарием?   Разработчик