Генератор, который тихо потерял половину командной строки
Справочник команд на этом сайте генерируется. Первая версия генератора выдала аккуратный, отсортированный и совершенно убедительный JSON, в котором не хватало 53 подкоманд, — и не выдала ни одной ошибки.
У командной строки Chimera 109 вызовов: 56 корневых команд плюс 53 подкоманды в 11 группах. Документация описывает удачный путь примерно для семнадцати из них и написана человеком — это правильный способ писать руководство для начинающих и неправильный способ поддерживать исчерпывающий справочник. Список флагов, скопированный руками, верен ровно один раз — в день, когда его набрали.
Поэтому справочник на этом сайте генерируется из самой командной строки — так же, как типы TypeScript для приложения генерируются из схемы API, и с тем же заслоном от расхождения в CI: перегенерировать и упасть, если закоммиченная копия отличается.
Ошибка
Первая версия обходила дерево команд так:
root = typer.main.get_command(cli_app)
if not isinstance(root, click.Group):
raise TypeError("the Chimera CLI is expected to be a command group")
Не прошла проверка. Не обход — именно проверка. typer.main.get_command() вернул объект, который не
является экземпляром установленного click.Group, потому что Typer 0.27 везёт с собой собственную
копию Click под именем typer._click. TyperGroup наследуется от typer._click.core.Command, а
click, который импортирует ваш собственный код, — это совершенно другой объект-класс.
То, что она подняла исключение, было везением. Версия до неё поступала очевидным образом — считала
листовой командой всё, что не является click.Group, — и вот та не падала. Она выдавала
правильно оформленный, отсортированный, детерминированный JSON с описанием 56 команд, где каждая
группа была сплющена в одну запись, а все 53 подкоманды исчезли.
Почему это худший из возможных отказов
Генератор, который падает, сообщает вам, что он не справился. Генератор, который выдаёт
правдоподобный результат, не сообщает ничего, и всё, что стоит ниже по течению, наследует это
молчание. Страница справочника отрисовалась бы прекрасно. chimera kanban значилась бы со своим
текстом справки и без подкоманд, что читается ровно как команда, у которой подкоманд нет. Поиск бы
её проиндексировал. Никто, глядя на страницу, не смог бы заметить отсутствие, потому что полная
страница выглядит точно так же.
Отказ поймали счётом, а не взглядом: справочник должен показывать 109 вызовов, а показывал 56.
Исправление и тест, который его переживёт
Теперь выгрузчик работает по утиной типизации — он спрашивает, есть ли у объекта отображение
commands, а не какого он класса. Это переживёт и вложенный Click, и обновление Typer, и смену
мажорной версии Click, потому что «есть подкоманды» — свойство самой вещи, а «является
click.Group» — свойство графа импортов.
Полезнее самого исправления — форма теста. Очевидное утверждение состоит в том, что выгрузка удалась:
def test_dump_works():
assert build() # passes with 53 subcommands missing
А утверждение, которое ловит этот отказ, говорит о том, что было найдено:
def test_finds_the_groups_and_not_just_the_leaves():
groups = [c for c in build()["commands"] if "commands" in c]
assert len(groups) >= 10
Любой тест, утверждающий, что генератор отработал, — это тест, который проходит, пока генератор ошибается. Писать стоит тот, который утверждает, что именно генератор должен был найти.
Куда смотреть
- Выгрузчик и его тесты:
chimera/cli/schema_dump.pyиtests/test_cli_schema_dump.py - Вложенный в Typer Click, если хотите убедиться в механизме сами:
typer/_click
https://chimeraagent.space