arm-ka-docs/scripts/statements_check.py

126 lines
5.8 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Помощник sync-statements.sh: проверки пакета arm_statements против хоста (arm-core, tabular-parser, scan-gateway).
Команды:
deps <pyproject.toml> <requirements.txt>
зависимости пакета (`[project].dependencies`), которых нет в requirements хоста;
lines <statement_lines.yaml> <statement_code_orders.yaml>
коды словаря имён строк, которых нет в перечне кодов копии пакета.
Коды возврата: 0 — находок нет, 20 — есть находки, 1 — файл не читается или не той формы, 2 — неверные аргументы.
Спека: openspec statement-normalization (единая нормализация ББ/ОФР, issue #116). Только stdlib и PyYAML.
"""
from __future__ import annotations
import re
import sys
import tomllib
import yaml
_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*")
def normalize_name(name: str) -> str:
"""Имя пакета по PEP 503."""
return re.sub(r"[-_.]+", "-", name).lower()
def requirement_name(line: str) -> str | None:
"""Имя пакета из строки требования; пустая строка, комментарий и опции (`-r`, `--index-url`) дают None."""
line = line.strip()
if not line or line.startswith("#") or line.startswith("-"):
return None
match = _NAME_RE.match(line)
return match.group(0) if match else None
def _pinned_version(line: str) -> str | None:
match = re.search(r"==\s*([^\s;,#]+)", line)
return match.group(1) if match else None
def missing_dependencies(pyproject_text: str, requirements_text: str) -> tuple[list[str], list[str]]:
"""(зависимости пакета, которых нет среди requirements; примечания о различии версий)."""
project = tomllib.loads(pyproject_text).get("project")
deps = project.get("dependencies") if isinstance(project, dict) else None
if not deps:
return [], []
host: dict[str, str] = {}
for line in requirements_text.splitlines():
name = requirement_name(line)
if name:
host[normalize_name(name)] = line.split("#", 1)[0].strip()
missing: list[str] = []
notes: list[str] = []
for dep in deps:
name = requirement_name(dep)
if not name:
continue
host_line = host.get(normalize_name(name))
if host_line is None:
missing.append(dep.strip())
continue
pkg_version, host_version = _pinned_version(dep), _pinned_version(host_line)
if pkg_version and host_version and pkg_version != host_version:
notes.append(f"версии различаются: пакет {dep.strip()}, хост {host_line}")
return missing, notes
def codes_outside_orders(lines_doc: object, orders_doc: object) -> list[str]:
"""Коды словаря имён, которых нет в объединении вариантов full и simplified перечня."""
if not isinstance(lines_doc, dict) or not isinstance(lines_doc.get("editions"), list):
raise ValueError("словарь имён: нет списка editions")
if not isinstance(orders_doc, dict) or not isinstance(orders_doc.get("editions"), dict):
raise ValueError("перечень кодов: нет словаря editions")
problems: list[str] = []
for edition in lines_doc["editions"]:
if not isinstance(edition, dict) or "id" not in edition or not isinstance(edition.get("forms"), dict):
raise ValueError("словарь имён: запись редакции без id или forms")
ed_id = str(edition["id"])
ed_orders = orders_doc["editions"].get(ed_id)
for form, entries in edition["forms"].items():
allowed: set[str] = set()
if isinstance(ed_orders, dict) and isinstance(ed_orders.get(form), dict):
for variant in ("full", "simplified"):
allowed.update(str(c) for c in ed_orders[form].get(variant) or [])
for entry in entries or []:
code = str(entry["code"]) if isinstance(entry, dict) and "code" in entry else None
if code is None:
raise ValueError(f"словарь имён: запись без code в редакции {ed_id}, форма {form}")
if code not in allowed:
problems.append(f"код {code} формы {form} редакции {ed_id} нет в перечне копии пакета")
return problems
def _read(path: str) -> str:
with open(path, encoding="utf-8") as fh:
return fh.read()
def main(argv: list[str]) -> int:
if len(argv) != 3 or argv[0] not in ("deps", "lines"):
print("использование: statements_check.py deps <pyproject.toml> <requirements.txt>\n"
" statements_check.py lines <statement_lines.yaml> <statement_code_orders.yaml>",
file=sys.stderr)
return 2
try:
if argv[0] == "deps":
missing, notes = missing_dependencies(_read(argv[1]), _read(argv[2]))
findings = [f"зависимость пакета {dep} не найдена в {argv[2]}" for dep in missing]
else:
findings = codes_outside_orders(yaml.safe_load(_read(argv[1])), yaml.safe_load(_read(argv[2])))
notes = []
except (OSError, ValueError, yaml.YAMLError, tomllib.TOMLDecodeError) as exc:
print(f"ошибка: {exc}", file=sys.stderr)
return 1
for finding in findings:
print(finding)
for note in notes:
print(f"примечание: {note}")
return 20 if findings else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))