AutoPercenty3/check_setup.py

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)