diff --git a/py/config.py b/py/config.py index 03cdc13b..c4e0c1ae 100644 --- a/py/config.py +++ b/py/config.py @@ -2,12 +2,12 @@ import os import platform from pathlib import Path import folder_paths # type: ignore -from typing import Dict, Iterable, List, Mapping, Set +from typing import Any, Dict, Iterable, List, Mapping, Optional, Set import logging import json import urllib.parse -from .utils.settings_paths import ensure_settings_file +from .utils.settings_paths import ensure_settings_file, load_settings_template # Use an environment variable to control standalone mode standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1" or os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0" @@ -45,6 +45,30 @@ def _normalize_folder_paths_for_comparison( return normalized +def _normalize_library_folder_paths( + library_payload: Mapping[str, Any] +) -> Dict[str, Set[str]]: + """Return normalized folder paths extracted from a library payload.""" + + folder_paths = library_payload.get("folder_paths") + if isinstance(folder_paths, Mapping): + return _normalize_folder_paths_for_comparison(folder_paths) + return {} + + +def _get_template_folder_paths() -> Dict[str, Set[str]]: + """Return normalized folder paths defined in the bundled template.""" + + template_payload = load_settings_template() + if not template_payload: + return {} + + folder_paths = template_payload.get("folder_paths") + if isinstance(folder_paths, Mapping): + return _normalize_folder_paths_for_comparison(folder_paths) + return {} + + class Config: """Global configuration for LoRA Manager""" @@ -81,6 +105,43 @@ class Config: comfy_library = libraries.get("comfyui", {}) default_library = libraries.get("default", {}) + template_folder_paths = _get_template_folder_paths() + default_library_paths: Dict[str, Set[str]] = {} + if isinstance(default_library, Mapping): + default_library_paths = _normalize_library_folder_paths(default_library) + + libraries_changed = False + if ( + isinstance(default_library, Mapping) + and template_folder_paths + and default_library_paths == template_folder_paths + ): + if "comfyui" in libraries: + try: + settings_service.delete_library("default") + libraries_changed = True + logger.info("Removed template 'default' library entry") + except Exception as delete_error: + logger.debug( + "Failed to delete template 'default' library: %s", + delete_error, + ) + else: + try: + settings_service.rename_library("default", "comfyui") + libraries_changed = True + logger.info("Renamed template 'default' library to 'comfyui'") + except Exception as rename_error: + logger.debug( + "Failed to rename template 'default' library: %s", + rename_error, + ) + + if libraries_changed: + libraries = settings_service.get_libraries() + comfy_library = libraries.get("comfyui", {}) + default_library = libraries.get("default", {}) + target_folder_paths = { 'loras': list(self.loras_roots), 'checkpoints': list(self.checkpoints_roots or []), @@ -90,9 +151,16 @@ class Config: normalized_target_paths = _normalize_folder_paths_for_comparison(target_folder_paths) - if (not comfy_library and default_library and normalized_target_paths and - _normalize_folder_paths_for_comparison(default_library.get("folder_paths", {})) == - normalized_target_paths): + normalized_default_paths: Optional[Dict[str, Set[str]]] = None + if isinstance(default_library, Mapping): + normalized_default_paths = _normalize_library_folder_paths(default_library) + + if ( + not comfy_library + and default_library + and normalized_target_paths + and normalized_default_paths == normalized_target_paths + ): try: settings_service.rename_library("default", "comfyui") logger.info("Renamed legacy 'default' library to 'comfyui'") diff --git a/py/utils/settings_paths.py b/py/utils/settings_paths.py index 2ebc4cf4..4dcfbf4e 100644 --- a/py/utils/settings_paths.py +++ b/py/utils/settings_paths.py @@ -6,7 +6,7 @@ import json import logging import os import shutil -from typing import Optional +from typing import Any, Dict, Optional from platformdirs import user_config_dir @@ -124,3 +124,32 @@ def _should_use_portable_settings(path: str, logger: logging.Logger) -> bool: ) return False + +def load_settings_template() -> Optional[Dict[str, Any]]: + """Return the parsed contents of ``settings.json.example`` when available.""" + + template_path = os.path.join(get_project_root(), "settings.json.example") + + try: + with open(template_path, "r", encoding="utf-8") as handle: + payload = json.load(handle) + except FileNotFoundError: + _LOGGER.debug("settings.json.example not found at %s", template_path) + return None + except json.JSONDecodeError as exc: + _LOGGER.warning("Failed to parse settings.json.example: %s", exc) + return None + except OSError as exc: + _LOGGER.warning( + "Could not read settings.json.example at %s: %s", template_path, exc + ) + return None + + if not isinstance(payload, dict): + _LOGGER.debug( + "settings.json.example at %s does not contain a JSON object", template_path + ) + return None + + return payload + diff --git a/tests/config/test_config_save_paths.py b/tests/config/test_config_save_paths.py index f0756e36..bd7cf07f 100644 --- a/tests/config/test_config_save_paths.py +++ b/tests/config/test_config_save_paths.py @@ -43,6 +43,7 @@ def test_save_paths_renames_default_library(monkeypatch: pytest.MonkeyPatch, tmp def __init__(self, default_paths: Dict[str, List[str]]): self._default_paths = default_paths self.rename_calls = [] + self.delete_calls = [] self.upsert_calls = [] self._renamed = False @@ -62,6 +63,10 @@ def test_save_paths_renames_default_library(monkeypatch: pytest.MonkeyPatch, tmp self.rename_calls.append((old_name, new_name)) self._renamed = True + def delete_library(self, name: str): # pragma: no cover - defensive guard + self.delete_calls.append(name) + raise AssertionError("delete_library should not be invoked in this scenario") + def upsert_library(self, name: str, **payload): self.upsert_calls.append((name, payload)) @@ -124,3 +129,89 @@ def test_save_paths_logs_warning_when_upsert_fails( assert isinstance(config_instance, config_module.Config) assert fake_settings.upsert_attempts and fake_settings.upsert_attempts[0][0] == "comfyui" assert "Failed to save folder paths: boom" in caplog.text + + +def test_save_paths_removes_template_default_library(monkeypatch, tmp_path): + folder_paths = _setup_config_environment(monkeypatch, tmp_path) + + placeholder_paths = { + "loras": [ + "C:/path/to/your/loras_folder", + "C:/path/to/another/loras_folder", + ], + "checkpoints": [ + "C:/path/to/your/checkpoints_folder", + "C:/path/to/another/checkpoints_folder", + ], + "embeddings": [ + "C:/path/to/your/embeddings_folder", + "C:/path/to/another/embeddings_folder", + ], + } + + class FakeSettingsService: + def __init__(self): + self.libraries = { + "default": { + "folder_paths": placeholder_paths, + "default_lora_root": "", + "default_checkpoint_root": "", + "default_embedding_root": "", + } + } + self.rename_calls = [] + self.delete_calls = [] + self.upsert_calls = [] + + def get_libraries(self): + return self.libraries + + def rename_library(self, old_name: str, new_name: str): + self.rename_calls.append((old_name, new_name)) + self.libraries[new_name] = self.libraries.pop(old_name) + + def delete_library(self, name: str): + self.delete_calls.append(name) + self.libraries.pop(name, None) + + def upsert_library(self, name: str, **payload): + self.upsert_calls.append((name, payload)) + self.libraries[name] = {**payload} + + fake_settings = FakeSettingsService() + monkeypatch.setattr(settings_manager_module, "settings", fake_settings) + + monkeypatch.setattr( + config_module, + "load_settings_template", + lambda: {"folder_paths": placeholder_paths}, + ) + + config_instance = config_module.Config() + + assert isinstance(config_instance, config_module.Config) + assert fake_settings.rename_calls == [("default", "comfyui")] + assert not fake_settings.delete_calls + assert len(fake_settings.upsert_calls) == 1 + assert "default" not in fake_settings.libraries + assert set(fake_settings.libraries.keys()) == {"comfyui"} + + name, payload = fake_settings.upsert_calls[0] + assert name == "comfyui" + + expected_folder_paths = { + key: [path.replace("\\", "/") for path in paths] + for key, paths in folder_paths.items() + } + assert payload["folder_paths"] == expected_folder_paths + assert payload["default_lora_root"] == folder_paths["loras"][0].replace("\\", "/") + assert ( + payload["default_checkpoint_root"] + == folder_paths["checkpoints"][0].replace("\\", "/") + ) + assert ( + payload["default_embedding_root"] + == folder_paths["embeddings"][0].replace("\\", "/") + ) + assert payload["metadata"] == {"display_name": "ComfyUI", "source": "comfyui"} + assert payload["activate"] is True