test_imports.py
1 """import検証テスト 2 3 全モジュールがimportできること、DB/LLM系のimportが含まれていないことを確認する。 4 """ 5 6 from __future__ import annotations 7 8 import ast 9 import importlib 10 import pathlib 11 from typing import Any 12 13 import pytest 14 15 # mureo-core パッケージルート 16 _MUREO_ROOT = pathlib.Path(__file__).resolve().parent.parent / "mureo" 17 18 # DB/LLM系の禁止importパターン 19 _FORBIDDEN_MODULES: frozenset[str] = frozenset( 20 { 21 "sqlalchemy", 22 "alembic", 23 "asyncpg", 24 "aiosqlite", 25 "supabase", 26 "openai", 27 "anthropic", 28 "google.generativeai", 29 "langchain", 30 "slack_bolt", 31 "slack_sdk", 32 "apscheduler", 33 "fastapi", 34 "uvicorn", 35 "redis", 36 } 37 ) 38 39 # テスト対象モジュール一覧 40 _ALL_MODULES: list[str] = [ 41 "mureo", 42 "mureo.google_ads", 43 "mureo.google_ads.client", 44 "mureo.google_ads.mappers", 45 "mureo.google_ads._ads", 46 "mureo.google_ads._keywords", 47 "mureo.google_ads._analysis", 48 "mureo.google_ads._analysis_constants", 49 "mureo.google_ads._analysis_performance", 50 "mureo.google_ads._analysis_search_terms", 51 "mureo.google_ads._analysis_keywords", 52 "mureo.google_ads._analysis_budget", 53 "mureo.google_ads._analysis_rsa", 54 "mureo.google_ads._analysis_auction", 55 "mureo.google_ads._analysis_btob", 56 "mureo.google_ads._extensions", 57 "mureo.google_ads._diagnostics", 58 "mureo.google_ads._monitoring", 59 "mureo.google_ads._creative", 60 "mureo.google_ads._rsa_validator", 61 "mureo.google_ads._rsa_insights", 62 "mureo.google_ads._intent_classifier", 63 "mureo.google_ads._message_match", 64 "mureo.meta_ads", 65 "mureo.meta_ads.client", 66 "mureo.meta_ads.mappers", 67 "mureo.meta_ads._campaigns", 68 "mureo.meta_ads._ad_sets", 69 "mureo.meta_ads._ads", 70 "mureo.meta_ads._creatives", 71 "mureo.meta_ads._audiences", 72 "mureo.meta_ads._pixels", 73 "mureo.meta_ads._insights", 74 "mureo.meta_ads._analysis", 75 "mureo.analysis", 76 "mureo.analysis.lp_analyzer", 77 "mureo.context", 78 "mureo.context.errors", 79 "mureo.context.models", 80 "mureo.context.state", 81 "mureo.context.strategy", 82 ] 83 84 85 def _collect_imports_from_file(filepath: pathlib.Path) -> list[str]: 86 """ASTを使ってPythonファイルからimport文のモジュール名を収集する""" 87 source = filepath.read_text(encoding="utf-8") 88 tree = ast.parse(source, filename=str(filepath)) 89 90 imports: list[str] = [] 91 for node in ast.walk(tree): 92 if isinstance(node, ast.Import): 93 for alias in node.names: 94 imports.append(alias.name) 95 elif isinstance(node, ast.ImportFrom): 96 if node.module: 97 imports.append(node.module) 98 return imports 99 100 101 @pytest.mark.unit 102 class TestModuleImports: 103 """全モジュールがimportできることを確認""" 104 105 @pytest.mark.parametrize("module_name", _ALL_MODULES) 106 def test_import_succeeds(self, module_name: str) -> None: 107 """各モジュールが正常にimportできる""" 108 mod = importlib.import_module(module_name) 109 assert mod is not None 110 111 112 @pytest.mark.unit 113 class TestNoForbiddenImports: 114 """DB/LLM系のimportが含まれていないことをAST解析で確認""" 115 116 def _collect_all_py_files(self) -> list[pathlib.Path]: 117 """mureo/ 配下の全.pyファイルを収集""" 118 return sorted(_MUREO_ROOT.rglob("*.py")) 119 120 def test_no_forbidden_imports_in_package(self) -> None: 121 """mureo/配下の全ファイルに禁止importが含まれていない""" 122 violations: list[str] = [] 123 124 for py_file in self._collect_all_py_files(): 125 imports = _collect_imports_from_file(py_file) 126 for imp in imports: 127 # トップレベルモジュール名で比較 128 top_module = imp.split(".")[0] 129 for forbidden in _FORBIDDEN_MODULES: 130 if top_module == forbidden.split(".")[0] and imp.startswith( 131 forbidden 132 ): 133 violations.append( 134 f"{py_file.relative_to(_MUREO_ROOT.parent)}: " 135 f"import {imp}" 136 ) 137 138 assert ( 139 violations == [] 140 ), "禁止されたDB/LLM系のimportが検出されました:\n" + "\n".join(violations)