|
| 1 | +"""Guard: the `all` extra must cover every runtime optional-dependency. |
| 2 | +
|
| 3 | +`all` is a hand-curated union of the runtime extras. Nothing in packaging |
| 4 | +enforces that a newly-added runtime extra is also wired into `all`, so it is |
| 5 | +easy to add e.g. a new provider extra and silently leave `pip install |
| 6 | +orb-py[all]` incomplete. This test asserts `all` transitively includes every |
| 7 | +runtime extra except an explicit denylist: |
| 8 | +
|
| 9 | +- test/tooling extras (`ci`, `dev`, `test-*`) are not runtime features; |
| 10 | +- `k8s-legacy` is the deprecated Symphony HostFactory plugin whose heavy legacy |
| 11 | + deps are intentionally kept out of `all`. |
| 12 | +
|
| 13 | +If this fails after adding an extra, either add it to `all` or, if it is |
| 14 | +deliberately excluded, add it to `_DENYLIST` with a reason. |
| 15 | +""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +import tomllib |
| 20 | +from pathlib import Path |
| 21 | + |
| 22 | +_PYPROJECT = Path(__file__).resolve().parents[2] / "pyproject.toml" |
| 23 | + |
| 24 | +# Extras that are intentionally NOT part of `all` (with rationale above). |
| 25 | +_DENYLIST = frozenset( |
| 26 | + { |
| 27 | + "all", # self |
| 28 | + "ci", # test/lint tooling, not runtime |
| 29 | + "dev", # dev tooling, not runtime |
| 30 | + "test-aws", # test-only |
| 31 | + "test-k8s", # test-only |
| 32 | + "k8s-legacy", # deprecated legacy plugin, heavy deps kept out of `all` |
| 33 | + } |
| 34 | +) |
| 35 | + |
| 36 | + |
| 37 | +def _load_extras() -> dict[str, list[str]]: |
| 38 | + data = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) |
| 39 | + return data["project"]["optional-dependencies"] |
| 40 | + |
| 41 | + |
| 42 | +def _resolve(name: str, extras: dict[str, list[str]], seen: set[str] | None = None) -> set[str]: |
| 43 | + """Return the set of concrete package names an extra pulls in, following |
| 44 | + ``orb-py[...]`` self-references transitively.""" |
| 45 | + seen = seen if seen is not None else set() |
| 46 | + if name in seen: |
| 47 | + return set() |
| 48 | + seen.add(name) |
| 49 | + pkgs: set[str] = set() |
| 50 | + for dep in extras.get(name, []): |
| 51 | + if dep.startswith("orb-py["): |
| 52 | + inner = dep[dep.index("[") + 1 : dep.index("]")] |
| 53 | + for ref in inner.split(","): |
| 54 | + pkgs |= _resolve(ref.strip(), extras, seen) |
| 55 | + else: |
| 56 | + # Strip version/marker/sub-extra to get the bare distribution name. |
| 57 | + pkgs.add(dep.split(">")[0].split("=")[0].split("[")[0].split(";")[0].strip()) |
| 58 | + return pkgs |
| 59 | + |
| 60 | + |
| 61 | +def test_all_extra_covers_every_runtime_extra() -> None: |
| 62 | + extras = _load_extras() |
| 63 | + assert "all" in extras, "pyproject is missing the `all` extra" |
| 64 | + |
| 65 | + all_pkgs = _resolve("all", extras) |
| 66 | + runtime_extras = [name for name in extras if name not in _DENYLIST] |
| 67 | + |
| 68 | + missing: dict[str, list[str]] = {} |
| 69 | + for name in runtime_extras: |
| 70 | + gap = _resolve(name, extras) - all_pkgs |
| 71 | + if gap: |
| 72 | + missing[name] = sorted(gap) |
| 73 | + |
| 74 | + assert not missing, ( |
| 75 | + "The `all` extra does not cover these runtime extras — add them to `all` " |
| 76 | + f"(or to _DENYLIST if deliberately excluded): {missing}" |
| 77 | + ) |
| 78 | + |
| 79 | + |
| 80 | +def test_denylisted_extras_still_exist() -> None: |
| 81 | + """Keep the denylist honest: every denylisted name (except `all` itself) |
| 82 | + must be a real extra, so a renamed/removed extra doesn't rot silently.""" |
| 83 | + extras = _load_extras() |
| 84 | + for name in _DENYLIST - {"all"}: |
| 85 | + assert name in extras, f"_DENYLIST names '{name}', which is not an extra anymore" |
| 86 | + |
| 87 | + |
| 88 | +if __name__ == "__main__": |
| 89 | + test_all_extra_covers_every_runtime_extra() |
| 90 | + test_denylisted_extras_still_exist() |
| 91 | + print("ok") |
0 commit comments