""" Root-level pytest configuration for standalone testing outside ComfyUI. Installs ComfyUI stubs and a custom import hook so that the package's __init__.py (which pytest's Package.setup() always imports) loads cleanly without a live ComfyUI runtime. """ import sys import types import os import importlib import importlib.abc import importlib.machinery import importlib.util # Run immediately at conftest import time (before pytest_configure) _NODE_ROOT = os.path.dirname(os.path.abspath(__file__)) _PKG_NAME = os.path.basename(_NODE_ROOT) # "ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite" def _install_stubs(): """Install all stubs needed to import the package __init__.py cleanly.""" # Try real torch first — tests that use torch tensors (crop/paste, integration) # need it. If real torch isn't installed, the stub-if-missing logic below leaves # the stub in place and those tests will skip or fail with a clear error. try: import torch # noqa: F401 except ImportError: pass # 1. External deps for name in [ "torch", "torchvision", "torchaudio", "folder_paths", "comfy", "comfy.utils", "comfy.sd", "comfy.model_management", "nodes", "aiohttp", ]: if name not in sys.modules: sys.modules[name] = types.ModuleType(name) # aiohttp.web if "aiohttp.web" not in sys.modules: web = types.ModuleType("aiohttp.web") web.Response = type("Response", (), {"__init__": lambda s, *a, **kw: None}) web.json_response = lambda *a, **kw: None web.RouteTableDef = type("RouteTableDef", (), { "get": lambda s, *a, **kw: (lambda f: f), "post": lambda s, *a, **kw: (lambda f: f), "delete": lambda s, *a, **kw: (lambda f: f), "put": lambda s, *a, **kw: (lambda f: f), }) sys.modules["aiohttp.web"] = web sys.modules["aiohttp"].web = web # type: ignore[attr-defined] # folder_paths.get_output_directory fp = sys.modules["folder_paths"] if not hasattr(fp, "get_output_directory"): fp.get_output_directory = lambda: os.path.join(_NODE_ROOT, "_test_output") # type: ignore if not hasattr(fp, "get_full_path"): # Stub returns a synthetic path for any (folder, name) lookup so tests # that don't care about file existence (e.g. mocked loaders) don't trip # the "file not found -> fall back" guard in florence2_hires. Tests that # specifically want to exercise the missing-file path can monkeypatch. fp.get_full_path = lambda folder, name: f"/{folder}/{name}" if name else None # type: ignore # server.PromptServer if "server" not in sys.modules: sys.modules["server"] = types.ModuleType("server") srv = sys.modules["server"] if not hasattr(srv, "PromptServer"): _noop_deco = staticmethod(lambda *a, **kw: (lambda f: f)) _routes = type("_Routes", (), { "get": _noop_deco, "post": _noop_deco, "delete": _noop_deco, "put": _noop_deco, })() _ps_instance = type("_PS", (), {"routes": _routes})() srv.PromptServer = type("PromptServer", (), {"instance": _ps_instance}) # type: ignore # 2. Sub-module stubs (for relative imports in __init__.py) # NOTE: config_builder_node is intentionally excluded from _subs so that # the real module (with UltimateConfigBuilder.state_to_configs_json etc.) # is loaded below via importlib. The stubs here are only for __init__.py's # other relative imports that don't need to be tested directly. _subs = { "sampler_node": {"SamplerGridTester": type("SamplerGridTester", (), {})}, "dashboard_node": {"SamplerConfigDashboardViewer": type("SamplerConfigDashboardViewer", (), {})}, "html_generator": {"get_html_template": lambda *a, **kw: ""}, "json_text_node": {"SmartJSONTextNode": type("SmartJSONTextNode", (), {})}, "metadata_packer": {"pack_metadata_into_image": lambda *a, **kw: None}, "directory_scanner": { "scan_directory_for_images": lambda *a, **kw: ([], {}), "MEDIA_EXTENSIONS": set(), }, "distribution_routes": {}, "upscale_runner": { "start_upscale_job": lambda *a, **kw: (None, None), "get_upscale_status": lambda *a, **kw: {}, "cancel_upscale_job": lambda *a, **kw: False, }, } for bare, attrs in _subs.items(): fq = f"{_PKG_NAME}.{bare}" if bare not in sys.modules: stub = types.ModuleType(bare) sys.modules[bare] = stub if fq not in sys.modules: sys.modules[fq] = sys.modules[bare] for k, v in attrs.items(): setattr(sys.modules[bare], k, v) setattr(sys.modules[fq], k, v) # 3. Pre-register the package itself if _PKG_NAME not in sys.modules: pkg_stub = types.ModuleType(_PKG_NAME) pkg_stub.__path__ = [_NODE_ROOT] pkg_stub.__package__ = _PKG_NAME sys.modules[_PKG_NAME] = pkg_stub # 4. Ensure node root is on sys.path if _NODE_ROOT not in sys.path: sys.path.insert(0, _NODE_ROOT) # 5. Load the real config_builder_node module (with package context so that # relative imports like `from .network_utils import ...` resolve correctly). # This must happen after all stubs are registered (steps 1-4) so that the # module-level decorator calls (server.PromptServer.instance.routes.post/get) # and relative imports in config_builder_node.py succeed. if "config_builder_node" not in sys.modules: _cb_path = os.path.join(_NODE_ROOT, "config_builder_node.py") _cb_fq = f"{_PKG_NAME}.config_builder_node" _cb_spec = importlib.util.spec_from_file_location( _cb_fq, _cb_path, submodule_search_locations=[] ) _cb_spec.submodule_search_locations = None _cb_mod = importlib.util.module_from_spec(_cb_spec) _cb_mod.__package__ = _PKG_NAME sys.modules[_cb_fq] = _cb_mod sys.modules["config_builder_node"] = _cb_mod _cb_spec.loader.exec_module(_cb_mod) # Also wire as package attribute so __init__.py's relative import finds it if _PKG_NAME in sys.modules: setattr(sys.modules[_PKG_NAME], "config_builder_node", _cb_mod) class _PackageInitLoader(importlib.abc.Loader): """ Custom loader for the package __init__.py. Sets __package__ correctly before executing the module so that relative imports (from .sampler_node import X) resolve via sys.modules. """ def __init__(self, path): self._path = path def create_module(self, spec): return None # use default semantics def exec_module(self, module): module.__file__ = self._path module.__package__ = _PKG_NAME module.__name__ = _PKG_NAME module.__path__ = [_NODE_ROOT] # makes relative imports resolve parent pkg # Point sys.modules to this module object so relative imports find it sys.modules[_PKG_NAME] = module with open(self._path, "rb") as fh: src = fh.read() code = compile(src, self._path, "exec") exec(code, module.__dict__) # noqa: S102 – intentional dynamic exec for stub loading class _PackageInitFinder(importlib.abc.MetaPathFinder): """ Intercepts any attempt to import the package __init__.py and routes it through _PackageInitLoader so relative imports get the correct __package__. """ _INIT_PATH = os.path.join(_NODE_ROOT, "__init__.py") def find_spec(self, fullname, path, target=None): # In importlib mode, pytest imports __init__.py as module name "__init__" # (module_name_from_path returns "__init__" for package root __init__.py). # We intercept that import and also the top-level package name import. if fullname in ("__init__", _PKG_NAME): if target is not None: # target is the path pytest wants to load; check it's our __init__.py try: if os.path.normcase(str(target.__spec__.origin)) != os.path.normcase(self._INIT_PATH): return None except Exception: pass spec = importlib.machinery.ModuleSpec( fullname, _PackageInitLoader(self._INIT_PATH), origin=self._INIT_PATH, ) spec.submodule_search_locations = [_NODE_ROOT] return spec return None # Install stubs + hook immediately so they're ready before pytest does anything _install_stubs() sys.meta_path.insert(0, _PackageInitFinder()) def pytest_configure(config): """Re-ensure stubs are in place after config initialises.""" _install_stubs()