|
| 1 | +"""Guard: a template-born plugin must never ship ``mloda/__init__.py``. |
| 2 | +
|
| 3 | +``mloda`` is a shared PEP 420 namespace package; a distribution that ships |
| 4 | +``mloda/__init__.py`` collapses it and makes mloda unimportable. See |
| 5 | +https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/ |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +# Names a plugin must not use for a package root (``mloda_plugins`` is reserved too). |
| 12 | +RESERVED_NAMESPACE_ROOTS = ("mloda", "mloda_plugins") |
| 13 | + |
| 14 | +# Dirs that never ship; pruned so an installed dep (e.g. mloda under .venv) is ignored. |
| 15 | +EXCLUDED_DIRS = frozenset( |
| 16 | + {".git", ".venv", "venv", ".tox", ".mypy_cache", ".ruff_cache", ".pytest_cache", "__pycache__", "build", "dist"} |
| 17 | +) |
| 18 | + |
| 19 | +REPO_ROOT = Path(__file__).resolve().parent.parent |
| 20 | + |
| 21 | + |
| 22 | +def find_reserved_namespace_violations(root: Path) -> list[str]: |
| 23 | + """Flag a reserved top-level package root, or a reserved ``<name>/__init__.py`` |
| 24 | + anywhere in the tree (so ``src/``-style layouts are covered too).""" |
| 25 | + violations: list[str] = [] |
| 26 | + |
| 27 | + for name in RESERVED_NAMESPACE_ROOTS: |
| 28 | + if (root / name).is_dir(): |
| 29 | + violations.append( |
| 30 | + f"package root directory '{name}/' uses the reserved mloda namespace; " |
| 31 | + "rename it (see README 'Setup Your Plugin')" |
| 32 | + ) |
| 33 | + |
| 34 | + for dirpath, dirnames, _ in os.walk(root): |
| 35 | + dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS and not d.endswith(".egg-info")] |
| 36 | + for name in RESERVED_NAMESPACE_ROOTS: |
| 37 | + init_file = Path(dirpath) / name / "__init__.py" |
| 38 | + if init_file.is_file(): |
| 39 | + rel = init_file.relative_to(root).as_posix() |
| 40 | + violations.append( |
| 41 | + f"'{rel}' would ship in the built distribution and collapse the PEP 420 " |
| 42 | + "'mloda' namespace package, making mloda unimportable for anyone who " |
| 43 | + "installs this plugin" |
| 44 | + ) |
| 45 | + |
| 46 | + return violations |
| 47 | + |
| 48 | + |
| 49 | +def test_repo_has_no_reserved_namespace_root() -> None: |
| 50 | + """Live guard: this repo must not ship into the mloda namespace.""" |
| 51 | + violations = find_reserved_namespace_violations(REPO_ROOT) |
| 52 | + assert not violations, "Reserved namespace violation(s):\n" + "\n".join(violations) |
| 53 | + |
| 54 | + |
| 55 | +def test_detects_reserved_root_with_init(tmp_path: Path) -> None: |
| 56 | + pkg = tmp_path / "mloda" |
| 57 | + pkg.mkdir() |
| 58 | + (pkg / "__init__.py").write_text("") |
| 59 | + |
| 60 | + violations = find_reserved_namespace_violations(tmp_path) |
| 61 | + |
| 62 | + # Root-name check and shipped-__init__.py check both fire. |
| 63 | + assert len(violations) == 2 |
| 64 | + assert any("__init__.py" in v for v in violations) |
| 65 | + |
| 66 | + |
| 67 | +def test_detects_reserved_root_without_init(tmp_path: Path) -> None: |
| 68 | + # Rejected even as a bare namespace dir (no __init__.py). |
| 69 | + (tmp_path / "mloda_plugins").mkdir() |
| 70 | + |
| 71 | + violations = find_reserved_namespace_violations(tmp_path) |
| 72 | + |
| 73 | + assert len(violations) == 1 |
| 74 | + assert "mloda_plugins/" in violations[0] |
| 75 | + |
| 76 | + |
| 77 | +def test_detects_reserved_namespace_in_src_layout(tmp_path: Path) -> None: |
| 78 | + # src/ layout shipping mloda/__init__.py is still caught. |
| 79 | + pkg = tmp_path / "src" / "mloda" |
| 80 | + pkg.mkdir(parents=True) |
| 81 | + (pkg / "__init__.py").write_text("") |
| 82 | + |
| 83 | + violations = find_reserved_namespace_violations(tmp_path) |
| 84 | + |
| 85 | + assert len(violations) == 1 |
| 86 | + assert "src/mloda/__init__.py" in violations[0] |
| 87 | + |
| 88 | + |
| 89 | +def test_ignores_excluded_dirs(tmp_path: Path) -> None: |
| 90 | + # A dep installed under .venv must not trip the guard. |
| 91 | + pkg = tmp_path / ".venv" / "lib" / "mloda" |
| 92 | + pkg.mkdir(parents=True) |
| 93 | + (pkg / "__init__.py").write_text("") |
| 94 | + |
| 95 | + assert find_reserved_namespace_violations(tmp_path) == [] |
| 96 | + |
| 97 | + |
| 98 | +def test_clean_tree_has_no_violations(tmp_path: Path) -> None: |
| 99 | + pkg = tmp_path / "acme" |
| 100 | + pkg.mkdir() |
| 101 | + (pkg / "__init__.py").write_text("") |
| 102 | + |
| 103 | + assert find_reserved_namespace_violations(tmp_path) == [] |
0 commit comments