feat(scripts): помощник statements_check.py — зависимости пакета и коды словаря имён против перечня (#120)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d4f5453980
commit
ddf6927332
12
Makefile
12
Makefile
@ -1,18 +1,18 @@
|
||||
# Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория.
|
||||
# Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md.
|
||||
VENV := .venv
|
||||
# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должен быть jsonschema)
|
||||
# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должны быть jsonschema и PyYAML)
|
||||
PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3)
|
||||
CHANGE ?= slice-0-skeleton
|
||||
# Текущая версия модели данных — по ней рисуются диаграммы (make diagrams)
|
||||
DBML ?= model/data-model-v5.18.dbml
|
||||
|
||||
.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web sync-web check diagrams
|
||||
.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web test-sync-statements sync-web check diagrams
|
||||
|
||||
# Единственная зависимость скриптов проверки контракта — jsonschema
|
||||
# Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml)
|
||||
venv:
|
||||
uv venv --python 3.11 --allow-existing $(VENV)
|
||||
uv pip install --python $(VENV)/bin/python jsonschema
|
||||
uv pip install --python $(VENV)/bin/python jsonschema pyyaml
|
||||
|
||||
# Примеры contracts/examples/ против трёх схем JSON Schema 2020-12
|
||||
validate-contracts:
|
||||
@ -30,6 +30,10 @@ test-snapshot:
|
||||
test-sync-web:
|
||||
scripts/test_sync_web.sh
|
||||
|
||||
# sync-statements.sh на синтетических репозиториях: помощник проверок, копирование пакета, отказы
|
||||
test-sync-statements:
|
||||
STATEMENTS_PY=$(PY) scripts/test_sync_statements.sh
|
||||
|
||||
# Синхронизация arm-core/web с arm-web@test одной командой (worktree, issue, PR); SYNC_ARGS="--check" или "--dry-run"
|
||||
sync-web:
|
||||
scripts/sync-web.sh $(SYNC_ARGS)
|
||||
|
||||
125
scripts/statements_check.py
Executable file
125
scripts/statements_check.py
Executable file
@ -0,0 +1,125 @@
|
||||
#!/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:]))
|
||||
104
scripts/test_sync_statements.sh
Executable file
104
scripts/test_sync_statements.sh
Executable file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Тест sync-statements.sh на синтетических репозиториях (bare origin + клон), без tea и сети.
|
||||
# Разделы: помощник statements_check.py; дальше — сценарии самого скрипта (дописываются по задачам плана).
|
||||
# Запуск: make test-sync-statements (или напрямую из корня docs; интерпретатор — STATEMENTS_PY, по умолчанию python3).
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
PY=${STATEMENTS_PY:-python3}
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
||||
|
||||
if ! "$PY" -c 'import tomllib, yaml' 2>/dev/null; then
|
||||
echo "нужен Python >= 3.11 с PyYAML: выполните make venv (или задайте STATEMENTS_PY)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fail=0
|
||||
check() { if [ "$1" = 0 ]; then echo "ok: $2"; else echo "FAIL: $2" >&2; fail=1; fi; }
|
||||
|
||||
put() { mkdir -p "$(dirname "$1")"; printf '%s\n' "${2:-x}" > "$1"; }
|
||||
# shellcheck disable=SC2317
|
||||
commit_push() { git -C "$1" add -A; git -C "$1" commit -qm "$2"; git -C "$1" push -q origin "HEAD:$3" 2>/dev/null; git -C "$1" fetch -q origin; }
|
||||
|
||||
# --- помощник statements_check.py ---------------------------------------------------------------------------------
|
||||
H="$WORK/h"; mkdir -p "$H"
|
||||
CHK="$HERE/statements_check.py"
|
||||
# run <rc-ожидаемый> <описание> <аргументы…>: вывод — в $OUT
|
||||
run() {
|
||||
local want=$1 desc=$2 rc=0; shift 2
|
||||
OUT=$("$PY" "$CHK" "$@" 2>&1) || rc=$?
|
||||
check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)"
|
||||
}
|
||||
has() { grep -q -- "$1" <<<"$OUT"; }
|
||||
|
||||
put "$H/pp.toml" $'[project]\ndependencies = ["PyYAML==6.0.3"]'
|
||||
put "$H/r1.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3'
|
||||
run 0 "deps: есть" deps "$H/pp.toml" "$H/r1.txt"
|
||||
check "$([ -z "$OUT" ] && echo 0 || echo 1)" "deps: есть — вывод пустой"
|
||||
put "$H/r2.txt" $'aiohttp==3.14.3\npyyaml==6.0.3'
|
||||
run 0 "deps: регистр" deps "$H/pp.toml" "$H/r2.txt"
|
||||
put "$H/pp_u.toml" $'[project]\ndependencies = ["Py_YAML==6.0.3"]'
|
||||
put "$H/r2u.txt" $'py-yaml==6.0.3'
|
||||
run 0 "deps: подчёркивание и дефис равны" deps "$H/pp_u.toml" "$H/r2u.txt"
|
||||
put "$H/r3.txt" $'aiohttp==3.14.3\n# PyYAML==6.0.3\n-r other.txt'
|
||||
run 20 "deps: нет" deps "$H/pp.toml" "$H/r3.txt"
|
||||
check "$(has PyYAML && echo 0 || echo 1)" "deps: нет — в выводе PyYAML"
|
||||
put "$H/r4.txt" $'PyYAML==6.0.4'
|
||||
run 0 "deps: другая версия" deps "$H/pp.toml" "$H/r4.txt"
|
||||
check "$(has 'примечание:' && has 6.0.4 && echo 0 || echo 1)" "deps: другая версия — примечание с 6.0.4"
|
||||
put "$H/pp_none.toml" $'[tool.ruff]\nline-length = 120'
|
||||
run 0 "deps: без [project]" deps "$H/pp_none.toml" "$H/r1.txt"
|
||||
put "$H/pp_jwt.toml" $'[project]\ndependencies = ["PyJWT==2.8.0"]'
|
||||
put "$H/r5.txt" $'PyJWT[crypto]==2.8.0'
|
||||
run 0 "deps: экстры хоста" deps "$H/pp_jwt.toml" "$H/r5.txt"
|
||||
|
||||
put "$H/orders.yaml" 'version: 1
|
||||
repeats: {}
|
||||
editions:
|
||||
66n:
|
||||
BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}
|
||||
INCOME: {full: ["2110"], simplified: ["2110"]}
|
||||
fsbu4:
|
||||
BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}
|
||||
INCOME: {full: ["2110"], simplified: ["2110"]}'
|
||||
put "$H/lines.yaml" 'editions:
|
||||
- id: 66n
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1110", name: "Основные средства"}
|
||||
- {code: "1100", name: "Итого"}
|
||||
- id: fsbu4
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1110", name: "Основные средства"}'
|
||||
run 0 "lines: все коды есть" lines "$H/lines.yaml" "$H/orders.yaml"
|
||||
put "$H/lines_x.yaml" 'editions:
|
||||
- id: 66n
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1110", name: "а"}
|
||||
- id: fsbu4
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1110", name: "а"}
|
||||
- {code: "1120", name: "б"}'
|
||||
run 20 "lines: код вне перечня" lines "$H/lines_x.yaml" "$H/orders.yaml"
|
||||
check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "lines: код вне перечня — в выводе 1120 и fsbu4"
|
||||
put "$H/orders_s.yaml" 'version: 1
|
||||
editions:
|
||||
fsbu4:
|
||||
BALANCE: {full: ["1110"], simplified: ["1100"]}'
|
||||
put "$H/lines_s.yaml" 'editions:
|
||||
- id: fsbu4
|
||||
forms:
|
||||
BALANCE:
|
||||
- {code: "1100", name: "а"}'
|
||||
run 0 "lines: код только в simplified" lines "$H/lines_s.yaml" "$H/orders_s.yaml"
|
||||
put "$H/bad.yaml" 'editions: ['
|
||||
run 1 "битый YAML" lines "$H/bad.yaml" "$H/orders.yaml"
|
||||
run 2 "аргументы"
|
||||
|
||||
[ "$fail" = 0 ] || exit 1
|
||||
echo "ok: sync-statements.sh — все сценарии пройдены"
|
||||
Loading…
x
Reference in New Issue
Block a user