refactor(nodes): drop importlib from the pytest import fallback

The `except ImportError` branch in __init__.py exists because pytest's
Package collector walks up from tests/ while __init__.py exists, which
makes the repo root a package node and imports this file as a top-level
module (__package__ == ""). Relative imports cannot resolve there.
ComfyUI itself always loads the directory as a package, so the branch is
test-only -- verified by replaying nodes.py:load_custom_node().

`importlib.import_module("py.nodes.prompt").PromptLM` is equivalent to
`from py.nodes.prompt import PromptLM`: nothing here is lazy and nothing
avoids a cycle, so the indirection bought nothing. Removing it also drops
the python_bytecode_manipulation finding (any-code-execute +
obfuscated-code) the registry security scan reports against __init__.py.

Verified: probe during the suite shows the fallback still runs with
__package__ == "" and builds all 21 NODE_CLASS_MAPPINGS entries;
3653 passed, 7 skipped.
This commit is contained in:
Will Miao
2026-10-05 15:18:19 +08:00
parent 0e3897f09b
commit 7e57f12796
+30 -47
View File
@@ -24,7 +24,6 @@ try: # pragma: no cover - import fallback for pytest collection
except ( except (
ImportError ImportError
): # pragma: no cover - allows running under pytest without package install ): # pragma: no cover - allows running under pytest without package install
import importlib
import pathlib import pathlib
import sys import sys
@@ -32,47 +31,33 @@ except (
if str(package_root) not in sys.path: if str(package_root) not in sys.path:
sys.path.append(str(package_root)) sys.path.append(str(package_root))
PromptLM = importlib.import_module("py.nodes.prompt").PromptLM # pytest collects this file as a top-level module: its Package collector walks
TextLM = importlib.import_module("py.nodes.text").TextLM # up from tests/ while __init__.py exists, so the repo root becomes a package
LoraManager = importlib.import_module("py.lora_manager").LoraManager # node and this file is imported without one. Relative imports cannot resolve
LoraLoaderLM = importlib.import_module("py.nodes.lora_loader").LoraLoaderLM # there, so pull the same objects through the top-level "py" package that the
LoraTextLoaderLM = importlib.import_module("py.nodes.lora_loader").LoraTextLoaderLM # sys.path entry above makes importable.
CheckpointLoaderLM = importlib.import_module( from py.lora_manager import LoraManager
"py.nodes.checkpoint_loader" from py.nodes.lora_loader import LoraLoaderLM, LoraTextLoaderLM
).CheckpointLoaderLM from py.nodes.checkpoint_loader import CheckpointLoaderLM
UNETLoaderLM = importlib.import_module("py.nodes.unet_loader").UNETLoaderLM from py.nodes.unet_loader import UNETLoaderLM
TriggerWordToggleLM = importlib.import_module( from py.nodes.trigger_word_toggle import TriggerWordToggleLM
"py.nodes.trigger_word_toggle" from py.nodes.prompt import PromptLM
).TriggerWordToggleLM from py.nodes.text import TextLM
LoraStackerLM = importlib.import_module("py.nodes.lora_stacker").LoraStackerLM from py.nodes.lora_stacker import LoraStackerLM
LoraStackCombinerLM = importlib.import_module( from py.nodes.lora_stack_combiner import LoraStackCombinerLM
"py.nodes.lora_stack_combiner" from py.nodes.save_image import SaveImageLM
).LoraStackCombinerLM from py.nodes.debug_metadata import DebugMetadataLM
SaveImageLM = importlib.import_module("py.nodes.save_image").SaveImageLM from py.nodes.wanvideo_lora_select import WanVideoLoraSelectLM
DebugMetadataLM = importlib.import_module("py.nodes.debug_metadata").DebugMetadataLM from py.nodes.wanvideo_lora_select_from_text import WanVideoLoraTextSelectLM
WanVideoLoraSelectLM = importlib.import_module( from py.nodes.lora_pool import LoraPoolLM
"py.nodes.wanvideo_lora_select" from py.nodes.lora_randomizer import LoraRandomizerLM
).WanVideoLoraSelectLM from py.nodes.lora_cycler import LoraCyclerLM
WanVideoLoraTextSelectLM = importlib.import_module( from py.nodes.lora_info import LoraInfoLM
"py.nodes.wanvideo_lora_select_from_text" from py.nodes.lora_syntax_to_path import LoraSyntaxToPath
).WanVideoLoraTextSelectLM from py.nodes.create_hook_lora import CreateHookLoraLM
LoraPoolLM = importlib.import_module("py.nodes.lora_pool").LoraPoolLM from py.nodes.load_image_metadata import LoadImageMetadataLM
LoraRandomizerLM = importlib.import_module( from py.nodes.metadata_overwrite import MetadataOverwriteLM
"py.nodes.lora_randomizer" from py.metadata_collector import init as init_metadata_collector
).LoraRandomizerLM
LoraCyclerLM = importlib.import_module("py.nodes.lora_cycler").LoraCyclerLM
LoraInfoLM = importlib.import_module("py.nodes.lora_info").LoraInfoLM
LoraSyntaxToPath = importlib.import_module(
"py.nodes.lora_syntax_to_path"
).LoraSyntaxToPath
CreateHookLoraLM = importlib.import_module(
"py.nodes.create_hook_lora"
).CreateHookLoraLM
MetadataOverwriteLM = importlib.import_module(
"py.nodes.metadata_overwrite"
).MetadataOverwriteLM
LoadImageMetadataLM = importlib.import_module("py.nodes.load_image_metadata").LoadImageMetadataLM
init_metadata_collector = importlib.import_module("py.metadata_collector").init
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
PromptLM.NAME: PromptLM, PromptLM.NAME: PromptLM,
@@ -107,12 +92,10 @@ try:
# Auto-build in development, warn only if fails # Auto-build in development, warn only if fails
check_and_build_vue_widgets(auto_build=True, warn_only=True) check_and_build_vue_widgets(auto_build=True, warn_only=True)
except ImportError: except ImportError:
# Fallback for pytest # Fallback for pytest (see the note in the import block above): go through the
import importlib # top-level "py" package, which that block has already put on sys.path.
from py.vue_widget_builder import check_and_build_vue_widgets
check_and_build_vue_widgets = importlib.import_module(
"py.vue_widget_builder"
).check_and_build_vue_widgets
check_and_build_vue_widgets(auto_build=True, warn_only=True) check_and_build_vue_widgets(auto_build=True, warn_only=True)
except Exception as e: except Exception as e:
import logging import logging