From d40f2fa37c1f6f32c24152e337b3f378b998bf5d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:57:45 +1000 Subject: [PATCH] feat(app): improved custom load loading ordering Previously, custom node loading occurred _during module imports_. A consequence of this is that when a custom node import fails (e.g. its type clobbers an existing node), the app fails to start up. In fact, any time we import basically anything from the app, we trigger custom node imports! Not good. This logic is now in its own function, called as the API app starts up. If a custom node load fails for any reason, it no longer prevents the app from starting up. One other bonus we get from this is that we can now ensure custom nodes are loaded _after_ core nodes. Any clobbering that may occur while loading custom nodes is now guaranteed to be a custom node clobbering a core node's type - and not the other way round. --- invokeai/app/api_app.py | 6 +++ invokeai/app/invocations/__init__.py | 28 ------------- invokeai/app/invocations/load_custom_nodes.py | 40 +++++++++++++++++++ 3 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 invokeai/app/invocations/load_custom_nodes.py diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index dbd861b2d3..9cfebbdca1 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -36,6 +36,7 @@ from invokeai.app.api.routers import ( workflows, ) from invokeai.app.api.sockets import SocketIO +from invokeai.app.invocations.load_custom_nodes import load_custom_nodes from invokeai.app.services.config.config_default import get_config from invokeai.app.util.custom_openapi import get_openapi_func from invokeai.backend.util.devices import TorchDevice @@ -63,6 +64,11 @@ loop = asyncio.new_event_loop() # the correct port when the server starts in the lifespan handler. port = app_config.port +# Load custom nodes. This must be done after importing the Graph class, which itself imports all modules from the +# invocations module. The ordering here is implicit, but important - we want to load custom nodes after all the +# core nodes have been imported so that we can catch when a custom node clobbers a core node. +load_custom_nodes(custom_nodes_path=app_config.custom_nodes_path) + @asynccontextmanager async def lifespan(app: FastAPI): diff --git a/invokeai/app/invocations/__init__.py b/invokeai/app/invocations/__init__.py index 9a49c2204f..c8d6443752 100644 --- a/invokeai/app/invocations/__init__.py +++ b/invokeai/app/invocations/__init__.py @@ -1,33 +1,5 @@ -import shutil -import sys -from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -from invokeai.app.services.config.config_default import get_config - -custom_nodes_path = Path(get_config().custom_nodes_path) -custom_nodes_path.mkdir(parents=True, exist_ok=True) - -custom_nodes_init_path = str(custom_nodes_path / "__init__.py") -custom_nodes_readme_path = str(custom_nodes_path / "README.md") - -# copy our custom nodes __init__.py to the custom nodes directory -shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path) -shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path) - -# set the same permissions as the destination directory, in case our source is read-only, -# so that the files are user-writable -for p in custom_nodes_path.glob("**/*"): - p.chmod(custom_nodes_path.stat().st_mode) - -# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically -spec = spec_from_file_location("custom_nodes", custom_nodes_init_path) -if spec is None or spec.loader is None: - raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}") -module = module_from_spec(spec) -sys.modules[spec.name] = module -spec.loader.exec_module(module) - # add core nodes to __all__ python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py")) __all__ = [f.stem for f in python_files] # type: ignore diff --git a/invokeai/app/invocations/load_custom_nodes.py b/invokeai/app/invocations/load_custom_nodes.py new file mode 100644 index 0000000000..993237478e --- /dev/null +++ b/invokeai/app/invocations/load_custom_nodes.py @@ -0,0 +1,40 @@ +import shutil +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + + +def load_custom_nodes(custom_nodes_path: Path): + """ + Loads all custom nodes from the custom_nodes_path directory. + + This function copies a custom __init__.py file to the custom_nodes_path directory, effectively turning it into a + python module. + + The custom __init__.py file itself imports all the custom node packs as python modules from the custom_nodes_path + directory. + + Then,the custom __init__.py file is programmatically imported using importlib. As it executes, it imports all the + custom node packs as python modules. + """ + custom_nodes_path.mkdir(parents=True, exist_ok=True) + + custom_nodes_init_path = str(custom_nodes_path / "__init__.py") + custom_nodes_readme_path = str(custom_nodes_path / "README.md") + + # copy our custom nodes __init__.py to the custom nodes directory + shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path) + shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path) + + # set the same permissions as the destination directory, in case our source is read-only, + # so that the files are user-writable + for p in custom_nodes_path.glob("**/*"): + p.chmod(custom_nodes_path.stat().st_mode) + + # Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically + spec = spec_from_file_location("custom_nodes", custom_nodes_init_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}") + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module)