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: nebo INFO:).

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 dependencies není objekt, nebo ne všechny klíče v dependencies jsou ř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) nebo None při neuzavřené závorce.

locate_vendor_tuple_assignment(text)

Najde v base.py span 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í je text[assign_start:assign_end]; inner_* je tělo n-tice pro ast.

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.Tuple nebo 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\n před následující definici.

read_tuple_names(base_path)

Načte base.py a 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í:

0 při OK; 1 při chybě nebo po úspěšném --fix se 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())