diff --git a/py/config.py b/py/config.py index 997e76c2..65c948bd 100644 --- a/py/config.py +++ b/py/config.py @@ -1,5 +1,6 @@ import os import platform +import posixpath import threading from pathlib import Path import folder_paths # type: ignore @@ -25,6 +26,15 @@ standalone_mode = ( logger = logging.getLogger(__name__) +def _normalize_root_identity(path: str) -> str: + """Normalize a root path for comparisons across slash styles.""" + + normalized = posixpath.normpath(path.strip().replace("\\", "/")) + if len(normalized) >= 2 and normalized[1] == ":": + return normalized.lower() + return normalized + + def _resolve_valid_default_root( current: str, primary_paths: List[str], allowed_paths: List[str], name: str ) -> str: @@ -37,14 +47,17 @@ def _resolve_valid_default_root( if not isinstance(path, str): continue stripped = path.strip() - if not stripped or stripped in seen: + if not stripped: continue - seen.add(stripped) + identity = _normalize_root_identity(stripped) + if identity in seen: + continue + seen.add(identity) fallback_paths.append(stripped) - allowed = set(fallback_paths) + allowed = {_normalize_root_identity(path) for path in fallback_paths} - if current and current in allowed: + if current and _normalize_root_identity(current) in allowed: return current if not valid_paths: diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 2c1fc727..b91b303f 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -2,6 +2,7 @@ import asyncio import copy import json import os +import posixpath import shutil import tempfile import logging @@ -103,6 +104,15 @@ DEFAULT_SETTINGS: Dict[str, Any] = { } +def _normalize_root_identity(path: str) -> str: + """Normalize a root path for equality checks across slash styles.""" + + normalized = posixpath.normpath(path.strip().replace("\\", "/")) + if len(normalized) >= 2 and normalized[1] == ":": + return normalized.lower() + return normalized + + class SettingsManager: def __init__(self): self.settings_file = ensure_settings_file(logger) @@ -773,7 +783,7 @@ class SettingsManager: return False allowed_roots = self._get_allowed_roots(key) - if current and current in allowed_roots: + if current and _normalize_root_identity(current) in allowed_roots: return False self.settings[setting_key] = primary_candidates[0] @@ -824,16 +834,19 @@ class SettingsManager: if not isinstance(value, str): continue normalized = value.strip() - if not normalized or normalized in seen: + if not normalized: continue - seen.add(normalized) + identity = _normalize_root_identity(normalized) + if identity in seen: + continue + seen.add(identity) candidates.append(normalized) return candidates def _get_allowed_roots(self, key: str) -> set[str]: """Return all valid roots for a model type, including extra roots.""" - return set(self._get_valid_root_candidates(key)) + return {_normalize_root_identity(path) for path in self._get_valid_root_candidates(key)} def _check_environment_variables(self) -> None: """Check for environment variables and update settings if needed""" diff --git a/tests/config/test_config_save_paths.py b/tests/config/test_config_save_paths.py index 0d2d13b8..a7721cdb 100644 --- a/tests/config/test_config_save_paths.py +++ b/tests/config/test_config_save_paths.py @@ -402,6 +402,49 @@ def test_save_paths_keeps_default_roots_in_extra_paths(monkeypatch: pytest.Monke assert fake_settings.payload["activate"] is True +def test_save_paths_keeps_default_roots_in_extra_paths_with_windows_slash_mismatch( + monkeypatch: pytest.MonkeyPatch, tmp_path +): + folder_paths = _setup_config_environment(monkeypatch, tmp_path) + + class FakeSettingsService: + active_library = "comfyui" + + def get_libraries(self): + return { + "comfyui": { + "folder_paths": {key: list(value) for key, value in folder_paths.items()}, + "extra_folder_paths": { + "loras": ["U:\\Lora7\\Loras"], + "checkpoints": ["U:\\Lora7\\Models"], + "embeddings": [], + }, + "default_lora_root": "U:/Lora7/Loras", + "default_checkpoint_root": "U:/Lora7/Models", + "default_embedding_root": folder_paths["embeddings"][0], + } + } + + def rename_library(self, *_): + raise AssertionError("rename_library should not be invoked") + + def get_active_library_name(self): + return self.active_library + + def upsert_library(self, name: str, **payload): + self.name = name + self.payload = payload + + fake_settings = FakeSettingsService() + monkeypatch.setattr(settings_manager_module, "settings", fake_settings) + + config_module.Config() + + assert fake_settings.name == "comfyui" + assert fake_settings.payload["default_lora_root"] == "U:/Lora7/Loras" + assert fake_settings.payload["default_checkpoint_root"] == "U:/Lora7/Models" + + def test_save_paths_repairs_empty_default_roots_to_extra_paths_when_primary_missing( monkeypatch: pytest.MonkeyPatch, tmp_path ): diff --git a/tests/services/test_settings_manager.py b/tests/services/test_settings_manager.py index 7b14db91..c5561520 100644 --- a/tests/services/test_settings_manager.py +++ b/tests/services/test_settings_manager.py @@ -359,6 +359,25 @@ def test_auto_set_default_roots_keeps_valid_extra_values(manager): assert manager.get("default_embedding_root") == "/extra-embeddings" +def test_auto_set_default_roots_keeps_valid_extra_values_with_windows_slash_mismatch(manager): + manager.settings["default_lora_root"] = "U:/Lora7/Loras" + manager.settings["default_checkpoint_root"] = "U:/Lora7/Models" + + manager.settings["folder_paths"] = { + "loras": ["R:/ComfyUI/models/loras"], + "checkpoints": ["R:/ComfyUI/models/checkpoints"], + } + manager.settings["extra_folder_paths"] = { + "loras": ["U:\\Lora7\\Loras"], + "checkpoints": ["U:\\Lora7\\Models"], + } + + manager._auto_set_default_roots() + + assert manager.get("default_lora_root") == "U:/Lora7/Loras" + assert manager.get("default_checkpoint_root") == "U:/Lora7/Models" + + def test_auto_set_default_roots_falls_back_to_extra_when_primary_missing(manager): manager.settings["default_lora_root"] = "" manager.settings["folder_paths"] = {"loras": []}