Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions scripts/translation/check_translation_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,51 @@ def load_cells(nb_path: Path) -> Tuple[List[CellRecord], Optional[str]]:
_SUFFIX_RE = re.compile(r"^(?P<stem>.+)_(?P<lang>" + "|".join(TARGET_LANGS) + r")$")


# Repertoires qui contiennent des COPIES du depot ou des dependances
# vendorisees. Les traverser fait dependre le compte de paires de
# l environnement local : une machine portant deux worktrees rend 6 la ou la
# CI rend 2, et `EXPECTED_PAIR_COUNT` — une egalite — devient non
# maintenable (le dev « corrige » vers son chiffre local et casse la CI).
# Mesure du 2026-08-29 sur cette machine : 6 paires vues, dont 4 venaient de
# `.worktrees/`, aucune n existant sur `main`.
_SKIP_DIRS = frozenset({
".worktrees", # copies du depot (git worktree)
".git",
".venv",
"venv",
"node_modules",
".lake", # paquets Lean vendorises
".ipynb_checkpoints",
})


def _is_scannable(path: Path, repo_root: Path) -> bool:
"""Vrai si ``path`` est dans l arbre source, hors copies et vendorise.

Teste les composants RELATIFS a ``repo_root`` : un depot situe sous un
chemin contenant lui-meme un nom de la liste (par ex. un checkout dans
``/home/x/venv/CoursIA``) ne doit pas devenir invisible en entier.
"""
try:
rel = path.relative_to(repo_root)
except ValueError:
return True
if any(part in _SKIP_DIRS for part in rel.parts):
return False
# La liste de noms ci-dessus ne couvre que les conventions connues. Un
# worktree nomme autrement contamine quand meme le compte : mesure du
# 2026-08-30, un worktree `.wt-parity-split` a la racine faisait rendre
# 2 paires qui n existaient que dans la copie, filtre actif. Ce qui
# caracterise une copie n est pas son nom mais la presence d un `.git`
# (repertoire pour un clone, fichier pour un worktree ou un submodule).
probe = repo_root
for part in rel.parts[:-1]:
probe = probe / part
if (probe / ".git").exists():
return False
return True


def discover_pairs(
repo_root: Path, langs: List[str]
) -> List[Tuple[Path, Path, str, str]]:
Expand Down Expand Up @@ -181,6 +226,8 @@ def discover_pairs(
pairs: List[Tuple[Path, Path, str, str]] = []
seen: Set[Tuple[str, str]] = set()
for nb_path in sorted(repo_root.rglob("*.ipynb")):
if not _is_scannable(nb_path, repo_root):
continue
stem = nb_path.stem
suffix_match = _SUFFIX_RE.match(stem)
if suffix_match and suffix_match.group("lang") in langs:
Expand Down
42 changes: 42 additions & 0 deletions scripts/translation/tests/test_check_translation_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,48 @@ def test_cli_repo_root_missing_returns_two(tmp_path, capsys, monkeypatch):
assert rc == 2


def test_discover_pairs_skips_repo_copies_and_vendored_trees(tmp_path):
"""Une paire dans `.worktrees/` (copie du depot) ne doit PAS etre comptee.

Controle POSITIF d abord : la meme paire, placee dans l arbre source, EST
vue. Sans cette moitie, le test passerait aussi avec un walk qui ne trouve
jamais rien — c est la seule facon de distinguer « exclu » de « aveugle ».
"""
src = tmp_path / "serie" / "N-1.ipynb"
src.parent.mkdir(parents=True)
src.write_text("{}", encoding="utf-8")
src.with_name("N-1_en.ipynb").write_text("{}", encoding="utf-8")

visible = p.discover_pairs(tmp_path, ["en"])
assert len(visible) == 1, "controle positif : une paire de l arbre source doit etre vue"

for skipped in (".worktrees", ".venv", "node_modules", ".lake"):
copy = tmp_path / skipped / "clone" / "serie" / "N-2.ipynb"
copy.parent.mkdir(parents=True)
copy.write_text("{}", encoding="utf-8")
copy.with_name("N-2_en.ipynb").write_text("{}", encoding="utf-8")

after = p.discover_pairs(tmp_path, ["en"])
assert len(after) == 1, (
f"4 paires ajoutees dans des repertoires exclus ne doivent rien changer, "
f"vu {len(after)}"
)


def test_is_scannable_matches_on_relative_parts_only(tmp_path):
"""Un depot dont le CHEMIN PARENT porte un nom exclu reste scannable.

Regression : un test sur le chemin absolu rendrait invisible tout depot
situe sous, par ex., `/home/x/venv/CoursIA`.
"""
root = tmp_path / "venv" / "CoursIA"
(root / "serie").mkdir(parents=True)
inside = root / "serie" / "N.ipynb"
inside.write_text("{}", encoding="utf-8")
assert p._is_scannable(inside, root) is True
assert p._is_scannable(root / ".venv" / "x" / "N.ipynb", root) is False


# ---------------------------------------------------------------------------
# Reference — live state (manual)
# ---------------------------------------------------------------------------
Expand Down
Loading