File size: 4,474 Bytes
0c85e62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | """Guards on which modules ``dev-sync`` ships, so the loader can never point
at a file the install lacks.
The #183 incident (root cause behind #198): a module that ``__init__.py``
imported but ``scripts/sync_to_dev.py``'s ``RUNTIME_PATHS`` never copied raised
``ImportError`` at startup. After the #198 refactor the blast radius of such a
gap depends on *where* the import sits — a per-group node import degrades to
"that node absent", but the context probe (``koolook_versioning``) and the
install guard gate *all* registration. So the real invariant is: every module
``__init__.py`` imports at load must be shipped by ``RUNTIME_PATHS``. This test
derives those imports from ``__init__.py`` itself (rather than a ``k_*.py``
glob, which misses the non-``k_`` gates) so it can't drift.
"""
from __future__ import annotations
import ast
import importlib.util
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
_SCRIPT = REPO_ROOT / "scripts" / "sync_to_dev.py"
_spec = importlib.util.spec_from_file_location("koolook_sync_to_dev_under_test", _SCRIPT)
assert _spec is not None and _spec.loader is not None
sync_to_dev = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(sync_to_dev)
def _init_load_time_module_deps() -> set[str]:
"""Top-level package modules/packages ``__init__.py`` imports at load.
Covers relative ``from .X import ...`` / ``from . import X`` statements and
the ``_merge_node_group(label, ".X")`` node-group imports. Returns the
first path segment of each (e.g. ``forks.radiance_koolook`` -> ``forks``),
the granularity ``RUNTIME_PATHS`` ships at. Absolute imports (the install
guard's out-of-package fallback) are level 0 and intentionally ignored.
"""
tree = ast.parse((REPO_ROOT / "__init__.py").read_text(encoding="utf-8"))
deps: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and (node.level or 0) >= 1:
if node.module: # from .pkg import ...
deps.add(node.module.split(".")[0])
else: # from . import a, b
deps.update(alias.name.split(".")[0] for alias in node.names)
elif (
isinstance(node, ast.Call)
and getattr(node.func, "id", None) == "_merge_node_group"
):
for arg in node.args:
if (
isinstance(arg, ast.Constant)
and isinstance(arg.value, str)
and arg.value.startswith(".")
):
deps.add(arg.value.lstrip(".").split(".")[0])
return deps
def _route_load_time_module_deps() -> set[str]:
tree = ast.parse((REPO_ROOT / "koolook_routes.py").read_text(encoding="utf-8"))
deps: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if (node.level or 0) >= 1 and node.module:
deps.add(node.module.split(".")[0])
elif node.module and node.module.startswith("koolook_"):
deps.add(node.module.split(".")[0])
return deps
def test_init_load_deps_are_shipped_by_runtime_paths():
runtime_paths = set(sync_to_dev.RUNTIME_PATHS)
deps = _init_load_time_module_deps() | _route_load_time_module_deps()
# Sanity: the parse actually found the imports (guards against a refactor
# that renames _merge_node_group and silently makes this test vacuous).
assert "koolook_versioning" in deps and "k_ai_pipeline" in deps, (
f"__init__.py import parse looks broken — only found: {sorted(deps)}"
)
# A single-file module ships as "<dep>.py"; a package (e.g. forks) ships as
# its top-level dir entry.
missing = sorted(
dep
for dep in deps
if f"{dep}.py" not in runtime_paths and dep not in runtime_paths
)
assert not missing, (
"__init__.py imports these at load but dev-sync RUNTIME_PATHS does not "
f"ship them: {missing}. Add them to RUNTIME_PATHS in "
"scripts/sync_to_dev.py so a plain dev-sync can't 404 the loader."
)
def test_dev_sync_ships_manager_visible_package_metadata():
"""The live dev install should report the same pack version as the source.
ComfyUI Manager reads package metadata from ``pyproject.toml``. If dev-sync
skips it, the code can be current while the UI still reports an older
installed version.
"""
assert "pyproject.toml" in sync_to_dev.RUNTIME_PATHS
|