계획서 p.24 가 No.3/4/7 의 평가환경을 python 3.9 로 못박았는데, 실제로는
성능지표 평가 스크립트 3종이 전부 3.9 에서 임포트조차 되지 않았다.
python:3.9-slim 컨테이너에서 재현·수정·재검증했다.
원인: config.py 와 schemas.py 가 `from __future__ import annotations` 없이
`str | None` (PEP 604) 을 썼다. 3.10 부터의 문법이라 클래스 본문이 실행되는
순간 인터프리터가 평가하며 TypeError 로 죽는다. 이 두 파일을 타고 detector /
extractor / clustering / taxonomy / jobs.store / routes / main 이 연쇄로 깨졌다.
No.7 eval_rouge.py → summarizer → core.config
No.3 eval_metadata_f1.py → extractor → api.schemas
No.4 evaluate_pairs.py → detector → api.schemas
개발환경이 3.14 라 로컬에서는 전혀 드러나지 않았다. 정답셋을 받아 측정하는
시점에 발견했다면 일정이 밀렸을 문제다.
수정은 두 가지가 모두 필요하다. future 임포트만 넣으면 pydantic 이 문자열
'str | None' 을 해석하지 못하고("Unable to evaluate type annotation"),
eval_type_backport 만 넣으면 인터프리터가 먼저 죽는다.
검증: 3.9 에서 eval_rouge.py / eval_metadata_f1.py 가 정상 실행되고 수치가
3.14 와 동일하다(ROUGE-1 recall 0.5778). evaluate_pairs.py 와 app.main 임포트도
통과. 3.14 회귀 없음.
재발 방지로 tests/test_py39_compat.py 를 추가했다. Pydantic 모델을 정의하는
모듈에 future 임포트가 있는지 AST 로 검사하므로 3.14 에서도 회귀를 잡는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
3.3 KiB
Python
85 lines
3.3 KiB
Python
"""공인인증 평가환경(python 3.9) 호환성 가드.
|
|
|
|
왜 필요한가:
|
|
계획서 p.24 는 No.3/4/7 의 평가환경을 **python 3.9** 로 못박았다. 그런데
|
|
`str | None` (PEP 604) 은 3.10 부터의 문법이라, `from __future__ import
|
|
annotations` 없이 쓰면 클래스 본문이 실행되는 순간 인터프리터가 평가해
|
|
TypeError 로 죽는다. Pydantic 모델은 어노테이션이 런타임에 해석되므로
|
|
특히 위험하다.
|
|
|
|
개발 환경이 3.14 라 이 문제는 로컬에서 절대 드러나지 않는다. 실제로
|
|
`app/core/config.py` 와 `app/api/schemas.py` 가 이 상태였고, 그 두 파일을
|
|
타고 detector·routes·main 과 **성능지표 평가 스크립트 3종이 전부** 3.9 에서
|
|
임포트 불가였다(2026-08-19 확인·수정).
|
|
|
|
이 테스트는 3.14 에서도 그 회귀를 잡는다. 문법이 아니라 소스에 선언이
|
|
있는지를 본다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PYDANTIC_BASES = {"BaseModel", "BaseSettings"}
|
|
|
|
|
|
def _has_future_annotations(tree: ast.Module) -> bool:
|
|
for node in tree.body:
|
|
if isinstance(node, ast.ImportFrom) and node.module == "__future__":
|
|
if any(alias.name == "annotations" for alias in node.names):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _defines_pydantic_model(tree: ast.Module) -> bool:
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ClassDef):
|
|
for base in node.bases:
|
|
name = base.id if isinstance(base, ast.Name) else getattr(base, "attr", None)
|
|
if name in PYDANTIC_BASES:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _pydantic_modules() -> list[Path]:
|
|
found = []
|
|
for path in sorted((ROOT / "app").rglob("*.py")):
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
if _defines_pydantic_model(tree):
|
|
found.append(path)
|
|
return found
|
|
|
|
|
|
def test_pydantic_modules_exist():
|
|
"""탐지 자체가 망가지면 아래 테스트가 조용히 무력화되므로 먼저 확인한다."""
|
|
assert _pydantic_modules(), "Pydantic 모델 모듈을 하나도 못 찾았습니다."
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path", _pydantic_modules(), ids=lambda p: str(p.relative_to(ROOT))
|
|
)
|
|
def test_pydantic_module_defers_annotations(path: Path):
|
|
"""Pydantic 모델을 정의하는 모듈은 어노테이션 평가를 미뤄야 한다."""
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
assert _has_future_annotations(tree), (
|
|
f"{path.relative_to(ROOT)} 에 `from __future__ import annotations` 가 "
|
|
"없습니다. PEP 604(`X | None`)를 쓰면 평가환경(python 3.9)에서 임포트가 "
|
|
"실패합니다."
|
|
)
|
|
|
|
|
|
def test_eval_type_backport_pinned_for_py39():
|
|
"""3.9 에서는 __future__ 만으로 부족하고 pydantic 이 백포트를 요구한다."""
|
|
req = (ROOT / "requirements.txt").read_text(encoding="utf-8")
|
|
assert "eval_type_backport" in req, (
|
|
"requirements.txt 에 eval_type_backport 가 없습니다. python 3.9 에서 "
|
|
"pydantic 이 문자열 어노테이션 'str | None' 을 해석하지 못합니다."
|
|
)
|
|
assert 'python_version < "3.10"' in req, (
|
|
"eval_type_backport 는 3.9 전용입니다. 환경 마커를 붙이세요."
|
|
)
|