eaf10c7e78
The hermes_state split added 15 root-level sibling modules but [tool.setuptools] py-modules still listed only the old set, so the built wheel/sdist (and the uv2nix sealed venv) failed at 'import hermes_state' with ModuleNotFoundError. hermes_state_holders and hermes_state_registry were already missing on main (registry is imported by gateway/run.py and hermes_state.py). Add an invariant test that parses pyproject and walks every import in the packaged root modules and packages, requiring any import that resolves to a root-level *.py to be declared - so the class of bug is caught by CI rather than by a broken install.
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""Invariant: every root-level module the packaged code imports ships in the wheel.
|
|
|
|
The repo keeps a handful of single-file modules at the repository root
|
|
(``hermes_state.py``, ``run_agent.py``, ``cli.py``, ...). setuptools'
|
|
``packages.find`` never picks those up, so ``[tool.setuptools] py-modules``
|
|
in pyproject.toml has to name each one explicitly. When a root module is
|
|
split into siblings (``hermes_state_*``) and the list is not updated, the
|
|
source tree still works (cwd is on ``sys.path``) but the built wheel/sdist
|
|
and the uv2nix sealed venv raise ``ModuleNotFoundError`` on the very first
|
|
``import hermes_state``.
|
|
|
|
This test derives the required set from the code rather than freezing a
|
|
list: parse pyproject, walk every import (top-level *and* lazy/function-
|
|
scoped) in the packaged root modules and packaged packages, and require
|
|
that any import resolving to a root-level ``*.py`` file is declared.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _pyproject() -> dict:
|
|
return tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
|
|
|
|
def _declared_py_modules() -> set[str]:
|
|
return set(_pyproject()["tool"]["setuptools"]["py-modules"])
|
|
|
|
|
|
def _packaged_package_roots() -> list[Path]:
|
|
include = _pyproject()["tool"]["setuptools"]["packages"]["find"]["include"]
|
|
roots = []
|
|
for pattern in include:
|
|
top = pattern.split(".", 1)[0]
|
|
if "*" in top:
|
|
continue
|
|
path = REPO_ROOT / top
|
|
if path.is_dir() and path not in roots:
|
|
roots.append(path)
|
|
return roots
|
|
|
|
|
|
def _root_module_names() -> set[str]:
|
|
return {p.stem for p in REPO_ROOT.glob("*.py")}
|
|
|
|
|
|
def _imported_top_names(path: Path) -> set[str]:
|
|
"""All top-level names imported anywhere in ``path`` (absolute imports only)."""
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
names: set[str] = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
names.add(alias.name.split(".", 1)[0])
|
|
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
names.add(node.module.split(".", 1)[0])
|
|
return names
|
|
|
|
|
|
def _packaged_source_files() -> list[Path]:
|
|
files = [REPO_ROOT / f"{name}.py" for name in _declared_py_modules()]
|
|
for root in _packaged_package_roots():
|
|
files.extend(root.rglob("*.py"))
|
|
return [f for f in files if f.is_file()]
|
|
|
|
|
|
def test_declared_py_modules_exist_at_repo_root():
|
|
missing = sorted(n for n in _declared_py_modules() if not (REPO_ROOT / f"{n}.py").is_file())
|
|
assert not missing, f"py-modules names files that do not exist at the repo root: {missing}"
|
|
|
|
|
|
def test_every_root_module_imported_by_packaged_code_is_in_py_modules():
|
|
declared = _declared_py_modules()
|
|
root_modules = _root_module_names()
|
|
|
|
# importer -> set of undeclared root modules it imports
|
|
offenders: dict[str, set[str]] = {}
|
|
for src in _packaged_source_files():
|
|
needed = _imported_top_names(src) & root_modules
|
|
undeclared = needed - declared
|
|
if undeclared:
|
|
offenders[str(src.relative_to(REPO_ROOT))] = undeclared
|
|
|
|
assert not offenders, (
|
|
"Root-level modules are imported by packaged code but missing from "
|
|
"[tool.setuptools] py-modules in pyproject.toml. The source tree hides "
|
|
"this (cwd is on sys.path); the built wheel / uv2nix sealed venv will "
|
|
"fail with ModuleNotFoundError. Add them to py-modules:\n"
|
|
+ "\n".join(f" {importer}: {sorted(mods)}" for importer, mods in sorted(offenders.items()))
|
|
)
|