скрипты: запись настроек перестала уходить мимо и молчать
Найдено ревью, каждое воспроизведено на фикстуре. Граница репозитория. find_root обходил всех предков в поисках .av-dev.toml и только потом смотрел на .git: вложенный проект объявлялся здоровым по конфигу соседа, а init дописывал секцию в чужой репозиторий. Подъём останавливается на первом .git. Туда же ключ [tasks] dir: он не судился как путь, и «../соседний» уводил запись за пределы репозитория с зелёным кодом. Запись TOML. Значения склеивались в кавычки без экранирования — имя индекса с кавычкой ломало файл целиком, и оба скрипта после этого отвечали кодом 3 на любую команду. Заголовок секции искался точным сравнением строки, так что «[tasks] # комментарий» — ровно та возможность, ради которой взят формат, — не находился, и дописывалась вторая таблица. set_version правил version внутри секции и ломал файл, если версия в кавычках. Молчание вместо отказа. Неизвестные ключи не отвергались: migrations, положенный верхним уровнем (а именно так его перенесут руками из .docs.json), давал «проверка неприменима» и зелёный итог. Неверный dir проваливался в поиск вверх, и скрипт работал в другом каталоге, не сказав о ключе. Прежние файлы узнавались только когда нового нет — половина переезда проходила молча. Честность доклада. Версию не двигал никто: set_version был написан и не подключён, init печатал версию, которой не записал, adopt писал dir мимо --target. Теперь повышение — команда docs.py bump, а init и adopt зовут общий write_config, который пишет версию, дописывает ключи и вслух называет разошедшиеся. Отсутствие shared/config.py давало трейсбек и код 1 вместо 3. Число зовётся LAYOUT_VERSION в обоих скриптах вместо CANON_VERSION и FORMAT_VERSION.
This commit is contained in:
@@ -37,8 +37,11 @@ def _load_shared() -> ModuleType:
|
||||
Своё дерево — единственное, куда ходить можно; в чужое не ходим никогда.
|
||||
"""
|
||||
path = Path(__file__).resolve().parents[3] / "shared" / "config.py"
|
||||
# Проверка именно файлом: `spec_from_file_location` на отсутствующем пути
|
||||
# возвращает исправный спек, и падает уже `exec_module` — трейсбеком и кодом
|
||||
# 1, то есть «найден дрейф, чинится». Битая установка дрейфом не является.
|
||||
spec = importlib.util.spec_from_file_location("avdev_config", path)
|
||||
if spec is None or spec.loader is None:
|
||||
if not path.is_file() or spec is None or spec.loader is None:
|
||||
print(f"ОТКАЗ: не читается {path} — общий читатель настроек;"
|
||||
f" переустанови плагин av-dev", file=sys.stderr)
|
||||
sys.exit(ENV)
|
||||
@@ -51,7 +54,7 @@ conf = _load_shared()
|
||||
|
||||
# Версия раскладки одна на плагин и живёт в `shared/config.py`: её знают оба
|
||||
# скрипта, и второе число здесь было бы вторым домом.
|
||||
CANON_VERSION = conf.VERSION
|
||||
LAYOUT_VERSION = conf.VERSION
|
||||
|
||||
# Дом версии и путей, нужных проверкам, — `.av-dev.toml` в корне репозитория.
|
||||
# До слияния плагинов файлов было два, `docs/.docs.json` и `.tasks.json`, и
|
||||
@@ -288,9 +291,16 @@ def fail(code: int, msg: str) -> NoReturn:
|
||||
def read_config(root: Path, rep: Report) -> dict:
|
||||
"""Настройки проекта целиком; проверкам канона нужна секция `[docs]`."""
|
||||
try:
|
||||
return conf.read(root)
|
||||
cfg = conf.read(root)
|
||||
conf.check_keys(docs_cfg(cfg), DOCS_KEYS, "в секции [docs]")
|
||||
except conf.ConfigError as exc:
|
||||
fail(ENV, str(exc))
|
||||
return cfg
|
||||
|
||||
|
||||
# Ключи секции `[docs]`. Секцию знает этот скрипт, а не общий читатель: ключ
|
||||
# заводится вместе с проверкой, которая его читает.
|
||||
DOCS_KEYS = ("migrations",)
|
||||
|
||||
|
||||
def docs_cfg(cfg: dict) -> dict:
|
||||
@@ -307,15 +317,15 @@ def check_version(root: Path, cfg: dict, rep: Report) -> None:
|
||||
if got is None:
|
||||
rep.error(f"в {CONFIG} нет ключа version — версия раскладки не объявлена")
|
||||
return
|
||||
if got < CANON_VERSION:
|
||||
if got < LAYOUT_VERSION:
|
||||
rep.error(
|
||||
f"проект приведён к раскладке версии {got}, текущая — {CANON_VERSION}: "
|
||||
f"нужен canon upgrade"
|
||||
f"проект приведён к раскладке версии {got}, текущая — {LAYOUT_VERSION}:"
|
||||
f" нужно повышение (скилл av-dev:doc-canon, операция upgrade)"
|
||||
)
|
||||
elif got > CANON_VERSION:
|
||||
elif got > LAYOUT_VERSION:
|
||||
rep.error(
|
||||
f"проект приведён к раскладке версии {got}, а скрипт знает"
|
||||
f" {CANON_VERSION}: устарел плагин, обнови маркетплейс"
|
||||
f" {LAYOUT_VERSION}: устарел плагин, обнови маркетплейс"
|
||||
)
|
||||
|
||||
|
||||
@@ -345,24 +355,40 @@ def doc_home(root: Path, name: str) -> tuple[Path | None, str | None]:
|
||||
return None, None
|
||||
|
||||
|
||||
def check_legacy(root: Path, rep: Report) -> None:
|
||||
"""Следы прежней раскладки — отдельная проверка, а не ветка отсутствия.
|
||||
|
||||
Пока она жила внутри «нового файла нет», половина переезда проходила молча:
|
||||
завели `.av-dev.toml`, старые файлы удалить забыли — и оба скрипта считали
|
||||
проект здоровым. Это ровно тот второй дом, против которого переезд и
|
||||
делался, и увидеть его можно только тогда, когда новый файл уже есть.
|
||||
"""
|
||||
legacy = conf.legacy_files(root)
|
||||
if not legacy:
|
||||
return
|
||||
if (root / CONFIG).is_file():
|
||||
rep.error(
|
||||
f"прежняя раскладка не убрана: {', '.join(legacy)} рядом с {CONFIG}."
|
||||
f" Эти файлы не читаются, и версия в них своя — второй дом для того"
|
||||
f" же числа. Удали их: переезд не закончен (журнал, версия 1, шаг 3)"
|
||||
)
|
||||
return
|
||||
rep.error(
|
||||
f"нет {CONFIG}, а настройки лежат по прежней раскладке"
|
||||
f" ({', '.join(legacy)}) — она осталась от трёх плагинов, которые"
|
||||
f" слились в один: перенеси значения и удали старые файлы операцией"
|
||||
f" upgrade скилла av-dev:doc-canon (журнал, версия 1). Прежние имена не"
|
||||
f" читаются, поэтому в этом прогоне всё остальное проверено так, будто"
|
||||
f" настроек нет вовсе"
|
||||
)
|
||||
|
||||
|
||||
def check_required(root: Path, cfg: dict, rep: Report) -> None:
|
||||
for rel, what in REQUIRED.items():
|
||||
if (root / rel).exists():
|
||||
continue
|
||||
# Настройки под прежними именами — это не «нет файла», а незаконченный
|
||||
# переезд. Без этой ветки проект слышал бы «нет версии» и шёл заводить
|
||||
# второй файл рядом с первым, а старые остались бы вторым домом.
|
||||
legacy = conf.legacy_files(root)
|
||||
if rel == CONFIG and legacy:
|
||||
rep.error(
|
||||
f"нет {rel} — {what}. Настройки лежат по прежней раскладке"
|
||||
f" ({', '.join(legacy)}) — она осталась от трёх плагинов, которые"
|
||||
f" слились в один: перенеси значения и удали старые файлы"
|
||||
f" операцией upgrade скилла av-dev:doc-canon (журнал, версия 1)."
|
||||
f" Прежние имена не читаются, поэтому в этом прогоне всё"
|
||||
f" остальное проверено так, будто настроек нет вовсе"
|
||||
)
|
||||
continue
|
||||
if rel == CONFIG and conf.legacy_files(root):
|
||||
continue # об этом уже сказала check_legacy, и подробнее
|
||||
rep.error(f"нет {rel} — {what}")
|
||||
|
||||
for name, (kind, what) in DOCS.items():
|
||||
@@ -640,6 +666,7 @@ def cmd_check(args: argparse.Namespace) -> int:
|
||||
rep = Report()
|
||||
cfg = read_config(root, rep)
|
||||
check_version(root, cfg, rep)
|
||||
check_legacy(root, rep)
|
||||
check_required(root, cfg, rep)
|
||||
check_stray(root, rep)
|
||||
check_slugs(root, rep)
|
||||
@@ -652,10 +679,36 @@ def cmd_check(args: argparse.Namespace) -> int:
|
||||
|
||||
def cmd_version(args: argparse.Namespace) -> int:
|
||||
root = Path(args.dir).resolve()
|
||||
if not root.is_dir():
|
||||
fail(ENV, f"нет каталога {root}")
|
||||
cfg = read_config(root, Report())
|
||||
got = conf.version(cfg)
|
||||
print(f"раскладка скрипта: {CANON_VERSION}")
|
||||
print(f"раскладка проекта: {got if got is not None else 'не объявлена'}")
|
||||
print(f"версия раскладки, скрипт: {LAYOUT_VERSION}")
|
||||
print(f"версия раскладки, проект: {got if got is not None else 'не объявлена'}")
|
||||
return OK
|
||||
|
||||
|
||||
def cmd_bump(args: argparse.Namespace) -> int:
|
||||
"""Поднять версию проекта до той, что знает скрипт. Последний шаг повышения.
|
||||
|
||||
Двигается **строка**, а не файл: комментарии в нём принадлежат проекту.
|
||||
Поднять раньше времени нельзя не потому, что скрипт не даст, а потому что
|
||||
число объявляет пройденными шаги журнала, которых никто не делал, — поэтому
|
||||
команда отдельная и зовётся руками, а `check --fix` этого не пишет.
|
||||
"""
|
||||
root = Path(args.dir).resolve()
|
||||
if not (root / CONFIG).is_file():
|
||||
fail(ENV, f"нет {root / CONFIG} — сперва заведи раскладку (adopt)")
|
||||
was = conf.version(read_config(root, Report()))
|
||||
if was == LAYOUT_VERSION:
|
||||
print(f"версия уже {LAYOUT_VERSION}, файл не тронут")
|
||||
return OK
|
||||
if was is not None and was > LAYOUT_VERSION:
|
||||
fail(ENV, f"проект на версии {was}, скрипт знает {LAYOUT_VERSION}:"
|
||||
f" устарел плагин, обнови маркетплейс")
|
||||
conf.set_version(root, LAYOUT_VERSION)
|
||||
print(f"версия раскладки: {was if was is not None else 'не была объявлена'}"
|
||||
f" → {LAYOUT_VERSION} в {CONFIG}")
|
||||
return OK
|
||||
|
||||
|
||||
@@ -671,10 +724,14 @@ def main() -> int:
|
||||
p_check.add_argument("--base", default=None, help="база диффа для сверки миграций")
|
||||
p_check.set_defaults(func=cmd_check)
|
||||
|
||||
p_ver = sub.add_parser("version", help="версия канона скрипта и проекта")
|
||||
p_ver = sub.add_parser("version", help="версия раскладки: скрипта и проекта")
|
||||
p_ver.add_argument("--dir", default=".", help="корень проекта")
|
||||
p_ver.set_defaults(func=cmd_version)
|
||||
|
||||
p_bump = sub.add_parser("bump", help="поднять версию проекта до версии скрипта")
|
||||
p_bump.add_argument("--dir", default=".", help="корень проекта")
|
||||
p_bump.set_defaults(func=cmd_bump)
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return args.func(args)
|
||||
|
||||
Reference in New Issue
Block a user