182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
from collections.abc import Iterable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
# Exact temporary exceptions to the wf_sources_mcp -> wf_mcp import boundary.
|
|
# Keep this empty unless a compatibility seam truly cannot live on the wf_mcp
|
|
# side. Values must name the removal condition, not just restate the exception.
|
|
ALLOWED_WF_MCP_IMPORTS: dict[tuple[str, str], str] = {}
|
|
|
|
WF_SOURCES_MCP_ROOT = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ImportViolation:
|
|
module: str
|
|
line: int
|
|
imported: str
|
|
statement: str
|
|
|
|
def format(self) -> str:
|
|
return f"{self.module}:{self.line}: {self.statement}"
|
|
|
|
|
|
def _module_name(py_file: Path) -> str:
|
|
rel = py_file.relative_to(WF_SOURCES_MCP_ROOT.parent)
|
|
return str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
|
|
|
|
|
|
def _is_allowed(module: str, imported: str) -> bool:
|
|
return (module, imported) in ALLOWED_WF_MCP_IMPORTS
|
|
|
|
|
|
def _collect_forbidden_imports(
|
|
*,
|
|
forbidden_exact: Iterable[str] = (),
|
|
forbidden_prefixes: Iterable[str] = (),
|
|
) -> list[ImportViolation]:
|
|
exact = set(forbidden_exact)
|
|
prefixes = tuple(forbidden_prefixes)
|
|
violations: list[ImportViolation] = []
|
|
|
|
def is_forbidden(imported: str) -> bool:
|
|
return imported in exact or imported.startswith(prefixes)
|
|
|
|
for py_file in sorted(WF_SOURCES_MCP_ROOT.rglob("*.py")):
|
|
module = _module_name(py_file)
|
|
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ImportFrom) and node.module is not None:
|
|
imported = node.module
|
|
if is_forbidden(imported) and not _is_allowed(module, imported):
|
|
violations.append(
|
|
ImportViolation(
|
|
module=module,
|
|
line=node.lineno,
|
|
imported=imported,
|
|
statement=f"from {imported} import ...",
|
|
)
|
|
)
|
|
elif isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
imported = alias.name
|
|
if is_forbidden(imported) and not _is_allowed(module, imported):
|
|
violations.append(
|
|
ImportViolation(
|
|
module=module,
|
|
line=node.lineno,
|
|
imported=imported,
|
|
statement=f"import {imported}",
|
|
)
|
|
)
|
|
|
|
return violations
|
|
|
|
|
|
def _assert_no_forbidden_imports(
|
|
message: str,
|
|
*,
|
|
forbidden_exact: Iterable[str] = (),
|
|
forbidden_prefixes: Iterable[str] = (),
|
|
) -> None:
|
|
violations = _collect_forbidden_imports(
|
|
forbidden_exact=forbidden_exact,
|
|
forbidden_prefixes=forbidden_prefixes,
|
|
)
|
|
|
|
assert violations == [], (
|
|
message
|
|
+ "\n"
|
|
+ "\n".join(f" {violation.format()}" for violation in violations)
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_frontend_mcp_modules() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp imports frontend/proxy MCP modules:",
|
|
forbidden_prefixes=(
|
|
"wf_mcp.admin_surface",
|
|
"wf_mcp.workflow_surface",
|
|
"wf_mcp.server",
|
|
"wf_mcp.proxy",
|
|
"wf_mcp.cli",
|
|
),
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_wf_mcp_catalog_dtos() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp catalog DTO modules:",
|
|
forbidden_exact={
|
|
"wf_mcp.broker.catalog",
|
|
"wf_mcp.capabilities",
|
|
"wf_mcp.catalog",
|
|
"wf_mcp.catalog.models",
|
|
},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:",
|
|
forbidden_exact={
|
|
"wf_mcp.sdk",
|
|
"wf_mcp.sdk.adapter",
|
|
"wf_mcp.sdk.base",
|
|
"wf_mcp.runtime",
|
|
"wf_mcp.runtime.protocols",
|
|
},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp SDK converter module:",
|
|
forbidden_exact={"wf_mcp.sdk.converters"},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_broker_discovery_module() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp broker discovery module:",
|
|
forbidden_exact={"wf_mcp.broker.discovery"},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_workflow_wrapper_module() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp workflow wrapper module:",
|
|
forbidden_exact={"wf_mcp.workflow", "wf_mcp.workflow.wrappers"},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_broker_event_modules() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp broker event modules:",
|
|
forbidden_exact={"wf_mcp.events", "wf_mcp.broker.events"},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_broker_service_adapter_module() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp broker service adapter module:",
|
|
forbidden_exact={"wf_mcp.broker.service.adapters"},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_old_wf_mcp_id_modules() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports old wf_mcp source ID modules:",
|
|
forbidden_exact={"wf_mcp.connections", "wf_mcp.shared.names"},
|
|
)
|
|
|
|
|
|
def test_wf_sources_mcp_does_not_import_wf_mcp_broker_dtos() -> None:
|
|
_assert_no_forbidden_imports(
|
|
"wf_sources_mcp still imports wf_mcp broker DTO modules:",
|
|
forbidden_exact={"wf_mcp.models", "wf_mcp.broker.models"},
|
|
)
|