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)