Files
wildminder-ComfyUI-KEEP/tests/_headless.py
T
WildAi 56f8f0b4b1 plan(P1.4): add the headless bootstrap helper
tests/_headless.py resolves the ComfyUI tree from COMFYUI_ROOT and the weight
directory from KEEP_MODEL_DIR (no hardcoded paths, actionable errors), loads
extra_model_paths.yaml through utils.extra_config like the server does,
registers a package under an importable alias with __path__ set and can stub
only the ui preview helpers.
tests/test_headless_bootstrap.py proves the helper on a dummy package: alias
resolution, submodule import, the missing-environment-variable errors and the
absence of machine-specific paths.
2026-10-23 15:51:21 +03:00

199 lines
7.2 KiB
Python

"""Headless bootstrap helper for ComfyUI-KEEP (P1.4).
"Headless" means: drive the real node code without a ComfyUI server. That
needs four things, and this helper provides them:
1. **the ComfyUI source tree on ``sys.path``** - resolved from the
``COMFYUI_ROOT`` environment variable (or an explicit argument). There are
**no hardcoded paths** in this module: a helper that only works on the
machine of its author is a test that passes for the wrong reason.
2. **the extra model paths** - ``folder_paths`` does *not* read
``extra_model_paths.yaml`` at import time; the server does it in
``main.py``. :func:`load_extra_model_paths` does the same, so the test
sees the same model tree as the server.
3. **the package under an importable alias** - a custom-node folder is called
``ComfyUI-KEEP``, which is not a valid Python identifier; the package is
registered under an alias with ``__path__`` and
``submodule_search_locations`` set.
4. **optional preview stubs** - the ``ui.*`` preview helpers read the server's
execution context and raise outside a server. Only the *preview* is
stubbed; generation, patching and offload stay real.
Every failure is an :class:`HeadlessBootstrapError` whose message names the
environment variable to set.
"""
from __future__ import annotations
import importlib.util
import os
import re
import sys
import types
from pathlib import Path
#: Environment variable pointing at the ComfyUI source tree.
COMFYUI_ROOT_ENV = "COMFYUI_ROOT"
#: Environment variable pointing at the directory with the model weights.
MODEL_DIR_ENV = "KEEP_MODEL_DIR"
#: The preview helpers that are replaced by :func:`stub_ui`.
PREVIEW_HELPERS = ("PreviewImage", "PreviewAudio")
class HeadlessBootstrapError(RuntimeError):
"""Raised when the headless environment cannot be prepared."""
def _env_path(name: str) -> str:
"""The value of an environment variable (empty string when unset/blank)."""
return (os.environ.get(name) or "").strip()
def resolve_comfyui_root(explicit: str | os.PathLike[str] | None = None) -> Path:
"""The ComfyUI source tree.
Resolution order: the explicit argument, then the ``COMFYUI_ROOT``
environment variable. There is no fallback default - a wrong default
would make the tests pass against the wrong tree.
"""
if explicit:
root = Path(explicit).expanduser()
if not (root / "folder_paths.py").is_file():
raise HeadlessBootstrapError(
f"{root} is not a ComfyUI source tree (folder_paths.py is missing). "
f"Point {COMFYUI_ROOT_ENV} at a ComfyUI checkout."
)
return root.resolve()
value = _env_path(COMFYUI_ROOT_ENV)
if not value:
raise HeadlessBootstrapError(
f"{COMFYUI_ROOT_ENV} is not set. Set {COMFYUI_ROOT_ENV}=<path to the "
f"ComfyUI checkout> to run the headless tests."
)
root = Path(value).expanduser()
if not (root / "folder_paths.py").is_file():
raise HeadlessBootstrapError(
f"{COMFYUI_ROOT_ENV}={value!r} is not a ComfyUI source tree "
f"(folder_paths.py is missing)."
)
return root.resolve()
def resolve_model_dir(explicit: str | os.PathLike[str] | None = None) -> Path:
"""The directory that holds the model weights.
Resolution order: the explicit argument, then the ``KEEP_MODEL_DIR``
environment variable. The directory does not have to exist yet - the
caller decides whether a missing directory is fatal.
"""
if explicit:
return Path(explicit).expanduser().resolve()
value = _env_path(MODEL_DIR_ENV)
if not value:
raise HeadlessBootstrapError(
f"{MODEL_DIR_ENV} is not set. Set {MODEL_DIR_ENV}=<absolute path to the "
f"directory with the KEEP weights> to run the model tests."
)
return Path(value).expanduser().resolve()
def alias_for(package_dir: str | os.PathLike[str]) -> str:
"""A valid Python alias for a package directory (``ComfyUI-KEEP`` -> ``ComfyUI_KEEP``)."""
return re.sub(r"\W", "_", Path(package_dir).name)
def register_package(
package_dir: str | os.PathLike[str],
alias: str | None = None,
execute: bool = False,
) -> types.ModuleType:
"""Register a package directory under an importable alias.
The module gets ``__path__`` and ``submodule_search_locations``, so its
subpackages resolve inside the directory. With ``execute=False`` the
``__init__.py`` is *not* run (the package can then be imported without
ComfyUI); with ``execute=True`` the real entry point runs.
"""
package_dir = Path(package_dir).expanduser().resolve()
init_file = package_dir / "__init__.py"
if not init_file.is_file():
raise HeadlessBootstrapError(
f"{package_dir} is not a package: {init_file} does not exist."
)
alias = alias or alias_for(package_dir)
spec = importlib.util.spec_from_file_location(
alias,
str(init_file),
submodule_search_locations=[str(package_dir)],
)
if spec is None or spec.loader is None:
raise HeadlessBootstrapError(f"cannot build an import spec for {init_file}")
module = importlib.util.module_from_spec(spec)
module.__path__ = [str(package_dir)] # type: ignore[attr-defined]
sys.modules[alias] = module
if execute:
spec.loader.exec_module(module)
return module
def load_extra_model_paths(
comfyui_root: str | os.PathLike[str],
config_path: str | os.PathLike[str] | None = None,
) -> bool:
"""Load ComfyUI's ``extra_model_paths.yaml`` the way the server does.
``folder_paths`` resolves only the default ``models/`` tree on its own;
the server applies the extra paths in ``main.py`` through
``utils.extra_config.load_extra_path_config``. Returns ``True`` when a
configuration was loaded, ``False`` when there is none.
"""
root = Path(comfyui_root).expanduser().resolve()
config = Path(config_path).expanduser() if config_path else root / "extra_model_paths.yaml"
if not config.is_file():
return False
if str(root) not in sys.path:
sys.path.insert(0, str(root))
# The real module is ``utils.extra_config`` (top-level ``utils``), not
# ``comfy.utils.extra_config`` - ``comfy/utils.py`` has no submodules.
import utils.extra_config as extra_config # noqa: PLC0415 - needs the root on sys.path
extra_config.load_extra_path_config(str(config))
return True
def stub_ui(ui_module: types.ModuleType | None = None) -> list[str]:
"""Replace the preview helpers of the ``ui`` module with no-ops.
Only the preview is stubbed - everything else (generation, patching,
offload) stays real. Returns the names that were stubbed.
"""
if ui_module is None:
from comfy_api.latest import ui as ui_module # noqa: PLC0415 - optional dependency
stubbed: list[str] = []
for name in PREVIEW_HELPERS:
if not hasattr(ui_module, name):
continue
setattr(ui_module, name, _make_noop(name))
stubbed.append(name)
return stubbed
def _make_noop(name: str):
"""A no-op replacement for a preview helper."""
def _noop(*args, **kwargs):
return None
_noop.__name__ = name
_noop.__qualname__ = name
return _noop