mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Add extra folder paths support for LoRA Manager
Introduce extra_folder_paths feature to allow users to add additional model roots that are managed by LoRA Manager but not shared with ComfyUI. Changes: - Add extra_folder_paths support in SettingsManager (stored per library) - Add extra path attributes in Config class (extra_loras_roots, etc.) - Merge folder_paths with extra_folder_paths when applying library settings - Update LoraScanner, CheckpointScanner, EmbeddingScanner to include extra paths in their model roots - Add comprehensive tests for the new functionality This enables users to manage models from additional directories without modifying ComfyUI's model folder configuration.
This commit is contained in:
@@ -215,3 +215,110 @@ def test_save_paths_removes_template_default_library(monkeypatch, tmp_path):
|
||||
)
|
||||
assert payload["metadata"] == {"display_name": "ComfyUI", "source": "comfyui"}
|
||||
assert payload["activate"] is True
|
||||
|
||||
|
||||
def test_apply_library_settings_merges_extra_paths(monkeypatch, tmp_path):
|
||||
"""Test that apply_library_settings correctly merges folder_paths with extra_folder_paths."""
|
||||
loras_dir = tmp_path / "loras"
|
||||
extra_loras_dir = tmp_path / "extra_loras"
|
||||
checkpoints_dir = tmp_path / "checkpoints"
|
||||
extra_checkpoints_dir = tmp_path / "extra_checkpoints"
|
||||
embeddings_dir = tmp_path / "embeddings"
|
||||
extra_embeddings_dir = tmp_path / "extra_embeddings"
|
||||
|
||||
for directory in (loras_dir, extra_loras_dir, checkpoints_dir, extra_checkpoints_dir, embeddings_dir, extra_embeddings_dir):
|
||||
directory.mkdir()
|
||||
|
||||
config_instance = config_module.Config()
|
||||
|
||||
folder_paths = {
|
||||
"loras": [str(loras_dir)],
|
||||
"checkpoints": [str(checkpoints_dir)],
|
||||
"unet": [],
|
||||
"embeddings": [str(embeddings_dir)],
|
||||
}
|
||||
extra_folder_paths = {
|
||||
"loras": [str(extra_loras_dir)],
|
||||
"checkpoints": [str(extra_checkpoints_dir)],
|
||||
"unet": [],
|
||||
"embeddings": [str(extra_embeddings_dir)],
|
||||
}
|
||||
|
||||
library_config = {
|
||||
"folder_paths": folder_paths,
|
||||
"extra_folder_paths": extra_folder_paths,
|
||||
}
|
||||
|
||||
config_instance.apply_library_settings(library_config)
|
||||
|
||||
assert str(loras_dir) in config_instance.loras_roots
|
||||
assert str(extra_loras_dir) in config_instance.extra_loras_roots
|
||||
assert str(checkpoints_dir) in config_instance.base_models_roots
|
||||
assert str(extra_checkpoints_dir) in config_instance.extra_checkpoints_roots
|
||||
assert str(embeddings_dir) in config_instance.embeddings_roots
|
||||
assert str(extra_embeddings_dir) in config_instance.extra_embeddings_roots
|
||||
|
||||
|
||||
def test_apply_library_settings_without_extra_paths(monkeypatch, tmp_path):
|
||||
"""Test that apply_library_settings works when extra_folder_paths is not provided."""
|
||||
loras_dir = tmp_path / "loras"
|
||||
checkpoints_dir = tmp_path / "checkpoints"
|
||||
embeddings_dir = tmp_path / "embeddings"
|
||||
|
||||
for directory in (loras_dir, checkpoints_dir, embeddings_dir):
|
||||
directory.mkdir()
|
||||
|
||||
config_instance = config_module.Config()
|
||||
|
||||
folder_paths = {
|
||||
"loras": [str(loras_dir)],
|
||||
"checkpoints": [str(checkpoints_dir)],
|
||||
"unet": [],
|
||||
"embeddings": [str(embeddings_dir)],
|
||||
}
|
||||
|
||||
library_config = {
|
||||
"folder_paths": folder_paths,
|
||||
}
|
||||
|
||||
config_instance.apply_library_settings(library_config)
|
||||
|
||||
assert str(loras_dir) in config_instance.loras_roots
|
||||
assert config_instance.extra_loras_roots == []
|
||||
assert str(checkpoints_dir) in config_instance.base_models_roots
|
||||
assert config_instance.extra_checkpoints_roots == []
|
||||
assert str(embeddings_dir) in config_instance.embeddings_roots
|
||||
assert config_instance.extra_embeddings_roots == []
|
||||
|
||||
|
||||
def test_extra_paths_deduplication(monkeypatch, tmp_path):
|
||||
"""Test that extra paths are stored separately from main paths in Config."""
|
||||
loras_dir = tmp_path / "loras"
|
||||
extra_loras_dir = tmp_path / "extra_loras"
|
||||
loras_dir.mkdir()
|
||||
extra_loras_dir.mkdir()
|
||||
|
||||
config_instance = config_module.Config()
|
||||
|
||||
folder_paths = {
|
||||
"loras": [str(loras_dir)],
|
||||
"checkpoints": [],
|
||||
"unet": [],
|
||||
"embeddings": [],
|
||||
}
|
||||
extra_folder_paths = {
|
||||
"loras": [str(extra_loras_dir)],
|
||||
"checkpoints": [],
|
||||
"unet": [],
|
||||
"embeddings": [],
|
||||
}
|
||||
|
||||
library_config = {
|
||||
"folder_paths": folder_paths,
|
||||
"extra_folder_paths": extra_folder_paths,
|
||||
}
|
||||
|
||||
config_instance.apply_library_settings(library_config)
|
||||
|
||||
assert config_instance.loras_roots == [str(loras_dir)]
|
||||
assert config_instance.extra_loras_roots == [str(extra_loras_dir)]
|
||||
|
||||
@@ -132,4 +132,59 @@ async def test_persisted_cache_restores_model_type(tmp_path: Path, monkeypatch):
|
||||
assert types_by_path[normalized_unet_file] == "diffusion_model"
|
||||
|
||||
assert ws_stub.payloads
|
||||
assert ws_stub.payloads[-1]["stage"] == "loading_cache"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkpoint_scanner_get_model_roots_includes_extra_paths(monkeypatch, tmp_path):
|
||||
"""Test that get_model_roots includes both main and extra paths."""
|
||||
checkpoints_root = tmp_path / "checkpoints"
|
||||
extra_checkpoints_root = tmp_path / "extra_checkpoints"
|
||||
unet_root = tmp_path / "unet"
|
||||
extra_unet_root = tmp_path / "extra_unet"
|
||||
|
||||
for directory in (checkpoints_root, extra_checkpoints_root, unet_root, extra_unet_root):
|
||||
directory.mkdir()
|
||||
|
||||
normalized_checkpoints = _normalize(checkpoints_root)
|
||||
normalized_extra_checkpoints = _normalize(extra_checkpoints_root)
|
||||
normalized_unet = _normalize(unet_root)
|
||||
normalized_extra_unet = _normalize(extra_unet_root)
|
||||
|
||||
monkeypatch.setattr(
|
||||
model_scanner.config,
|
||||
"base_models_roots",
|
||||
[normalized_checkpoints, normalized_unet],
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_scanner.config,
|
||||
"checkpoints_roots",
|
||||
[normalized_checkpoints],
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_scanner.config,
|
||||
"unet_roots",
|
||||
[normalized_unet],
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_scanner.config,
|
||||
"extra_checkpoints_roots",
|
||||
[normalized_extra_checkpoints],
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
model_scanner.config,
|
||||
"extra_unet_roots",
|
||||
[normalized_extra_unet],
|
||||
raising=False,
|
||||
)
|
||||
|
||||
scanner = CheckpointScanner()
|
||||
roots = scanner.get_model_roots()
|
||||
|
||||
assert normalized_checkpoints in roots
|
||||
assert normalized_unet in roots
|
||||
assert normalized_extra_checkpoints in roots
|
||||
assert normalized_extra_unet in roots
|
||||
|
||||
@@ -470,6 +470,100 @@ def test_upsert_library_creates_entry_and_activates(manager, tmp_path):
|
||||
assert str(lora_dir).replace(os.sep, "/") in normalized_stored_paths
|
||||
|
||||
|
||||
def test_extra_folder_paths_stored_separately(manager, tmp_path):
|
||||
lora_dir = tmp_path / "loras"
|
||||
extra_dir = tmp_path / "extra_loras"
|
||||
lora_dir.mkdir()
|
||||
extra_dir.mkdir()
|
||||
|
||||
manager.upsert_library(
|
||||
"test_library",
|
||||
folder_paths={"loras": [str(lora_dir)]},
|
||||
extra_folder_paths={"loras": [str(extra_dir)]},
|
||||
activate=True,
|
||||
)
|
||||
|
||||
libraries = manager.get_libraries()
|
||||
lib = libraries["test_library"]
|
||||
|
||||
# Verify folder_paths contains main path
|
||||
assert str(lora_dir) in lib["folder_paths"]["loras"]
|
||||
# Verify extra_folder_paths contains extra path
|
||||
assert str(extra_dir) in lib["extra_folder_paths"]["loras"]
|
||||
# Verify they are separate
|
||||
assert str(extra_dir) not in lib["folder_paths"]["loras"]
|
||||
|
||||
|
||||
def test_get_extra_folder_paths(manager, tmp_path):
|
||||
extra_dir = tmp_path / "extra_loras"
|
||||
extra_dir.mkdir()
|
||||
|
||||
manager.update_extra_folder_paths({"loras": [str(extra_dir)]})
|
||||
|
||||
extra_paths = manager.get_extra_folder_paths()
|
||||
assert str(extra_dir) in extra_paths.get("loras", [])
|
||||
|
||||
|
||||
def test_library_switch_preserves_extra_paths(manager, tmp_path):
|
||||
"""Test that switching libraries preserves each library's extra paths."""
|
||||
lora_dir1 = tmp_path / "lib1_loras"
|
||||
extra_dir1 = tmp_path / "lib1_extra"
|
||||
lora_dir2 = tmp_path / "lib2_loras"
|
||||
extra_dir2 = tmp_path / "lib2_extra"
|
||||
|
||||
for directory in (lora_dir1, extra_dir1, lora_dir2, extra_dir2):
|
||||
directory.mkdir()
|
||||
|
||||
manager.create_library(
|
||||
"library1",
|
||||
folder_paths={"loras": [str(lora_dir1)]},
|
||||
extra_folder_paths={"loras": [str(extra_dir1)]},
|
||||
activate=True,
|
||||
)
|
||||
|
||||
manager.create_library(
|
||||
"library2",
|
||||
folder_paths={"loras": [str(lora_dir2)]},
|
||||
extra_folder_paths={"loras": [str(extra_dir2)]},
|
||||
)
|
||||
|
||||
assert manager.get_active_library_name() == "library1"
|
||||
lib1 = manager.get_active_library()
|
||||
assert str(lora_dir1) in lib1["folder_paths"]["loras"]
|
||||
assert str(extra_dir1) in lib1["extra_folder_paths"]["loras"]
|
||||
|
||||
manager.activate_library("library2")
|
||||
|
||||
assert manager.get_active_library_name() == "library2"
|
||||
lib2 = manager.get_active_library()
|
||||
assert str(lora_dir2) in lib2["folder_paths"]["loras"]
|
||||
assert str(extra_dir2) in lib2["extra_folder_paths"]["loras"]
|
||||
|
||||
|
||||
def test_extra_paths_validation_no_overlap_with_other_libraries(manager, tmp_path):
|
||||
"""Test that extra paths cannot overlap with other libraries' paths."""
|
||||
lora_dir1 = tmp_path / "lib1_loras"
|
||||
lora_dir1.mkdir()
|
||||
|
||||
manager.create_library(
|
||||
"library1",
|
||||
folder_paths={"loras": [str(lora_dir1)]},
|
||||
activate=True,
|
||||
)
|
||||
|
||||
extra_dir = tmp_path / "extra_loras"
|
||||
extra_dir.mkdir()
|
||||
|
||||
manager.create_library(
|
||||
"library2",
|
||||
folder_paths={"loras": [str(extra_dir)]},
|
||||
activate=True,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="already assigned to library"):
|
||||
manager.update_extra_folder_paths({"loras": [str(lora_dir1)]})
|
||||
|
||||
|
||||
def test_delete_library_switches_active(manager, tmp_path):
|
||||
other_dir = tmp_path / "other"
|
||||
other_dir.mkdir()
|
||||
|
||||
Reference in New Issue
Block a user