319 lines
12 KiB
Python
319 lines
12 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
check_setup.py - 빌드 전 모듈 커버리지 검증 도구
|
|
|
|
프로젝트 루트의 모든 .py 파일을 스캔해서,
|
|
setup_ob2.py 의 CYTHON_MODULES / BASE_INCLUDES / BASE_EXCLUDES_RAW 에
|
|
포함되지 않은 "로컬 모듈"이 있으면 경고합니다.
|
|
|
|
사용법:
|
|
python check_setup.py # 검사만 (경고 있어도 계속)
|
|
python check_setup.py --strict # 누락 발견 시 exit(1) (빌드 중단)
|
|
python check_setup.py --fix # 누락 모듈을 CYTHON_MODULES 추가 안내 출력
|
|
"""
|
|
|
|
import ast
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Set, List, Tuple
|
|
|
|
# ============================================================
|
|
# 설정
|
|
# ============================================================
|
|
|
|
ROOT = Path(__file__).parent.resolve()
|
|
SETUP_FILE = ROOT / "setup_ob2.py"
|
|
|
|
# 검사 대상에서 제외할 디렉토리
|
|
SKIP_DIRS: Set[str] = {
|
|
# 빌드 결과물
|
|
"build", "dist", ".git", "__pycache__",
|
|
"Lib", "lib", "Scripts", "Include",
|
|
".venv", "venv", ".mypy_cache", ".pytest_cache",
|
|
"node_modules",
|
|
# 별도 관리 컴포넌트
|
|
"updateManager",
|
|
# 제3자 / OCR / AI 모델 라이브러리
|
|
"ppocr", "PP_Models", "paddle", "paddleocr",
|
|
# 개발용 유틸리티 및 레거시
|
|
"tools", "old",
|
|
# 로그 분석 / 테스트
|
|
"loggerModules", "logs",
|
|
# 사용안하는 모듈
|
|
"ai_modules", "ocr_backends", "limited_contents",
|
|
# pyu-data 등 파일명에 점이 있는 특수 디렉토리
|
|
"pyu-data",
|
|
}
|
|
|
|
# 검사 대상에서 제외할 루트 파일 (빌드 전용 스크립트 등)
|
|
SKIP_ROOT_FILES: Set[str] = {
|
|
# 빌드 관련
|
|
"setup_ob2.py", "setup_clean.py", "setup.py",
|
|
"generate_iss.py", "check_setup.py",
|
|
"conftest.py",
|
|
# 일회성 개발/테스트 스크립트
|
|
"vertexAI.py", "update_ai_client.py", "upload_categories.py",
|
|
"user_manual_dialog.py", "whale_new.py", "whale_test.py",
|
|
"whale_translator.py", "whale_translator2.py", "widgetmap.py",
|
|
"title.py", "toggleSwitch_bak.py", "release_note_dialog.py",
|
|
"recovery_log.py", "settings_dialog.py", "signup_dialog.py",
|
|
"simple_test.py", "simple_translate.py",
|
|
"setup1.py", "setup_non_img.py", "setup_onnx.py", "setup_paddle.py",
|
|
}
|
|
|
|
# test_ 로 시작하는 루트 파일은 테스트 스크립트이므로 자동 제외
|
|
SKIP_ROOT_PREFIX: Set[str] = {"test_"}
|
|
|
|
# 알려진 표준 라이브러리 최상위 패키지 (Python 3.10+ 내장, 이하 버전은 추정)
|
|
try:
|
|
_STDLIB: Set[str] = sys.stdlib_module_names # type: ignore[attr-defined]
|
|
except AttributeError:
|
|
_STDLIB: Set[str] = set(sys.builtin_module_names) | {
|
|
"abc", "ast", "asyncio", "atexit", "base64", "collections",
|
|
"concurrent", "contextlib", "copy", "csv", "dataclasses",
|
|
"datetime", "decimal", "enum", "functools", "glob", "hashlib",
|
|
"http", "importlib", "inspect", "io", "itertools", "json",
|
|
"logging", "math", "multiprocessing", "operator", "os",
|
|
"pathlib", "pickle", "platform", "pprint", "queue", "random",
|
|
"re", "shutil", "signal", "socket", "sqlite3", "string",
|
|
"struct", "subprocess", "sys", "tempfile", "threading",
|
|
"time", "traceback", "typing", "unittest", "urllib", "uuid",
|
|
"warnings", "weakref", "xml", "zipfile", "zlib",
|
|
}
|
|
|
|
# 알려진 서드파티 최상위 패키지 (로컬 모듈로 오탐되지 않도록 제외)
|
|
_KNOWN_THIRD_PARTY: Set[str] = {
|
|
"PySide6", "shiboken6", "PIL", "cv2", "numpy", "pandas",
|
|
"requests", "httpx", "curl_cffi", "supabase", "gotrue",
|
|
"openai", "playwright", "psutil", "pyperclip", "comtypes",
|
|
"win32api", "win32com", "win32event", "win32gui", "win32con",
|
|
"winerror", "pythoncom", "pydantic", "pydantic_core", "greenlet",
|
|
"translatepy", "bs4", "markdown", "packaging", "tqdm",
|
|
"Cython", "cx_Freeze", "setuptools", "pkg_resources",
|
|
"google", "anthropic", "aiohttp", "httpcore",
|
|
"storage3", "postgrest", "supafunc", "realtime",
|
|
"ctypes", "sleep_control",
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# setup_ob2.py AST 파싱 헬퍼
|
|
# ============================================================
|
|
|
|
def _parse_string_list_from_ast(tree: ast.Module, var_name: str) -> List[str]:
|
|
"""AST 트리에서 특정 변수에 할당된 문자열 리스트를 추출합니다."""
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.Assign):
|
|
continue
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name) and target.id == var_name:
|
|
if isinstance(node.value, ast.List):
|
|
result = []
|
|
for elt in node.value.elts:
|
|
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
result.append(elt.value)
|
|
return result
|
|
return []
|
|
|
|
|
|
def load_setup_config() -> Tuple[Set[str], Set[str], Set[str]]:
|
|
"""
|
|
setup_ob2.py 를 AST 로 파싱해서
|
|
(cython_module_names, base_include_names, base_exclude_names) 를 반환합니다.
|
|
모두 점 표기법(dot notation) 모듈명 집합입니다.
|
|
"""
|
|
if not SETUP_FILE.exists():
|
|
print("[check_setup] ERROR: {} 를 찾을 수 없습니다.".format(SETUP_FILE))
|
|
sys.exit(1)
|
|
|
|
src = SETUP_FILE.read_text(encoding="utf-8")
|
|
tree = ast.parse(src, filename=str(SETUP_FILE))
|
|
|
|
# CYTHON_MODULES: 경로 형식("src/contents/option") -> 점 표기법
|
|
raw_cython = _parse_string_list_from_ast(tree, "CYTHON_MODULES")
|
|
cython_names: Set[str] = set()
|
|
for p in raw_cython:
|
|
clean = os.path.splitext(p)[0].replace("/", ".").replace("\\", ".")
|
|
cython_names.add(clean)
|
|
|
|
# BASE_INCLUDES: 이미 점 표기법
|
|
raw_includes = _parse_string_list_from_ast(tree, "BASE_INCLUDES")
|
|
include_names: Set[str] = set(raw_includes)
|
|
|
|
# BASE_EXCLUDES_RAW: 의도적으로 제외하는 로컬 모듈
|
|
raw_excludes = _parse_string_list_from_ast(tree, "BASE_EXCLUDES_RAW")
|
|
exclude_names: Set[str] = set(raw_excludes)
|
|
|
|
return cython_names, include_names, exclude_names
|
|
|
|
|
|
# ============================================================
|
|
# 프로젝트 파일 스캔
|
|
# ============================================================
|
|
|
|
def get_project_py_files() -> List[Path]:
|
|
"""프로젝트 루트의 모든 .py 파일을 반환합니다. (제외 디렉토리 필터링 포함)"""
|
|
result: List[Path] = []
|
|
for path in sorted(ROOT.rglob("*.py")):
|
|
rel = path.relative_to(ROOT)
|
|
parts = rel.parts
|
|
|
|
# 제외 디렉토리 체크
|
|
if any(part in SKIP_DIRS for part in parts[:-1]):
|
|
continue
|
|
|
|
# 루트 파일 제외 (정확한 파일명)
|
|
if len(parts) == 1 and path.name in SKIP_ROOT_FILES:
|
|
continue
|
|
|
|
# 루트 파일 제외 (prefix 패턴, e.g. test_*)
|
|
if len(parts) == 1 and any(path.name.startswith(p) for p in SKIP_ROOT_PREFIX):
|
|
continue
|
|
|
|
result.append(path)
|
|
return result
|
|
|
|
|
|
def path_to_module_name(path: Path) -> str:
|
|
"""
|
|
절대 경로 -> 점 표기법 모듈명.
|
|
예: ROOT/src/contents/option.py -> src.contents.option
|
|
ROOT/src/contents/__init__.py -> src.contents
|
|
"""
|
|
rel = path.relative_to(ROOT)
|
|
parts = list(rel.parts)
|
|
|
|
if parts[-1] == "__init__.py":
|
|
parts = parts[:-1]
|
|
else:
|
|
parts[-1] = parts[-1][:-3] # .py 제거
|
|
|
|
return ".".join(parts)
|
|
|
|
|
|
# ============================================================
|
|
# 커버리지 판정
|
|
# ============================================================
|
|
|
|
def is_covered(module_name: str, cython: Set[str], includes: Set[str], excludes: Set[str]) -> bool:
|
|
"""
|
|
모듈이 setup_ob2.py 에 의해 커버되는지 확인합니다.
|
|
|
|
커버 조건 (하나라도 해당 시 OK):
|
|
1) CYTHON_MODULES 에 포함
|
|
2) BASE_INCLUDES 에 포함
|
|
3) BASE_EXCLUDES_RAW 에 포함 (의도적 제외 = 문제 없음)
|
|
4) 상위 패키지가 위 중 하나에 포함
|
|
5) 표준 라이브러리 or 알려진 서드파티
|
|
"""
|
|
all_covered = cython | includes | excludes
|
|
|
|
parts = module_name.split(".")
|
|
for depth in range(len(parts), 0, -1):
|
|
candidate = ".".join(parts[:depth])
|
|
if candidate in all_covered:
|
|
return True
|
|
|
|
top = parts[0]
|
|
if top in _STDLIB or top in _KNOWN_THIRD_PARTY:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_only_third_party(module_name: str) -> bool:
|
|
"""최상위 패키지가 서드파티 or 표준 라이브러리인지 확인합니다."""
|
|
top = module_name.split(".")[0]
|
|
return top in _STDLIB or top in _KNOWN_THIRD_PARTY
|
|
|
|
|
|
# ============================================================
|
|
# 메인 검사 함수
|
|
# ============================================================
|
|
|
|
def run_check(strict: bool = False, show_fix: bool = False) -> List[Tuple[str, Path]]:
|
|
"""
|
|
누락 모듈을 검사하고 결과를 반환합니다.
|
|
|
|
Args:
|
|
strict: True 이면 누락 발견 시 sys.exit(1)
|
|
show_fix: True 이면 CYTHON_MODULES / BASE_INCLUDES 추가 안내 출력
|
|
|
|
Returns:
|
|
[(module_name, file_path), ...] 누락 목록
|
|
"""
|
|
print("\n" + "=" * 60)
|
|
print(" [check_setup] 빌드 전 모듈 커버리지 검사 시작")
|
|
print("=" * 60)
|
|
|
|
# 1. setup_ob2.py 파싱
|
|
cython_names, include_names, exclude_names = load_setup_config()
|
|
print(" CYTHON_MODULES : {}개".format(len(cython_names)))
|
|
print(" BASE_INCLUDES : {}개".format(len(include_names)))
|
|
print(" BASE_EXCLUDES : {}개".format(len(exclude_names)))
|
|
|
|
# 2. 프로젝트 파일 스캔
|
|
py_files = get_project_py_files()
|
|
print(" 스캔 대상 파일 : {}개\n".format(len(py_files)))
|
|
|
|
# 3. 커버리지 판정
|
|
uncovered: List[Tuple[str, Path]] = []
|
|
for path in py_files:
|
|
module = path_to_module_name(path)
|
|
|
|
if is_only_third_party(module):
|
|
continue
|
|
|
|
if not is_covered(module, cython_names, include_names, exclude_names):
|
|
uncovered.append((module, path))
|
|
|
|
# 4. 결과 출력
|
|
if not uncovered:
|
|
print(" [OK] 모든 모듈이 setup_ob2.py 에 등록되어 있습니다.\n")
|
|
return []
|
|
|
|
print(" [WARN] 누락된 모듈 {}개 발견!\n".format(len(uncovered)))
|
|
print(" {:<50} 파일 경로".format("모듈명"))
|
|
print(" {} {}".format("-" * 50, "-" * 30))
|
|
for module, path in uncovered:
|
|
rel_path = path.relative_to(ROOT)
|
|
print(" {:<50} {}".format(module, rel_path))
|
|
|
|
# 5. --fix 안내
|
|
if show_fix:
|
|
print("\n" + "-" * 60)
|
|
print(" [추천] CYTHON_MODULES 에 추가할 항목 (경로 형식):")
|
|
print("-" * 60)
|
|
for module, path in uncovered:
|
|
rel_path = path.relative_to(ROOT).as_posix().replace(".py", "")
|
|
print(' "{}",'.format(rel_path))
|
|
|
|
print("\n 또는 BASE_INCLUDES 에 추가할 항목 (점 표기법):")
|
|
print("-" * 60)
|
|
for module, _ in uncovered:
|
|
print(' "{}",'.format(module))
|
|
|
|
print()
|
|
|
|
# 6. strict 모드: 빌드 중단
|
|
if strict:
|
|
print(" [ERROR] --strict 모드: 누락 모듈이 있어 빌드를 중단합니다.")
|
|
print(" 위 모듈을 setup_ob2.py 에 추가한 후 다시 빌드하세요.\n")
|
|
sys.exit(1)
|
|
else:
|
|
print(" [WARN] 경고만 표시합니다. (빌드는 계속)")
|
|
print(" --strict 옵션으로 실행하면 빌드를 자동 중단할 수 있습니다.\n")
|
|
|
|
return uncovered
|
|
|
|
|
|
# ============================================================
|
|
# 진입점
|
|
# ============================================================
|
|
|
|
if __name__ == "__main__":
|
|
strict_mode = "--strict" in sys.argv
|
|
fix_mode = "--fix" in sys.argv
|
|
run_check(strict=strict_mode, show_fix=fix_mode)
|