From 24372a5def0a90569961b281de92ee0c71e06c4c Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:22:42 +0300 Subject: [PATCH] fix: the prompt enhancer no longer needs the CLIP loader module to import; a test forbids module-level imports between modules Co-Authored-By: Claude Sonnet 5.5 --- core/tests/test_module_imports.py | 24 +++++++++++++++++++ .../conditioning/prompt_enhancer/__init__.py | 8 ++++--- modules/conditioning/prompt_enhancer/nodes.py | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/core/tests/test_module_imports.py b/core/tests/test_module_imports.py index bb811273..b03670d1 100644 --- a/core/tests/test_module_imports.py +++ b/core/tests/test_module_imports.py @@ -57,3 +57,27 @@ def test_a_module_reaches_core_only_through_the_shim(path): + ", ".join(f"line {line} ({what})" for line, what in offences) + ". Under ComfyUI there is no top-level `core`. Use `from ..._core import ...`." ) + + +def _other_module_imports(path: Path): + """Imports at MODULE level that reach into a different module (`from ...group.other import`).""" + parts = path.relative_to(MODULES).parts + if len(parts) < 3: + return [] + own = parts[:2] + found = [] + for node in ast.parse(path.read_text()).body: # module level only: a guarded import inside a function is how a module may ask another for help + if isinstance(node, ast.ImportFrom) and node.level >= 3 and node.module: + target = node.module.split(".")[:2] + if len(target) == 2 and tuple(target) != own: + found.append((node.lineno, f"from {'.' * node.level}{node.module} import")) + return found + + +@pytest.mark.parametrize("path", _module_files(), ids=lambda p: str(p.relative_to(MODULES))) +def test_a_module_does_not_depend_on_another_module_at_import(path): + offences = _other_module_imports(path) + assert not offences, ( + f"{path.relative_to(MODULES)} imports another module at line {offences[0][0]} ({offences[0][1]}). " + "If that module fails to import, this one would be lost with it. Import inside the function that needs it, " + "guarded, or ask through PROVIDES / USES.") diff --git a/modules/conditioning/prompt_enhancer/__init__.py b/modules/conditioning/prompt_enhancer/__init__.py index 17b1bac3..35874a36 100644 --- a/modules/conditioning/prompt_enhancer/__init__.py +++ b/modules/conditioning/prompt_enhancer/__init__.py @@ -44,9 +44,11 @@ def routes(table, base, web): @table.get(base + "/encoders") async def _encoders(_req): """Encoder files and families the Enhance panel can offer (read fresh: files come and go).""" - from ...loaders.clip import nodes as clip_nodes - files = clip_nodes.encoder_files() - return web.json_response({"files": files, "families": clip_nodes.clip_types()}) + try: + from ...loaders.clip import nodes as clip_nodes + return web.json_response({"files": clip_nodes.encoder_files(), "families": clip_nodes.clip_types()}) + except Exception as exc: # noqa: BLE001 -- the CLIP loader module is another module; its absence empties the picker, not the panel + return web.json_response({"files": [], "families": [], "why": f"the CLIP loader module is not available: {exc}"}) PROVIDES = {"routes": routes} diff --git a/modules/conditioning/prompt_enhancer/nodes.py b/modules/conditioning/prompt_enhancer/nodes.py index 01ce508a..ed8b61fc 100644 --- a/modules/conditioning/prompt_enhancer/nodes.py +++ b/modules/conditioning/prompt_enhancer/nodes.py @@ -8,7 +8,6 @@ import time from comfy_api.latest import io from ..._core import log -from ...loaders.clip import nodes as clip_nodes from . import enhance as en #: The last runs' rewrites, newest last: what the Composer's readout shows. In @@ -25,6 +24,7 @@ _LOADED = {} # one entry: the last separate encoder, so a second Generate d def _own_encoder(file, family): """The encoder the person picked for the enhancer, loaded the way the pipeline's own loader loads one.""" if _LOADED.get("key") != (file, family): + from ...loaders.clip import nodes as clip_nodes # here, not at the top: the enhancer must load even when the CLIP loader module did not _LOADED.clear() _LOADED.update(key=(file, family), clip=clip_nodes.FunPackCLIPLoader.execute(clip_name1=file, type=family).args[0]) return _LOADED["clip"]