Skript check_npm_vendor_package_names.py
Automaticky generovaná dokumentace skriptu scripts/check_npm_vendor_package_names.py.
Přehled modulu
Kontrola souladu záložní n-tice _NPM_VENDOR_PACKAGE_NAMES v Django base.py s klíči
dependencies v kořenovém package.json.
Při --fix přepíše n-tici tak, aby přesně odpovídala (abecedně seřazené) klíčům z dependencies.
Výstup pro uživatele a CI: řádky na stderr s prefixem [npm-vendor-names] (grep v GitHub Actions).
Funkce
- log_msg(message)
Vypíše jeden řádek na stderr s prefixem pro přehled v CI a PR komentářích.
- Parametry:
message – Text bez prefixu (typicky
ERROR:,FIX:neboINFO:).
- repo_root()
Vrátí kořen repozitáře (nadřazený adresář
scripts/).- Vrací:
Cesta ke kořeni.
- load_dependency_keys(root)
Načte množinu jmen přímých závislostí z kořenového
package.json.- Parametry:
root – Kořen repozitáře.
- Vrací:
Klíče sekce
dependencies.- Vyvolá:
FileNotFoundError – Vyvolá se, pokud v kořeni chybí soubor
package.json.json.JSONDecodeError – Vyvolá se při neplatném JSON v souboru.
ValueError – Vyvolá se, pokud kořen JSON není objekt, pole
dependenciesnení objekt, nebo ne všechny klíče vdependenciesjsou řetězce.
- _tuple_inner_span(text, open_paren_index)
Najde indexy obsahu n-tice:
text[inner_start:inner_end]je tělo mezi závorkami (bez(a)).Respektuje řetězce a escape sekvence; mimo řetězec ignoruje obsah po
#do konce řádku.- Parametry:
text – Celý obsah souboru.
open_paren_index – Index otevírací závorky
(přiřazení n-tice.
- Vrací:
(inner_start, inner_end)neboNonepři neuzavřené závorce.
- locate_vendor_tuple_assignment(text)
Najde v
base.pyspan přiřazení_NPM_VENDOR_PACKAGE_NAMES = (...).- Parametry:
text – Obsah
base.py.- Vrací:
(assign_start, assign_end, inner_start, inner_end)— celý blok k nahrazení jetext[assign_start:assign_end];inner_*je tělo n-tice proast.
- parse_tuple_string_literals(inner)
Parsuje tělo n-tice a vrátí seznam řetězcových literálů v pořadí výskytu.
- Parametry:
inner – Obsah mezi
(a)včetně komentářů a řádkových zalomení.- Vrací:
Seznam hodnot řetězců.
- Vyvolá:
ValueError – při neplatné syntaxi nebo nečistě řetězcových prvcích.
- _elts_as_str_list(value)
Vrátí řetězce z
ast.Tuplenebo jednoprvkové n-tice.
- build_tuple_assignment(sorted_names)
Sestaví text přiřazení
_NPM_VENDOR_PACKAGE_NAMES = (...)ve stylu Black (odsazení 4 mezery).- Parametry:
sorted_names – Již seřazené názvy balíčků.
- Vrací:
Text včetně koncového
\n\npřed následující definici.
- read_tuple_names(base_path)
Načte
base.pya extrahuje množinu jmen z_NPM_VENDOR_PACKAGE_NAMES.- Parametry:
base_path – Cesta k
base.py.- Vrací:
(celý_text, množina_jmen).
- main(argv)
Vstupní bod CLI.
- Parametry:
argv – Argumenty bez
sys.argv[0];None=sys.argv[1:].- Vrací:
0při OK;1při chybě nebo po úspěšném--fixse změnou souboru (pre-commit).
Zdrojový kód
1#!/usr/bin/env python3
2"""
3Kontrola souladu záložní n-tice ``_NPM_VENDOR_PACKAGE_NAMES`` v Django ``base.py`` s klíči
4``dependencies`` v kořenovém ``package.json``.
5
6Při ``--fix`` přepíše n-tici tak, aby přesně odpovídala (abecedně seřazené) klíčům z ``dependencies``.
7
8Výstup pro uživatele a CI: řádky na stderr s prefixem ``[npm-vendor-names]`` (grep v GitHub Actions).
9"""
10
11from __future__ import annotations
12
13import argparse
14import ast
15import json
16import re
17import sys
18from pathlib import Path
19from typing import List, Optional, Set, Tuple
20
21CONST_NAME = "_NPM_VENDOR_PACKAGE_NAMES"
22ASSIGN_PREFIX_RE = re.compile(rf"^({CONST_NAME}\s*=\s*\()", re.MULTILINE)
23LOG_PREFIX = "[npm-vendor-names]"
24
25# Relativní cesty od kořene repozitáře (pre-commit spouští z rootu).
26PACKAGE_JSON = "package.json"
27BASE_PY = "webclient/webclient/settings/base.py"
28
29
30def log_msg(message: str) -> None:
31 """
32 Vypíše jeden řádek na stderr s prefixem pro přehled v CI a PR komentářích.
33
34 :param message: Text bez prefixu (typicky ``ERROR:``, ``FIX:`` nebo ``INFO:``).
35 """
36 print(f"{LOG_PREFIX} {message}", file=sys.stderr)
37
38
39def repo_root() -> Path:
40 """
41 Vrátí kořen repozitáře (nadřazený adresář ``scripts/``).
42
43 :return: Cesta ke kořeni.
44 """
45 return Path(__file__).resolve().parent.parent
46
47
48def load_dependency_keys(root: Path) -> Set[str]:
49 """
50 Načte množinu jmen přímých závislostí z kořenového ``package.json``.
51
52 :param root: Kořen repozitáře.
53 :return: Klíče sekce ``dependencies``.
54 :raises FileNotFoundError: Vyvolá se, pokud v kořeni chybí soubor ``package.json``.
55 :raises json.JSONDecodeError: Vyvolá se při neplatném JSON v souboru.
56 :raises ValueError: Vyvolá se, pokud kořen JSON není objekt, pole ``dependencies`` není objekt,
57 nebo ne všechny klíče v ``dependencies`` jsou řetězce.
58 """
59 path = root / PACKAGE_JSON
60 if not path.is_file():
61 raise FileNotFoundError(f"Chybí soubor {path}")
62 with open(path, encoding="utf-8") as f:
63 data = json.load(f)
64 if not isinstance(data, dict):
65 raise ValueError(f"{path}: očekáván JSON objekt na kořeni")
66 deps = data.get("dependencies")
67 if deps is None:
68 return set()
69 if not isinstance(deps, dict):
70 raise ValueError(f"{path}: pole 'dependencies' musí být objekt")
71 keys = [k for k in deps if isinstance(k, str)]
72 if len(keys) != len(deps):
73 raise ValueError(f"{path}: všechny klíče v 'dependencies' musí být řetězce")
74 return set(keys)
75
76
77def _tuple_inner_span(text: str, open_paren_index: int) -> Optional[Tuple[int, int]]:
78 """
79 Najde indexy obsahu n-tice: ``text[inner_start:inner_end]`` je tělo mezi závorkami (bez ``(`` a ``)``).
80
81 Respektuje řetězce a escape sekvence; mimo řetězec ignoruje obsah po ``#`` do konce řádku.
82
83 :param text: Celý obsah souboru.
84 :param open_paren_index: Index otevírací závorky ``(`` přiřazení n-tice.
85 :return: ``(inner_start, inner_end)`` nebo ``None`` při neuzavřené závorce.
86 """
87 i = open_paren_index + 1
88 depth = 1
89 in_str = False
90 str_delim: Optional[str] = None
91 escape = False
92 n = len(text)
93 while i < n and depth > 0:
94 c = text[i]
95 if escape:
96 escape = False
97 i += 1
98 continue
99 if in_str:
100 if c == "\\":
101 escape = True
102 elif c == str_delim:
103 in_str = False
104 str_delim = None
105 i += 1
106 continue
107 if c in "\"'":
108 in_str = True
109 str_delim = c
110 i += 1
111 continue
112 if c == "#":
113 while i < n and text[i] != "\n":
114 i += 1
115 continue
116 if c == "(":
117 depth += 1
118 elif c == ")":
119 depth -= 1
120 if depth == 0:
121 return open_paren_index + 1, i
122 i += 1
123 return None
124
125
126def locate_vendor_tuple_assignment(text: str) -> Optional[Tuple[int, int, int, int]]:
127 """
128 Najde v ``base.py`` span přiřazení ``_NPM_VENDOR_PACKAGE_NAMES = (...)``.
129
130 :param text: Obsah ``base.py``.
131 :return: ``(assign_start, assign_end, inner_start, inner_end)`` — celý blok k nahrazení
132 je ``text[assign_start:assign_end]``; ``inner_*`` je tělo n-tice pro ``ast``.
133 """
134 m = ASSIGN_PREFIX_RE.search(text)
135 if not m:
136 return None
137 assign_start = m.start(1)
138 open_paren = m.end(1) - 1
139 inner = _tuple_inner_span(text, open_paren)
140 if inner is None:
141 return None
142 inner_start, inner_end = inner
143 close_paren = inner_end
144 assign_end = close_paren + 1
145 return assign_start, assign_end, inner_start, inner_end
146
147
148def parse_tuple_string_literals(inner: str) -> List[str]:
149 """
150 Parsuje tělo n-tice a vrátí seznam řetězcových literálů v pořadí výskytu.
151
152 :param inner: Obsah mezi ``(`` a ``)`` včetně komentářů a řádkových zalomení.
153 :return: Seznam hodnot řetězců.
154 :raises ValueError: při neplatné syntaxi nebo nečistě řetězcových prvcích.
155 """
156 snippet = f"{CONST_NAME} = ({inner})"
157 try:
158 tree = ast.parse(snippet)
159 except SyntaxError as e:
160 raise ValueError(f"Neplatná syntaxe n-tice {CONST_NAME}: {e}") from e
161 for node in tree.body:
162 if not isinstance(node, ast.Assign) or len(node.targets) != 1:
163 continue
164 t = node.targets[0]
165 if isinstance(t, ast.Name) and t.id == CONST_NAME:
166 return _elts_as_str_list(node.value)
167 raise ValueError(f"V úryvku chybí přiřazení {CONST_NAME}")
168
169
170def _elts_as_str_list(value: ast.AST) -> List[str]:
171 """Vrátí řetězce z ``ast.Tuple`` nebo jednoprvkové n-tice."""
172 if isinstance(value, ast.Tuple):
173 out: List[str] = []
174 for elt in value.elts:
175 if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
176 out.append(elt.value)
177 else:
178 raise ValueError(f"Očekávány pouze řetězcové literály v {CONST_NAME}, dostal {type(elt).__name__}")
179 return out
180 if isinstance(value, ast.Constant) and isinstance(value.value, str):
181 return [value.value]
182 raise ValueError(f"Očekána n-tice řetězců pro {CONST_NAME}, dostal {type(value).__name__}")
183
184
185def build_tuple_assignment(sorted_names: List[str]) -> str:
186 """
187 Sestaví text přiřazení ``_NPM_VENDOR_PACKAGE_NAMES = (...)`` ve stylu Black (odsazení 4 mezery).
188
189 :param sorted_names: Již seřazené názvy balíčků.
190 :return: Text včetně koncového ``\\n\\n`` před následující definici.
191 """
192 if not sorted_names:
193 return f"{CONST_NAME} = ()\n\n"
194 lines = [f' "{name}",' for name in sorted_names]
195 body = "\n".join(lines)
196 return f"{CONST_NAME} = (\n{body}\n)\n\n"
197
198
199def read_tuple_names(base_path: Path) -> Tuple[str, Set[str]]:
200 """
201 Načte ``base.py`` a extrahuje množinu jmen z ``_NPM_VENDOR_PACKAGE_NAMES``.
202
203 :param base_path: Cesta k ``base.py``.
204 :return: ``(celý_text, množina_jmen)``.
205 """
206 text = base_path.read_text(encoding="utf-8")
207 loc = locate_vendor_tuple_assignment(text)
208 if loc is None:
209 raise ValueError(f"V {base_path} se nepodařilo najít {CONST_NAME} = (...)")
210 _a, _b, inner_start, inner_end = loc
211 inner = text[inner_start:inner_end]
212 names = parse_tuple_string_literals(inner)
213 return text, set(names)
214
215
216def main(argv: Optional[List[str]] = None) -> int:
217 """
218 Vstupní bod CLI.
219
220 :param argv: Argumenty bez ``sys.argv[0]``; ``None`` = ``sys.argv[1:]``.
221 :return: ``0`` při OK; ``1`` při chybě nebo po úspěšném ``--fix`` se změnou souboru (pre-commit).
222 """
223 parser = argparse.ArgumentParser(
224 description="Kontrola souladu _NPM_VENDOR_PACKAGE_NAMES s package.json dependencies.",
225 )
226 parser.add_argument(
227 "--fix",
228 action="store_true",
229 help="Přepsat n-tici v base.py podle package.json (abecedně).",
230 )
231 args = parser.parse_args(argv)
232
233 root = repo_root()
234 base_path = root / BASE_PY
235 if not base_path.is_file():
236 log_msg(f"ERROR: chybí soubor {base_path}")
237 return 1
238
239 try:
240 dep_keys = load_dependency_keys(root)
241 except (OSError, ValueError, json.JSONDecodeError) as e:
242 log_msg(f"ERROR: čtení package.json: {e}")
243 return 1
244
245 try:
246 text, tuple_names = read_tuple_names(base_path)
247 except (OSError, ValueError) as e:
248 log_msg(f"ERROR: čtení {CONST_NAME}: {e}")
249 return 1
250
251 missing = sorted(dep_keys - tuple_names)
252 extra = sorted(tuple_names - dep_keys)
253
254 if not missing and not extra:
255 return 0
256
257 if not args.fix:
258 if missing:
259 log_msg(
260 f"ERROR: v {CONST_NAME} chybí oproti package.json dependencies: {', '.join(missing)}",
261 )
262 if extra:
263 log_msg(
264 f"ERROR: v {CONST_NAME} jsou navíc oproti package.json dependencies: {', '.join(extra)}",
265 )
266 log_msg("INFO: spusť s --fix pro úpravu base.py (nebo doplň n-tici ručně).")
267 return 1
268
269 loc = locate_vendor_tuple_assignment(text)
270 if loc is None:
271 log_msg(f"ERROR: interní chyba — nenalezen span pro {CONST_NAME}")
272 return 1
273 assign_start, assign_end, _is, _ie = loc
274 new_assign = build_tuple_assignment(sorted(dep_keys))
275 new_text = text[:assign_start] + new_assign + text[assign_end:]
276 base_path.write_text(new_text, encoding="utf-8", newline="\n")
277 log_msg(
278 f"FIX: upraven {base_path} — n-tice sjednocena s package.json ({len(dep_keys)} balíčků); "
279 "znovu přidej do commitu.",
280 )
281 return 1
282
283
284if __name__ == "__main__":
285 sys.exit(main())