fix(cache): harden metadata defaults

This commit is contained in:
pixelpaws
2025-10-18 21:19:09 +08:00
parent 4e4ea85cc3
commit e4d58d0f60
3 changed files with 87 additions and 9 deletions

View File

@@ -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()

View File

@@ -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

View 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