refactor(#27): IMP-27 K5 catalog loader + _get_block_by_id cleanup
Consolidate three duplicated catalog readers and two _get_block_by_id implementations behind a single shared module (src/catalog.py) that owns file-read + mtime cache. All caller signatures and return contracts remain byte-identical. Units: - u1 NEW src/catalog.py (76 lines): load_root_catalog / load_blocks / get_block_by_id / get_catalog_mtime as the sole file-read + mtime-cache owner. - u2 src/block_reference.py: _load_catalog delegates to load_blocks (list[dict] preserved); _get_block_by_id (no-arg) delegates to catalog.get_block_by_id. Module-level _catalog_cache removed. - u3 src/block_selector.py: load_catalog delegates to load_root_catalog (root dict preserved); _get_block_by_id (catalog-injected sig preserved) delegates to catalog.get_block_by_id. Module-level _catalog_cache / _catalog_mtime / CATALOG_PATH removed. - u4 src/renderer.py: _load_catalog_map and _load_catalog_map_with_variants consume catalog.load_blocks; renderer projection caches kept local but keyed via catalog.get_catalog_mtime(). Per-projection invalidation keys (_CATALOG_MAP_MTIME / _CATALOG_VARIANT_MAP_MTIME) introduced. import yaml, CATALOG_PATH, legacy _CATALOG_MTIME removed. - tests NEW tests/test_catalog_shared_loader.py (421 lines, 23 cases): shared loader + 3 wrappers covering single file-read, contract preservation, signature preservation, shared cache, private state absence, mtime invalidation propagation to renderer projections. Verification: - pytest tests/test_catalog_shared_loader.py -v: 23/23 PASS in 0.13s. - pytest tests/ -q --ignore=tests/matching: 365/365 PASS in 38.10s. - src/fit_verifier.py, src/space_allocator.py, src/pipeline.py and templates/catalog.yaml unchanged (git diff empty). Out of scope: - catalog.yaml schema/path unchanged. - Catalog direct-read call sites in fit_verifier / space_allocator / pipeline left for a separate follow-up axis. - Phase Z 22-step runtime, frame_selection, light_edit/restructure flows untouched. Refs: IMP-27 (gitea #27), INSIGHT-MAP §5 K5, PHASE-Q-AUDIT §2.10
This commit is contained in:
76
src/catalog.py
Normal file
76
src/catalog.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""IMP-27: Shared catalog.yaml loader (single file-read + mtime cache).
|
||||
|
||||
Phase Q evolution 중 block_reference, block_selector, renderer 가 각각 templates/
|
||||
catalog.yaml 을 읽고 mtime 캐시하던 중복을 한 곳으로 통합한다. call-site
|
||||
signature 는 그대로 유지되며, 각 wrapper 는 본 모듈의 결과를 자신이 약속하는
|
||||
형태(list[dict] / root dict / id→path projection)로 변환만 수행한다.
|
||||
|
||||
Functions:
|
||||
load_root_catalog() -> dict : raw catalog dict (matches block_selector contract)
|
||||
load_blocks() -> list[dict] : root_catalog.get("blocks", []) projection
|
||||
get_block_by_id(block_id, catalog=None) -> dict | None
|
||||
get_catalog_mtime() -> float : current cached mtime (renderer projection key)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CATALOG_PATH = Path(__file__).parent.parent / "templates" / "catalog.yaml"
|
||||
|
||||
_catalog_cache: dict | None = None
|
||||
_catalog_mtime: float = 0.0
|
||||
|
||||
|
||||
def load_root_catalog() -> dict:
|
||||
"""Load templates/catalog.yaml as root dict, with mtime caching.
|
||||
|
||||
Missing file → logs warning and returns ``{"blocks": []}`` (matches the
|
||||
pre-IMP-27 behavior of block_selector.load_catalog and renderer._load_catalog_map).
|
||||
"""
|
||||
global _catalog_cache, _catalog_mtime
|
||||
|
||||
if not CATALOG_PATH.exists():
|
||||
logger.warning(f"catalog.yaml 미발견: {CATALOG_PATH}")
|
||||
return {"blocks": []}
|
||||
|
||||
current_mtime = CATALOG_PATH.stat().st_mtime
|
||||
if _catalog_cache is not None and current_mtime == _catalog_mtime:
|
||||
return _catalog_cache
|
||||
|
||||
with open(CATALOG_PATH, encoding="utf-8") as f:
|
||||
_catalog_cache = yaml.safe_load(f)
|
||||
_catalog_mtime = current_mtime
|
||||
|
||||
block_count = len((_catalog_cache or {}).get("blocks", []))
|
||||
logger.info(f"[catalog] load: {block_count} blocks")
|
||||
return _catalog_cache
|
||||
|
||||
|
||||
def load_blocks() -> list[dict]:
|
||||
"""Return blocks list (= root_catalog.get('blocks', []))."""
|
||||
return load_root_catalog().get("blocks", [])
|
||||
|
||||
|
||||
def get_block_by_id(block_id: str, catalog: dict | None = None) -> dict | None:
|
||||
"""Locate a block entry by id.
|
||||
|
||||
``catalog=None`` → uses shared loader. caller-supplied catalog dict is
|
||||
accepted as-is so the existing block_selector contract (catalog-injected)
|
||||
keeps working unchanged.
|
||||
"""
|
||||
if catalog is None:
|
||||
catalog = load_root_catalog()
|
||||
for block in catalog.get("blocks", []):
|
||||
if block.get("id") == block_id:
|
||||
return block
|
||||
return None
|
||||
|
||||
|
||||
def get_catalog_mtime() -> float:
|
||||
"""Current cached mtime (renderer projection caches key off this)."""
|
||||
return _catalog_mtime
|
||||
Reference in New Issue
Block a user