mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
fix(cache): harden metadata defaults
This commit is contained in:
@@ -32,6 +32,7 @@ class ModelCache:
|
|||||||
# Cache for last sort: (sort_key, order) -> sorted list
|
# Cache for last sort: (sort_key, order) -> sorted list
|
||||||
self._last_sort: Tuple[str, str] = (None, None)
|
self._last_sort: Tuple[str, str] = (None, None)
|
||||||
self._last_sorted_data: List[Dict] = []
|
self._last_sorted_data: List[Dict] = []
|
||||||
|
self._normalize_raw_data()
|
||||||
self.name_display_mode = self._normalize_display_mode(self.name_display_mode)
|
self.name_display_mode = self._normalize_display_mode(self.name_display_mode)
|
||||||
# Default sort on init
|
# Default sort on init
|
||||||
asyncio.create_task(self.resort())
|
asyncio.create_task(self.resort())
|
||||||
@@ -43,20 +44,43 @@ class ModelCache:
|
|||||||
return value
|
return value
|
||||||
return "model_name"
|
return "model_name"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ensure_string(value: Any) -> str:
|
||||||
|
"""Return a safe string representation for metadata fields."""
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def _normalize_item(self, item: Dict) -> None:
|
||||||
|
"""Ensure core metadata fields are present and string typed."""
|
||||||
|
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
for field in ("model_name", "file_name", "folder"):
|
||||||
|
item[field] = self._ensure_string(item.get(field))
|
||||||
|
|
||||||
|
def _normalize_raw_data(self) -> None:
|
||||||
|
"""Normalize every cached entry before it is consumed."""
|
||||||
|
|
||||||
|
for item in self.raw_data:
|
||||||
|
self._normalize_item(item)
|
||||||
|
|
||||||
def _get_display_name(self, item: Dict) -> str:
|
def _get_display_name(self, item: Dict) -> str:
|
||||||
"""Return the value used for name-based sorting based on display settings."""
|
"""Return the value used for name-based sorting based on display settings."""
|
||||||
|
|
||||||
if self.name_display_mode == "file_name":
|
if self.name_display_mode == "file_name":
|
||||||
primary = item.get("file_name", "")
|
primary = self._ensure_string(item.get("file_name"))
|
||||||
fallback = item.get("model_name", "")
|
fallback = self._ensure_string(item.get("model_name"))
|
||||||
else:
|
else:
|
||||||
primary = item.get("model_name", "")
|
primary = self._ensure_string(item.get("model_name"))
|
||||||
fallback = item.get("file_name", "")
|
fallback = self._ensure_string(item.get("file_name"))
|
||||||
|
|
||||||
candidate = primary or fallback or ""
|
candidate = primary or fallback
|
||||||
if isinstance(candidate, str):
|
return candidate or ""
|
||||||
return candidate
|
|
||||||
return str(candidate)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_version_id(value: Any) -> Optional[int]:
|
def _normalize_version_id(value: Any) -> Optional[int]:
|
||||||
@@ -119,7 +143,11 @@ class ModelCache:
|
|||||||
# Update folder list
|
# Update folder list
|
||||||
# else: do nothing
|
# else: do nothing
|
||||||
|
|
||||||
all_folders = set(l['folder'] for l in self.raw_data)
|
all_folders = {
|
||||||
|
self._ensure_string(item.get('folder'))
|
||||||
|
for item in self.raw_data
|
||||||
|
if isinstance(item, dict)
|
||||||
|
}
|
||||||
self.folders = sorted(list(all_folders), key=lambda x: x.lower())
|
self.folders = sorted(list(all_folders), key=lambda x: x.lower())
|
||||||
self.rebuild_version_index()
|
self.rebuild_version_index()
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ class SearchStrategy:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def _matches(self, candidate: str, search_term: str, search_lower: str, fuzzy: bool) -> bool:
|
def _matches(self, candidate: str, search_term: str, search_lower: str, fuzzy: bool) -> bool:
|
||||||
|
if not isinstance(candidate, str):
|
||||||
|
candidate = "" if candidate is None else str(candidate)
|
||||||
|
|
||||||
if not candidate:
|
if not candidate:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
47
tests/services/test_model_cache_resilience.py
Normal file
47
tests/services/test_model_cache_resilience.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from py.services.model_cache import ModelCache
|
||||||
|
from py.services.model_query import SearchStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_model_cache_handles_missing_string_fields():
|
||||||
|
cache = ModelCache(
|
||||||
|
raw_data=[
|
||||||
|
{
|
||||||
|
"file_path": "/models/example.safetensors",
|
||||||
|
"file_name": None,
|
||||||
|
"model_name": None,
|
||||||
|
"folder": None,
|
||||||
|
"size": 0,
|
||||||
|
"modified": 0.0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
folders=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(0) # allow background resort task to run
|
||||||
|
sorted_data = await cache.get_sorted_data("name", "asc")
|
||||||
|
|
||||||
|
assert sorted_data[0]["model_name"] == ""
|
||||||
|
assert sorted_data[0]["file_name"] == ""
|
||||||
|
assert cache.folders == [""]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_strategy_handles_non_string_candidates():
|
||||||
|
strategy = SearchStrategy()
|
||||||
|
options = strategy.normalize_options(None)
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"file_name": "example.safetensors",
|
||||||
|
"model_name": None,
|
||||||
|
"tags": ["test"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
results = strategy.apply(data, "example", options)
|
||||||
|
|
||||||
|
assert data[0] in results
|
||||||
Reference in New Issue
Block a user