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:
@@ -13,86 +13,76 @@ from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from src import catalog as _catalog_mod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
|
||||
STATIC_DIR = Path(__file__).parent.parent / "static"
|
||||
CATALOG_PATH = TEMPLATES_DIR / "catalog.yaml"
|
||||
|
||||
# 카테고리 검색 순서
|
||||
BLOCK_CATEGORIES = ["headers", "cards", "tables", "visuals", "emphasis", "media"]
|
||||
|
||||
# catalog.yaml에서 id → template 경로 매핑 로드 (BF-10: mtime 체크로 자동 갱신)
|
||||
# id → template 경로 매핑 (IMP-27: src.catalog 공유 로더 위임, renderer-local projection cache)
|
||||
_CATALOG_MAP: dict[str, str] | None = None
|
||||
_CATALOG_MTIME: float = 0.0
|
||||
_CATALOG_MAP_MTIME: float = 0.0
|
||||
|
||||
# Phase R: variant별 template 경로 캐시 (renderer-local projection)
|
||||
_CATALOG_VARIANT_MAP: dict[str, str] | None = None
|
||||
_CATALOG_VARIANT_MAP_MTIME: float = 0.0
|
||||
|
||||
|
||||
def _load_catalog_map() -> dict[str, str]:
|
||||
"""catalog.yaml에서 블록 id → template 경로 매핑을 로드한다.
|
||||
"""블록 id → template 경로 projection (IMP-27: src.catalog 공유 로더 위임).
|
||||
|
||||
파일 수정시간(mtime)을 확인하여, 변경 시에만 재로드한다.
|
||||
catalog 파일 읽기와 mtime 캐싱은 ``src.catalog`` 가 단독 소유. 본 함수는
|
||||
그 결과를 ``id → template`` 형태로 변환한 renderer-local projection 캐시만
|
||||
유지하며, projection 무효화는 ``src.catalog.get_catalog_mtime()`` 키잉.
|
||||
"""
|
||||
global _CATALOG_MAP, _CATALOG_MTIME
|
||||
global _CATALOG_MAP, _CATALOG_MAP_MTIME
|
||||
|
||||
current_mtime = CATALOG_PATH.stat().st_mtime if CATALOG_PATH.exists() else 0.0
|
||||
blocks = _catalog_mod.load_blocks()
|
||||
current_mtime = _catalog_mod.get_catalog_mtime()
|
||||
|
||||
if _CATALOG_MAP is not None and _CATALOG_MTIME == current_mtime:
|
||||
return _CATALOG_MAP # 파일 변경 없음 → 캐시 재사용
|
||||
if _CATALOG_MAP is not None and _CATALOG_MAP_MTIME == current_mtime:
|
||||
return _CATALOG_MAP
|
||||
|
||||
# 변경 감지 또는 첫 로드 → 새로 읽기
|
||||
_CATALOG_MTIME = current_mtime
|
||||
_CATALOG_MAP_MTIME = current_mtime
|
||||
_CATALOG_MAP = {}
|
||||
if CATALOG_PATH.exists():
|
||||
try:
|
||||
with open(CATALOG_PATH, encoding="utf-8") as f:
|
||||
catalog = yaml.safe_load(f)
|
||||
for block in catalog.get("blocks", []):
|
||||
block_id = block.get("id", "")
|
||||
template = block.get("template", "")
|
||||
if block_id and template:
|
||||
_CATALOG_MAP[block_id] = template
|
||||
logger.info(f"catalog.yaml 로드: {len(_CATALOG_MAP)}개 블록 매핑")
|
||||
except Exception as e:
|
||||
logger.warning(f"catalog.yaml 로드 실패: {e}")
|
||||
else:
|
||||
logger.warning(f"catalog.yaml 미발견: {CATALOG_PATH}")
|
||||
for block in blocks:
|
||||
block_id = block.get("id", "")
|
||||
template = block.get("template", "")
|
||||
if block_id and template:
|
||||
_CATALOG_MAP[block_id] = template
|
||||
logger.info(f"catalog.yaml 로드: {len(_CATALOG_MAP)}개 블록 매핑")
|
||||
|
||||
return _CATALOG_MAP
|
||||
|
||||
|
||||
# Phase R: variant별 template 경로 캐시
|
||||
_CATALOG_VARIANT_MAP: dict[str, str] | None = None
|
||||
|
||||
|
||||
def _load_catalog_map_with_variants() -> dict[str, str]:
|
||||
"""catalog.yaml에서 variant별 template 경로 매핑을 로드한다.
|
||||
"""variant별 template 경로 projection (IMP-27: src.catalog 공유 로더 위임).
|
||||
|
||||
키: "block_id--variant_id" → 값: template 경로
|
||||
키: "block_id--variant_id" → 값: template 경로.
|
||||
"""
|
||||
global _CATALOG_VARIANT_MAP
|
||||
global _CATALOG_VARIANT_MAP, _CATALOG_VARIANT_MAP_MTIME
|
||||
|
||||
# _load_catalog_map이 이미 캐시 관리하므로 같은 mtime 사용
|
||||
_load_catalog_map() # 캐시 갱신 보장
|
||||
blocks = _catalog_mod.load_blocks()
|
||||
current_mtime = _catalog_mod.get_catalog_mtime()
|
||||
|
||||
if _CATALOG_VARIANT_MAP is not None and _CATALOG_MTIME == (CATALOG_PATH.stat().st_mtime if CATALOG_PATH.exists() else 0.0):
|
||||
if _CATALOG_VARIANT_MAP is not None and _CATALOG_VARIANT_MAP_MTIME == current_mtime:
|
||||
return _CATALOG_VARIANT_MAP
|
||||
|
||||
_CATALOG_VARIANT_MAP_MTIME = current_mtime
|
||||
_CATALOG_VARIANT_MAP = {}
|
||||
if CATALOG_PATH.exists():
|
||||
try:
|
||||
with open(CATALOG_PATH, encoding="utf-8") as f:
|
||||
catalog = yaml.safe_load(f)
|
||||
for block in catalog.get("blocks", []):
|
||||
block_id = block.get("id", "")
|
||||
for variant in block.get("variants", []):
|
||||
vid = variant.get("id", "default")
|
||||
vtemplate = variant.get("template", "")
|
||||
if vid != "default" and vtemplate:
|
||||
_CATALOG_VARIANT_MAP[f"{block_id}--{vid}"] = vtemplate
|
||||
except Exception as e:
|
||||
logger.warning(f"catalog variant 로드 실패: {e}")
|
||||
for block in blocks:
|
||||
block_id = block.get("id", "")
|
||||
for variant in block.get("variants", []):
|
||||
vid = variant.get("id", "default")
|
||||
vtemplate = variant.get("template", "")
|
||||
if vid != "default" and vtemplate:
|
||||
_CATALOG_VARIANT_MAP[f"{block_id}--{vid}"] = vtemplate
|
||||
|
||||
return _CATALOG_VARIANT_MAP
|
||||
|
||||
|
||||
Reference in New Issue
Block a user