Skip to content

Commit 59a8c0d

Browse files
feat(app): less janky custom node loading
- We don't need to copy the init file. Just crawl the custom nodes dir for modules and import them all. Dunno why I didn't do this initially. - Pass the logger in as an arg. There was a race condition where if we got the logger directly in the load_custom_nodes function, the config would not have been loaded fully yet and we'd end up with the wrong custom nodes path! - Remove permissions-setting logic, I do not believe it is relevant for custom nodes - Minor cleanup of the utility
1 parent d5d08f6 commit 59a8c0d

File tree

3 files changed

+66
-87
lines changed

3 files changed

+66
-87
lines changed

invokeai/app/invocations/custom_nodes/init.py

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,83 @@
1+
import logging
12
import shutil
23
import sys
4+
import traceback
35
from importlib.util import module_from_spec, spec_from_file_location
46
from pathlib import Path
57

68

7-
def load_custom_nodes(custom_nodes_path: Path):
9+
def load_custom_nodes(custom_nodes_path: Path, logger: logging.Logger):
810
"""
911
Loads all custom nodes from the custom_nodes_path directory.
1012
11-
This function copies a custom __init__.py file to the custom_nodes_path directory, effectively turning it into a
12-
python module.
13+
If custom_nodes_path does not exist, it creates it.
1314
14-
The custom __init__.py file itself imports all the custom node packs as python modules from the custom_nodes_path
15-
directory.
15+
It also copies the custom_nodes/README.md file to the custom_nodes_path directory. Because this file may change,
16+
it is _always_ copied to the custom_nodes_path directory.
1617
17-
Then,the custom __init__.py file is programmatically imported using importlib. As it executes, it imports all the
18-
custom node packs as python modules.
18+
Then, it crawls the custom_nodes_path directory and imports all top-level directories as python modules.
19+
20+
If the directory does not contain an __init__.py file or starts with an `_` or `.`, it is skipped.
1921
"""
22+
23+
# create the custom nodes directory if it does not exist
2024
custom_nodes_path.mkdir(parents=True, exist_ok=True)
2125

22-
custom_nodes_init_path = str(custom_nodes_path / "__init__.py")
23-
custom_nodes_readme_path = str(custom_nodes_path / "README.md")
26+
# Copy the README file to the custom nodes directory
27+
source_custom_nodes_readme_path = Path(__file__).parent / "custom_nodes/README.md"
28+
target_custom_nodes_readme_path = Path(custom_nodes_path) / "README.md"
2429

25-
# copy our custom nodes __init__.py to the custom nodes directory
26-
shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path)
27-
shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path)
30+
# copy our custom nodes README to the custom nodes directory
31+
shutil.copy(source_custom_nodes_readme_path, target_custom_nodes_readme_path)
2832

29-
# set the same permissions as the destination directory, in case our source is read-only,
30-
# so that the files are user-writable
31-
for p in custom_nodes_path.glob("**/*"):
32-
p.chmod(custom_nodes_path.stat().st_mode)
33+
loaded_packs: list[str] = []
34+
failed_packs: list[str] = []
3335

3436
# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically
35-
spec = spec_from_file_location("custom_nodes", custom_nodes_init_path)
36-
if spec is None or spec.loader is None:
37-
raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}")
38-
module = module_from_spec(spec)
39-
sys.modules[spec.name] = module
40-
spec.loader.exec_module(module)
37+
for d in custom_nodes_path.iterdir():
38+
# skip files
39+
if not d.is_dir():
40+
continue
41+
42+
# skip hidden directories
43+
if d.name.startswith("_") or d.name.startswith("."):
44+
continue
45+
46+
# skip directories without an `__init__.py`
47+
init = d / "__init__.py"
48+
if not init.exists():
49+
continue
50+
51+
module_name = init.parent.stem
52+
53+
# skip if already imported
54+
if module_name in globals():
55+
continue
56+
57+
# load the module
58+
spec = spec_from_file_location(module_name, init.absolute())
59+
60+
if spec is None or spec.loader is None:
61+
logger.warning(f"Could not load {init}")
62+
continue
63+
64+
logger.info(f"Loading node pack {module_name}")
65+
66+
try:
67+
module = module_from_spec(spec)
68+
sys.modules[spec.name] = module
69+
spec.loader.exec_module(module)
70+
71+
loaded_packs.append(module_name)
72+
except Exception:
73+
failed_packs.append(module_name)
74+
full_error = traceback.format_exc()
75+
logger.error(f"Failed to load node pack {module_name} (may have partially loaded):\n{full_error}")
76+
77+
del init, module_name
78+
79+
loaded_count = len(loaded_packs)
80+
if loaded_count > 0:
81+
logger.info(
82+
f"Loaded {loaded_count} node pack{'s' if loaded_count != 1 else ''} from {custom_nodes_path}: {', '.join(loaded_packs)}"
83+
)

invokeai/app/run_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def run_app() -> None:
5959
# Load custom nodes. This must be done after importing the Graph class, which itself imports all modules from the
6060
# invocations module. The ordering here is implicit, but important - we want to load custom nodes after all the
6161
# core nodes have been imported so that we can catch when a custom node clobbers a core node.
62-
load_custom_nodes(custom_nodes_path=app_config.custom_nodes_path)
62+
load_custom_nodes(custom_nodes_path=app_config.custom_nodes_path, logger=logger)
6363

6464
# Start the server.
6565
config = uvicorn.Config(

0 commit comments

Comments
 (0)