mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-08-30 13:30:05 +08:00
Add support for extra custom node path.
Clean up tests Add tests
This commit is contained in:
parent
a86aaa4301
commit
a718339c34
120
EXTRA_CUSTOM_NODE_PATHS_README.md
Normal file
120
EXTRA_CUSTOM_NODE_PATHS_README.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# Extra Custom Node Paths
|
||||||
|
|
||||||
|
This feature allows you to load custom nodes from multiple directories, similar to how extra model paths work in ComfyUI.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ComfyUI now supports loading custom nodes from multiple directories through a YAML configuration file called `extra_custom_node_paths.yaml`. This is useful when you want to:
|
||||||
|
|
||||||
|
- Organize custom nodes in different locations
|
||||||
|
- Share custom nodes between multiple ComfyUI installations
|
||||||
|
- Keep custom nodes separate from the main ComfyUI directory
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
Create a file called `extra_custom_node_paths.yaml` in your ComfyUI root directory:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Configuration for custom nodes
|
||||||
|
# The custom_nodes directory is always called "custom_nodes" and is located under base_path
|
||||||
|
# Each config section can only have one base_path
|
||||||
|
|
||||||
|
# Example configuration using base_path
|
||||||
|
# This will look for custom_nodes under the specified base_path
|
||||||
|
custom_nodes_example:
|
||||||
|
base_path: /path/to/base1
|
||||||
|
|
||||||
|
# Another example with a different path
|
||||||
|
other_custom_nodes:
|
||||||
|
base_path: /path/to/base
|
||||||
|
|
||||||
|
# You can add more custom node path configurations as needed
|
||||||
|
# additional_custom_nodes:
|
||||||
|
# base_path: another/base/path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
- **base_path**: The base directory path where the `custom_nodes` subdirectory is located
|
||||||
|
- Can be relative or absolute paths
|
||||||
|
- Relative paths are resolved relative to the YAML file location
|
||||||
|
- The system automatically looks for a `custom_nodes` subdirectory under each base_path
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Automatic Loading**: ComfyUI automatically looks for `extra_custom_node_paths.yaml` in the root directory
|
||||||
|
2. **Path Resolution**: All base paths are resolved to absolute paths and validated
|
||||||
|
3. **Subdirectory Detection**: The system automatically appends `custom_nodes` to each base path
|
||||||
|
4. **Integration**: Custom node paths are added to the existing custom nodes system
|
||||||
|
5. **Compatibility**: Works seamlessly with existing custom node functionality
|
||||||
|
|
||||||
|
## Example Use Cases
|
||||||
|
|
||||||
|
### Shared Custom Nodes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Share custom nodes between multiple ComfyUI installations
|
||||||
|
shared_nodes:
|
||||||
|
base_path: /shared/custom_nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relative Paths
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Use relative paths for portable configurations
|
||||||
|
portable_config:
|
||||||
|
base_path: ../shared_custom_nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
The custom node directories should follow the standard ComfyUI custom node structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
base_path/
|
||||||
|
└── custom_nodes/
|
||||||
|
├── node_name_1/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── nodes.py
|
||||||
|
├── node_name_2/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── nodes.py
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Path Not Found
|
||||||
|
- Ensure the directory exists and is accessible
|
||||||
|
- Check file permissions
|
||||||
|
- Verify the path is correctly formatted in the YAML file
|
||||||
|
- Make sure there's a `custom_nodes` subdirectory under the specified base_path
|
||||||
|
|
||||||
|
### Custom Nodes Not Loading
|
||||||
|
- Check the ComfyUI console for error messages
|
||||||
|
- Verify the YAML syntax is correct
|
||||||
|
- Ensure the custom node directories contain valid Python modules
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- Custom node paths are loaded during ComfyUI startup
|
||||||
|
- Paths are validated for existence before being added
|
||||||
|
- The system maintains the order of paths (default paths first)
|
||||||
|
- Duplicate paths are automatically handled
|
||||||
|
- All paths are normalized and resolved to absolute paths
|
||||||
|
- Each config section can only specify one base_path
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This feature is compatible with:
|
||||||
|
- All existing custom node functionality
|
||||||
|
- The existing custom node loading system
|
||||||
|
- Command line arguments and configuration files
|
||||||
|
- All supported operating systems
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Extra Model Paths](../extra_model_paths.yaml) - Similar functionality for model paths
|
||||||
|
- [Custom Nodes Documentation](../custom_nodes/) - General custom node information
|
||||||
|
- [ComfyUI CLI Arguments](../comfy/cli_args.py) - Command line options
|
||||||
18
extra_custom_node_paths.yaml.example
Normal file
18
extra_custom_node_paths.yaml.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Rename this to extra_custom_node_paths.yaml and ComfyUI will load it
|
||||||
|
|
||||||
|
# Configuration for custom nodes
|
||||||
|
# The custom_nodes directory is always called "custom_nodes" and is located under base_path
|
||||||
|
# Each config section can only have one base_path
|
||||||
|
|
||||||
|
# Example configuration using base_path
|
||||||
|
# This will look for custom_nodes under the specified base_path
|
||||||
|
comfyui:
|
||||||
|
base_path: ../
|
||||||
|
|
||||||
|
# Another example with a different path
|
||||||
|
# other_custom_nodes:
|
||||||
|
# base_path: /path/to/base
|
||||||
|
|
||||||
|
# You can add more custom node path configurations as needed
|
||||||
|
# additional_custom_nodes:
|
||||||
|
# base_path: another/base/path
|
||||||
@ -427,3 +427,24 @@ def get_input_subfolders() -> list[str]:
|
|||||||
return sorted(folders)
|
return sorted(folders)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_custom_nodes_directories() -> list[str]:
|
||||||
|
"""Get the list of custom node directories.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: List of paths to custom node directories
|
||||||
|
"""
|
||||||
|
if "custom_nodes" not in folder_names_and_paths:
|
||||||
|
return []
|
||||||
|
return list(folder_names_and_paths["custom_nodes"][0])
|
||||||
|
|
||||||
|
|
||||||
|
def add_custom_node_directory(directory: str) -> None:
|
||||||
|
"""Add a new custom node directory to the list of custom node directories"""
|
||||||
|
global folder_names_and_paths
|
||||||
|
if "custom_nodes" in folder_names_and_paths:
|
||||||
|
paths, _exts = folder_names_and_paths["custom_nodes"]
|
||||||
|
if directory not in paths:
|
||||||
|
paths.append(directory)
|
||||||
|
else:
|
||||||
|
folder_names_and_paths["custom_nodes"] = ([directory], set())
|
||||||
|
|||||||
7
main.py
7
main.py
@ -8,7 +8,7 @@ import time
|
|||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
from app.logger import setup_logger
|
from app.logger import setup_logger
|
||||||
import itertools
|
import itertools
|
||||||
import utils.extra_config
|
import utils.extra_config, utils.extra_custom_node_config
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from comfy_execution.progress import get_progress_state
|
from comfy_execution.progress import get_progress_state
|
||||||
@ -32,6 +32,11 @@ def apply_custom_paths():
|
|||||||
for config_path in itertools.chain(*args.extra_model_paths_config):
|
for config_path in itertools.chain(*args.extra_model_paths_config):
|
||||||
utils.extra_config.load_extra_path_config(config_path)
|
utils.extra_config.load_extra_path_config(config_path)
|
||||||
|
|
||||||
|
#extra custom node paths
|
||||||
|
extra_custom_node_paths_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "extra_custom_node_paths.yaml")
|
||||||
|
if os.path.isfile(extra_custom_node_paths_config_path):
|
||||||
|
utils.extra_custom_node_config.load_extra_custom_node_path_config(extra_custom_node_paths_config_path)
|
||||||
|
|
||||||
# --output-directory, --input-directory, --user-directory
|
# --output-directory, --input-directory, --user-directory
|
||||||
if args.output_directory:
|
if args.output_directory:
|
||||||
output_dir = os.path.abspath(args.output_directory)
|
output_dir = os.path.abspath(args.output_directory)
|
||||||
|
|||||||
152
tests-unit/test_extra_custom_node_paths.py
Normal file
152
tests-unit/test_extra_custom_node_paths.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch, mock_open, MagicMock
|
||||||
|
|
||||||
|
# Add the project root to the Python path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from utils.extra_custom_node_config import load_extra_custom_node_path_config, get_current_custom_node_paths
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup_teardown():
|
||||||
|
"""Fixture to save and restore the original folder paths state."""
|
||||||
|
# Save original state
|
||||||
|
original_paths = folder_paths.folder_names_and_paths.copy()
|
||||||
|
original_custom_nodes = folder_paths.get_custom_nodes_directories()
|
||||||
|
|
||||||
|
# Reset custom nodes for each test
|
||||||
|
if "custom_nodes" in folder_paths.folder_names_and_paths:
|
||||||
|
folder_paths.folder_names_and_paths["custom_nodes"] = (list(original_custom_nodes), set())
|
||||||
|
|
||||||
|
yield # Test runs here
|
||||||
|
|
||||||
|
# Restore original state
|
||||||
|
folder_paths.folder_names_and_paths = original_paths
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_extra_custom_node_paths():
|
||||||
|
"""Test loading custom node paths from YAML config."""
|
||||||
|
# Mock YAML content
|
||||||
|
yaml_content = """
|
||||||
|
custom_nodes_config:
|
||||||
|
base_path: /custom/nodes/path
|
||||||
|
another_config:
|
||||||
|
base_path: /another/nodes/path
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set up mocks
|
||||||
|
with patch('builtins.open', mock_open(read_data=yaml_content)) as mock_file, \
|
||||||
|
patch('os.path.exists', return_value=True), \
|
||||||
|
patch('os.path.isdir', return_value=True), \
|
||||||
|
patch('os.path.abspath', side_effect=lambda x: x) as mock_abspath:
|
||||||
|
|
||||||
|
# Call the function
|
||||||
|
load_extra_custom_node_path_config("dummy_path.yaml")
|
||||||
|
|
||||||
|
# Verify the file was opened
|
||||||
|
mock_file.assert_called_once_with("dummy_path.yaml", 'r', encoding='utf-8')
|
||||||
|
|
||||||
|
# Verify the paths were added
|
||||||
|
custom_paths = folder_paths.get_custom_nodes_directories()
|
||||||
|
# Normalize paths for consistent comparison
|
||||||
|
normalized_paths = [os.path.normpath(p).replace('\\', '/').lower() for p in custom_paths]
|
||||||
|
expected_paths = ["/custom/nodes/path/custom_nodes",
|
||||||
|
"/another/nodes/path/custom_nodes"]
|
||||||
|
|
||||||
|
for path in expected_paths:
|
||||||
|
assert path.lower() in normalized_paths, f"Expected path {path} not found in {normalized_paths}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_extra_custom_node_paths_with_tilde():
|
||||||
|
"""Test that paths with tilde are expanded."""
|
||||||
|
yaml_content = """
|
||||||
|
custom_nodes_config:
|
||||||
|
base_path: ~/custom_nodes
|
||||||
|
"""
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=yaml_content)) as mock_file, \
|
||||||
|
patch('os.path.expanduser', side_effect=lambda x: x.replace('~', '/home/user')), \
|
||||||
|
patch('os.path.exists', return_value=True), \
|
||||||
|
patch('os.path.isdir', return_value=True), \
|
||||||
|
patch('os.path.abspath', side_effect=lambda x: x):
|
||||||
|
|
||||||
|
load_extra_custom_node_path_config("dummy_path.yaml")
|
||||||
|
|
||||||
|
custom_paths = folder_paths.get_custom_nodes_directories()
|
||||||
|
# Normalize paths for consistent comparison
|
||||||
|
normalized_paths = [os.path.normpath(p).replace('\\', '/').lower() for p in custom_paths]
|
||||||
|
expected_path = "/home/user/custom_nodes/custom_nodes"
|
||||||
|
assert expected_path.lower() in normalized_paths, f"Expected path {expected_path} not found in {normalized_paths}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_extra_custom_node_paths_with_env_vars():
|
||||||
|
"""Test that environment variables in paths are expanded."""
|
||||||
|
yaml_content = """
|
||||||
|
custom_nodes_config:
|
||||||
|
base_path: ${MY_CUSTOM_NODES_PATH}/custom_nodes
|
||||||
|
"""
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=yaml_content)), \
|
||||||
|
patch.dict('os.environ', {'MY_CUSTOM_NODES_PATH': '/env/path'}), \
|
||||||
|
patch('os.path.exists', return_value=True), \
|
||||||
|
patch('os.path.isdir', return_value=True), \
|
||||||
|
patch('os.path.abspath', side_effect=lambda x: x):
|
||||||
|
|
||||||
|
load_extra_custom_node_path_config("dummy_path.yaml")
|
||||||
|
|
||||||
|
custom_paths = folder_paths.get_custom_nodes_directories()
|
||||||
|
# Normalize paths for consistent comparison
|
||||||
|
normalized_paths = [os.path.normpath(p).replace('\\', '/').lower() for p in custom_paths]
|
||||||
|
expected_path = "/env/path/custom_nodes/custom_nodes"
|
||||||
|
assert expected_path.lower() in normalized_paths, f"Expected path {expected_path} not found in {normalized_paths}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonexistent_paths_are_skipped():
|
||||||
|
"""Test that non-existent paths are skipped with a warning."""
|
||||||
|
yaml_content = """
|
||||||
|
custom_nodes_config:
|
||||||
|
base_path: /nonexistent/path
|
||||||
|
"""
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=yaml_content)), \
|
||||||
|
patch('os.path.exists', return_value=False), \
|
||||||
|
patch('os.path.isdir', return_value=False), \
|
||||||
|
patch('logging.warning') as mock_warning:
|
||||||
|
|
||||||
|
load_extra_custom_node_path_config("dummy_path.yaml")
|
||||||
|
|
||||||
|
# Verify warning was logged
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
assert "does not exist" in mock_warning.call_args[0][0]
|
||||||
|
|
||||||
|
# Verify path was not added
|
||||||
|
assert "/nonexistent/path/custom_nodes" not in folder_paths.get_custom_nodes_directories()
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_yaml_handling():
|
||||||
|
"""Test that invalid YAML is handled gracefully."""
|
||||||
|
with patch('builtins.open', mock_open(read_data='invalid: yaml: : :')), \
|
||||||
|
patch('logging.error') as mock_error:
|
||||||
|
|
||||||
|
load_extra_custom_node_path_config("invalid.yaml")
|
||||||
|
|
||||||
|
# Verify error was logged
|
||||||
|
mock_error.assert_called_once()
|
||||||
|
assert "Failed to load extra custom node paths" in mock_error.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_config():
|
||||||
|
"""Test that empty config doesn't cause errors."""
|
||||||
|
with patch('builtins.open', mock_open(read_data='')), \
|
||||||
|
patch('yaml.safe_load', return_value={'custom_nodes_config': {}}), \
|
||||||
|
patch('logging.info') as mock_info:
|
||||||
|
|
||||||
|
load_extra_custom_node_path_config("empty.yaml")
|
||||||
|
|
||||||
|
# Verify info was logged
|
||||||
|
mock_info.assert_called_once()
|
||||||
|
assert "No custom node paths found in configuration" in mock_info.call_args[0][0]
|
||||||
59
utils/extra_custom_node_config.py
Normal file
59
utils/extra_custom_node_config.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import folder_paths
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def load_extra_custom_node_path_config(yaml_path):
|
||||||
|
"""
|
||||||
|
Load extra custom node paths from a YAML configuration file.
|
||||||
|
Similar to load_extra_path_config but specifically for custom nodes.
|
||||||
|
Each config section can have one base_path, and custom_nodes will be looked for under that path.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(yaml_path, 'r', encoding='utf-8') as stream:
|
||||||
|
config = yaml.safe_load(stream)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to load extra custom node paths config from {yaml_path}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
yaml_dir = os.path.dirname(os.path.abspath(yaml_path))
|
||||||
|
|
||||||
|
# Collect all custom node paths from the YAML configuration
|
||||||
|
all_custom_node_paths = []
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
return
|
||||||
|
|
||||||
|
for c in config:
|
||||||
|
conf = config[c]
|
||||||
|
if conf is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle base_path (creates custom_nodes subdirectory)
|
||||||
|
if "base_path" in conf:
|
||||||
|
base_path = conf["base_path"]
|
||||||
|
# Process the base path
|
||||||
|
expanded_path = os.path.expandvars(os.path.expanduser(base_path))
|
||||||
|
if not os.path.isabs(expanded_path):
|
||||||
|
expanded_path = os.path.abspath(os.path.join(yaml_dir, expanded_path))
|
||||||
|
|
||||||
|
# Create the custom_nodes subdirectory path
|
||||||
|
custom_nodes_path = os.path.join(expanded_path, "custom_nodes")
|
||||||
|
all_custom_node_paths.append(custom_nodes_path)
|
||||||
|
|
||||||
|
# Add all custom node paths to the custom_nodes folder list
|
||||||
|
if all_custom_node_paths:
|
||||||
|
for custom_node_path in all_custom_node_paths:
|
||||||
|
if os.path.exists(custom_node_path):
|
||||||
|
logging.info(f"Adding extra custom node search path: {custom_node_path}")
|
||||||
|
folder_paths.add_custom_node_directory(custom_node_path)
|
||||||
|
else:
|
||||||
|
logging.warning(f"Custom node path does not exist, skipping: {custom_node_path}")
|
||||||
|
|
||||||
|
logging.info(f"Added {len(all_custom_node_paths)} custom node directories")
|
||||||
|
else:
|
||||||
|
logging.info("No custom node paths found in configuration")
|
||||||
|
|
||||||
|
def get_current_custom_node_paths() -> list[str]:
|
||||||
|
"""Get the current list of all custom node paths for debugging purposes"""
|
||||||
|
return folder_paths.get_custom_nodes_directories()
|
||||||
Loading…
x
Reference in New Issue
Block a user