mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-08-16 02:56:36 +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)
|
||||
except FileNotFoundError:
|
||||
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 app.logger import setup_logger
|
||||
import itertools
|
||||
import utils.extra_config
|
||||
import utils.extra_config, utils.extra_custom_node_config
|
||||
import logging
|
||||
import sys
|
||||
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):
|
||||
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
|
||||
if 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