diff --git a/docs/docs/in_depth/mloda-api.md b/docs/docs/in_depth/mloda-api.md index 71daffc2..6f80b964 100644 --- a/docs/docs/in_depth/mloda-api.md +++ b/docs/docs/in_depth/mloda-api.md @@ -198,7 +198,7 @@ from mloda.provider import FeatureResolutionError, ResolutionDiagnosis, Resoluti - **`ResolutionDiagnosis`** (frozen dataclass): the return value of `diagnose`, never raised. Fields: `records` (`list[ResolutionRecord]`), `complete` (`bool`), `feature_name` (`str | None`), `failed_result` (`EvaluationResult | None`), `message` (`str | None`). - **`ResolutionRecord`** (frozen dataclass): one per feature, returned inside `resolution_report()`, `ResolutionDiagnosis.records`, and `FeatureResolutionError.partial_records`; never raised. Fields: `feature_name` (`str`), `requested` (`bool`), `result` (`EvaluationResult`). -`EvaluationResult` is the captured matcher outcome carried by the fields above; it is defined in `mloda.core.prepare.resolution_types`, also importable from `mloda.core.prepare.identify_feature_group`, and is not part of the public `__init__` exports. +`EvaluationResult` is the captured matcher outcome carried by the fields above; it is defined in `mloda.core.prepare.resolution_types` and is not part of the public `__init__` exports. ##### get_feature_group_docs diff --git a/mloda/core/api/plugin_docs.py b/mloda/core/api/plugin_docs.py index 964d7e34..e1d44fc3 100644 --- a/mloda/core/api/plugin_docs.py +++ b/mloda/core/api/plugin_docs.py @@ -40,11 +40,9 @@ dedup_feature_group_subclasses, registry_for, ) -from mloda.core.prepare.identify_feature_group import ( - CandidateFrameworks, - evaluate_and_render, - scope_callout, -) +from mloda.core.prepare.identify_feature_group import evaluate_and_render +from mloda.core.prepare.resolution_failure_renderer import scope_callout +from mloda.core.prepare.resolution_types import CandidateFrameworks def list_registered(plugin_type: type[Any]) -> list[type[Any]]: diff --git a/mloda/core/api/request.py b/mloda/core/api/request.py index 4f3cf37d..cd48a124 100644 --- a/mloda/core/api/request.py +++ b/mloda/core/api/request.py @@ -12,6 +12,8 @@ from mloda.core.prepare.identify_feature_group import ( ComputeFrameworkPinError, FeatureResolutionError, +) +from mloda.core.prepare.resolution_types import ( ResolutionDiagnosis, ResolutionRecord, ) diff --git a/mloda/core/core/engine.py b/mloda/core/core/engine.py index 28a52fac..f9f3168b 100644 --- a/mloda/core/core/engine.py +++ b/mloda/core/core/engine.py @@ -21,10 +21,10 @@ from mloda.core.prepare.graph.build_graph import BuildGraph from mloda.core.prepare.resolve_graph import ResolveGraph from mloda.core.runtime.run import ExecutionOrchestrator -from mloda.core.prepare.identify_feature_group import ( +from mloda.core.prepare.identify_feature_group import resolve_or_raise +from mloda.core.prepare.resolution_types import ( EvaluationResult, ResolutionRecord, - resolve_or_raise, ) from mloda.core.runtime.flight.runner_flight_server import ParallelRunnerFlightServer from mloda.core.abstract_plugins.feature_group import FeatureGroup, format_feature_group_class diff --git a/mloda/core/prepare/identify_feature_group.py b/mloda/core/prepare/identify_feature_group.py index 721c0220..0e6df593 100644 --- a/mloda/core/prepare/identify_feature_group.py +++ b/mloda/core/prepare/identify_feature_group.py @@ -5,6 +5,8 @@ from typing import Optional from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping + +# Not a re-export facade: every import here is used by this module and ruff F401 fails any added just to re-export. from mloda.core.prepare.resolution_types import ( CandidateFrameworks, Elimination, @@ -12,13 +14,10 @@ EvaluationResult, PARTIAL_RECORDS_CAP, RenderFacts, - ResolutionDiagnosis, ResolutionRecord, ) from mloda.core.prepare.resolution_failure_renderer import ( - TROUBLESHOOTING_URL, render_resolution_failure, - scope_callout, _prefix_name, _supported_feature_names, ) @@ -39,27 +38,6 @@ logger = logging.getLogger(__name__) -# Keeps the pre-split import surface of this module intact: the moved names stay importable from here. -__all__ = [ - "CandidateFrameworks", - "ComputeFrameworkPinError", - "Elimination", - "EliminationStage", - "EvaluationResult", - "FeatureResolutionError", - "IdentifyFeatureGroupClass", - "PARTIAL_RECORDS_CAP", - "RenderFacts", - "ResolutionDiagnosis", - "ResolutionRecord", - "TROUBLESHOOTING_URL", - "evaluate_and_render", - "matches_feature_group_scope", - "render_resolution_failure", - "resolve_or_raise", - "scope_callout", -] - class FeatureResolutionError(ValueError): """Typed resolution failure carrying the feature name, the EvaluationResult of its single pass, diff --git a/mloda/provider/__init__.py b/mloda/provider/__init__.py index f10d403f..35d13f8b 100644 --- a/mloda/provider/__init__.py +++ b/mloda/provider/__init__.py @@ -102,8 +102,8 @@ # Feature resolution debugging from mloda.core.api.plugin_docs import resolve_feature -from mloda.core.prepare.identify_feature_group import ( - FeatureResolutionError, +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( ResolutionDiagnosis, ResolutionRecord, ) diff --git a/mloda/steward/__init__.py b/mloda/steward/__init__.py index 652b5fd6..b57f9443 100644 --- a/mloda/steward/__init__.py +++ b/mloda/steward/__init__.py @@ -33,8 +33,8 @@ ) # Feature resolution -from mloda.core.prepare.identify_feature_group import ( - FeatureResolutionError, +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( ResolutionDiagnosis, ResolutionRecord, ) diff --git a/mloda/user/__init__.py b/mloda/user/__init__.py index 8cfcac52..9d4fef2a 100644 --- a/mloda/user/__init__.py +++ b/mloda/user/__init__.py @@ -52,8 +52,8 @@ ) # Feature resolution -from mloda.core.prepare.identify_feature_group import ( - FeatureResolutionError, +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( ResolutionDiagnosis, ResolutionRecord, ) diff --git a/tests/test_core/test_api/test_diagnose.py b/tests/test_core/test_api/test_diagnose.py index d298d804..478772b2 100644 --- a/tests/test_core/test_api/test_diagnose.py +++ b/tests/test_core/test_api/test_diagnose.py @@ -1,7 +1,7 @@ """Failing tests for the non-raising resolution preflight ``mlodaAPI.diagnose()`` (issue #812). Contract under test: - * ``mloda.core.prepare.identify_feature_group.ResolutionDiagnosis`` is a frozen dataclass beside + * ``mloda.core.prepare.resolution_types.ResolutionDiagnosis`` is a frozen dataclass beside ``ResolutionRecord`` and ``EvaluationResult``, with fields ``records``, ``complete``, ``feature_name``, ``failed_result``, ``message`` in that order. It is re-exported from ``mloda.user`` and ``mloda.steward``. * ``mlodaAPI.diagnose(...)`` runs the SAME eager planning as ``prepare(...)`` but NEVER raises for a @@ -26,9 +26,9 @@ # ``mloda`` mlodaAPI alias imported below. import mloda.steward as mloda_steward import mloda.user as mloda_user -from mloda.core.prepare.identify_feature_group import ( +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( EvaluationResult, - FeatureResolutionError, ResolutionDiagnosis, ResolutionRecord, ) diff --git a/tests/test_core/test_api/test_resolution_report.py b/tests/test_core/test_api/test_resolution_report.py index 11dd8fae..e83a3d50 100644 --- a/tests/test_core/test_api/test_resolution_report.py +++ b/tests/test_core/test_api/test_resolution_report.py @@ -1,7 +1,7 @@ """Failing tests for per-feature EvaluationResult capture and ``session.resolution_report()`` (issue #811). Contract under test: - * ``mloda.core.prepare.identify_feature_group.ResolutionRecord`` is a frozen dataclass beside + * ``mloda.core.prepare.resolution_types.ResolutionRecord`` is a frozen dataclass beside ``EvaluationResult``, with fields ``feature_name: str``, ``requested: bool``, ``result: EvaluationResult`` in that order. It is re-exported from both ``mloda.user`` and ``mloda.steward``. * ``IdentifyFeatureGroupClass.evaluate(...)`` returns the winning ``EvaluationResult`` on a successful @@ -31,9 +31,9 @@ import mloda.steward as mloda_steward import mloda.user as mloda_user from mloda.core.core.engine import Engine -from mloda.core.prepare.identify_feature_group import ( +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( EvaluationResult, - FeatureResolutionError, ResolutionRecord, ) from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_core/test_engine_domain_propagation.py b/tests/test_core/test_core/test_engine_domain_propagation.py index 23d74966..0e8e237f 100644 --- a/tests/test_core/test_core/test_engine_domain_propagation.py +++ b/tests/test_core/test_core/test_engine_domain_propagation.py @@ -9,7 +9,7 @@ from mloda.core.core.engine import Engine -from mloda.core.prepare.identify_feature_group import EvaluationResult +from mloda.core.prepare.resolution_types import EvaluationResult from mloda.core.abstract_plugins.components.feature import Feature from mloda.core.abstract_plugins.components.feature_collection import Features from mloda.core.abstract_plugins.components.feature_name import FeatureName diff --git a/tests/test_core/test_integration/test_core/test_diagnose_integration.py b/tests/test_core/test_integration/test_core/test_diagnose_integration.py index 07a5650c..0e1af61e 100644 --- a/tests/test_core/test_integration/test_core/test_diagnose_integration.py +++ b/tests/test_core/test_integration/test_core/test_diagnose_integration.py @@ -15,9 +15,9 @@ import pytest -from mloda.core.prepare.identify_feature_group import ( +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( EvaluationResult, - FeatureResolutionError, ResolutionDiagnosis, ResolutionRecord, ) diff --git a/tests/test_core/test_integration/test_core/test_resolution_report_integration.py b/tests/test_core/test_integration/test_core/test_resolution_report_integration.py index a1a47951..a7b0b290 100644 --- a/tests/test_core/test_integration/test_core/test_resolution_report_integration.py +++ b/tests/test_core/test_integration/test_core/test_resolution_report_integration.py @@ -12,7 +12,7 @@ from typing import Any -from mloda.core.prepare.identify_feature_group import EvaluationResult +from mloda.core.prepare.resolution_types import EvaluationResult from mloda.provider import DefaultOptionKeys from mloda.user import Feature, Options, PluginCollector, ResolutionRecord, mloda, mlodaAPI from mloda_plugins.compute_framework.base_implementations.pandas.dataframe import PandasDataFrame diff --git a/tests/test_core/test_prepare/identify_seam.py b/tests/test_core/test_prepare/identify_seam.py index 733c12a3..7ea90add 100644 --- a/tests/test_core/test_prepare/identify_seam.py +++ b/tests/test_core/test_prepare/identify_seam.py @@ -21,10 +21,8 @@ from mloda.core.abstract_plugins.compute_framework import ComputeFramework from mloda.core.abstract_plugins.feature_group import FeatureGroup from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping -from mloda.core.prepare.identify_feature_group import ( - EvaluationResult, - resolve_or_raise, -) +from mloda.core.prepare.identify_feature_group import resolve_or_raise +from mloda.core.prepare.resolution_types import EvaluationResult def evaluate_or_raise( diff --git a/tests/test_core/test_prepare/test_candidate_elimination_reasons.py b/tests/test_core/test_prepare/test_candidate_elimination_reasons.py index 56034809..4329a76d 100644 --- a/tests/test_core/test_prepare/test_candidate_elimination_reasons.py +++ b/tests/test_core/test_prepare/test_candidate_elimination_reasons.py @@ -31,11 +31,11 @@ from mloda.core.abstract_plugins.feature_group import FeatureGroup from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping from mloda.core.prepare.identify_feature_group import ( - Elimination, FeatureResolutionError, IdentifyFeatureGroupClass, - render_resolution_failure, ) +from mloda.core.prepare.resolution_failure_renderer import render_resolution_failure +from mloda.core.prepare.resolution_types import Elimination from tests.test_core.test_prepare.identify_seam import evaluate_or_raise, identify_winner diff --git a/tests/test_core/test_prepare/test_feature_group_dedup.py b/tests/test_core/test_prepare/test_feature_group_dedup.py index 45fd841f..8cb2f348 100644 --- a/tests/test_core/test_prepare/test_feature_group_dedup.py +++ b/tests/test_core/test_prepare/test_feature_group_dedup.py @@ -36,7 +36,7 @@ dedup_feature_group_subclasses, _safe_class_source_hash, ) -from mloda.core.prepare.identify_feature_group import ResolutionDiagnosis +from mloda.core.prepare.resolution_types import ResolutionDiagnosis from mloda_plugins.compute_framework.base_implementations.pandas.dataframe import PandasDataFrame diff --git a/tests/test_core/test_prepare/test_feature_resolution_error.py b/tests/test_core/test_prepare/test_feature_resolution_error.py index d913187b..e64defdc 100644 --- a/tests/test_core/test_prepare/test_feature_resolution_error.py +++ b/tests/test_core/test_prepare/test_feature_resolution_error.py @@ -27,8 +27,8 @@ from mloda.core.prepare.identify_feature_group import ( FeatureResolutionError, IdentifyFeatureGroupClass, - render_resolution_failure, ) +from mloda.core.prepare.resolution_failure_renderer import render_resolution_failure from mloda.user import mlodaAPI from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_prepare/test_feature_resolution_error_partial_records.py b/tests/test_core/test_prepare/test_feature_resolution_error_partial_records.py index 00b056da..fcd96bdb 100644 --- a/tests/test_core/test_prepare/test_feature_resolution_error_partial_records.py +++ b/tests/test_core/test_prepare/test_feature_resolution_error_partial_records.py @@ -1,7 +1,7 @@ """Failing tests for the diagnose refactor: FeatureResolutionError carries the partial resolution records. Contract under test (PR #836): - * ``PARTIAL_RECORDS_CAP`` is a module-level int constant (1000) in ``identify_feature_group``. + * ``PARTIAL_RECORDS_CAP`` is a module-level int constant (1000) in ``resolution_types``. * ``FeatureResolutionError.__init__`` accepts ``partial_records: Sequence[ResolutionRecord] = ()`` and stores it as a tuple truncated to the LAST ``PARTIAL_RECORDS_CAP`` entries; ``__reduce__`` round-trips it via pickle. * The raising ``mloda.prepare`` path attaches the records resolved before the failure to the error, and @@ -23,9 +23,9 @@ from mloda.core.api.request import SetupConfigurationError from mloda.core.core.engine import Engine -from mloda.core.prepare.identify_feature_group import ( +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import ( EvaluationResult, - FeatureResolutionError, ResolutionRecord, ) from mloda.provider import BaseInputData, ComputeFramework, DataCreator, FeatureGroup, FeatureSet @@ -114,7 +114,7 @@ class TestPartialRecordsCapConstant: """PARTIAL_RECORDS_CAP is a module-level int constant with value 1000.""" def test_partial_records_cap_is_1000(self) -> None: - from mloda.core.prepare.identify_feature_group import PARTIAL_RECORDS_CAP + from mloda.core.prepare.resolution_types import PARTIAL_RECORDS_CAP assert isinstance(PARTIAL_RECORDS_CAP, int) assert PARTIAL_RECORDS_CAP == 1000 @@ -143,7 +143,7 @@ def test_partial_records_is_stored_as_a_tuple(self) -> None: assert error.partial_records == tuple(records) def test_partial_records_is_truncated_to_the_last_cap_entries(self) -> None: - from mloda.core.prepare.identify_feature_group import PARTIAL_RECORDS_CAP + from mloda.core.prepare.resolution_types import PARTIAL_RECORDS_CAP result = _empty_result() records = [_record(f"capped_836pr_{index}", result) for index in range(PARTIAL_RECORDS_CAP + 5)] diff --git a/tests/test_core/test_prepare/test_first_pass_rejection_recording.py b/tests/test_core/test_prepare/test_first_pass_rejection_recording.py index 8ee99a61..500e8a44 100644 --- a/tests/test_core/test_prepare/test_first_pass_rejection_recording.py +++ b/tests/test_core/test_prepare/test_first_pass_rejection_recording.py @@ -33,7 +33,8 @@ from mloda.core.abstract_plugins.compute_framework import ComputeFramework from mloda.core.abstract_plugins.feature_group import FeatureGroup from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping -from mloda.core.prepare.identify_feature_group import Elimination, EvaluationResult, FeatureResolutionError +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import Elimination, EvaluationResult from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_prepare/test_identify_feature_group_error_message.py b/tests/test_core/test_prepare/test_identify_feature_group_error_message.py index 89e14f46..85b80baf 100644 --- a/tests/test_core/test_prepare/test_identify_feature_group_error_message.py +++ b/tests/test_core/test_prepare/test_identify_feature_group_error_message.py @@ -1,8 +1,7 @@ -"""Tests for error message formatting in identify_feature_group.py. +"""Tests for the resolution failure messages raised when a feature does not resolve to exactly one group. -This test verifies that the multiple-feature-group error message uses the -formatted candidate output from _render_multiple (naming each class as -"ClassName (module.path)") instead of raw dict/class representation. +The formatting lives in mloda/core/prepare/resolution_failure_renderer.py: _render_multiple names each +candidate as "ClassName (module.path)" instead of a raw dict/class representation. """ from typing import Optional diff --git a/tests/test_core/test_prepare/test_identify_feature_group_evaluation_seam.py b/tests/test_core/test_prepare/test_identify_feature_group_evaluation_seam.py index 9e61a2cc..9f814f8d 100644 --- a/tests/test_core/test_prepare/test_identify_feature_group_evaluation_seam.py +++ b/tests/test_core/test_prepare/test_identify_feature_group_evaluation_seam.py @@ -23,9 +23,9 @@ from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping from mloda.core.prepare.identify_feature_group import ( ComputeFrameworkPinError, - EvaluationResult, IdentifyFeatureGroupClass, ) +from mloda.core.prepare.resolution_types import EvaluationResult from mloda_plugins.compute_framework.base_implementations.pandas.dataframe import PandasDataFrame from mloda_plugins.compute_framework.base_implementations.python_dict.python_dict_framework import PythonDictFramework from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_prepare/test_identify_seam.py b/tests/test_core/test_prepare/test_identify_seam.py index 2040d665..1aa2ef8f 100644 --- a/tests/test_core/test_prepare/test_identify_seam.py +++ b/tests/test_core/test_prepare/test_identify_seam.py @@ -21,11 +21,11 @@ from mloda.core.abstract_plugins.feature_group import FeatureGroup from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping from mloda.core.prepare.identify_feature_group import ( - EvaluationResult, FeatureResolutionError, IdentifyFeatureGroupClass, - render_resolution_failure, ) +from mloda.core.prepare.resolution_failure_renderer import render_resolution_failure +from mloda.core.prepare.resolution_types import EvaluationResult from tests.test_core.test_prepare.identify_seam import evaluate_or_raise, identify_winner diff --git a/tests/test_core/test_prepare/test_required_when_rejection_recording.py b/tests/test_core/test_prepare/test_required_when_rejection_recording.py index 6841336d..20e55a3b 100644 --- a/tests/test_core/test_prepare/test_required_when_rejection_recording.py +++ b/tests/test_core/test_prepare/test_required_when_rejection_recording.py @@ -29,7 +29,8 @@ from mloda.core.abstract_plugins.compute_framework import ComputeFramework from mloda.core.abstract_plugins.feature_group import FeatureGroup from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping -from mloda.core.prepare.identify_feature_group import EvaluationResult, FeatureResolutionError +from mloda.core.prepare.identify_feature_group import FeatureResolutionError +from mloda.core.prepare.resolution_types import EvaluationResult from mloda.provider import PropertySpec from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_prepare/test_identify_feature_group_failure_renderer.py b/tests/test_core/test_prepare/test_resolution_failure_renderer.py similarity index 99% rename from tests/test_core/test_prepare/test_identify_feature_group_failure_renderer.py rename to tests/test_core/test_prepare/test_resolution_failure_renderer.py index 1129f3c1..f1a893be 100644 --- a/tests/test_core/test_prepare/test_identify_feature_group_failure_renderer.py +++ b/tests/test_core/test_prepare/test_resolution_failure_renderer.py @@ -30,13 +30,13 @@ from mloda.core.abstract_plugins.feature_group import FeatureGroup from mloda.core.api.plugin_docs import resolve_feature from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping -from mloda.core.prepare.identify_feature_group import ( +from mloda.core.prepare.identify_feature_group import IdentifyFeatureGroupClass +from mloda.core.prepare.resolution_failure_renderer import render_resolution_failure +from mloda.core.prepare.resolution_types import ( CandidateFrameworks, Elimination, EvaluationResult, - IdentifyFeatureGroupClass, RenderFacts, - render_resolution_failure, ) diff --git a/tests/test_core/test_prepare/test_resolution_module_split.py b/tests/test_core/test_prepare/test_resolution_module_split.py index 9c761105..5e389ccb 100644 --- a/tests/test_core/test_prepare/test_resolution_module_split.py +++ b/tests/test_core/test_prepare/test_resolution_module_split.py @@ -1,7 +1,8 @@ """Pins the split of identify_feature_group into resolution_types and resolution_failure_renderer. -Ownership, re-export identity for the public names, and the acyclic direction -resolution_types <- resolution_failure_renderer <- identify_feature_group. +Which module owns which name, that the matcher is not a facade (no __all__, no call site importing a +name it does not own from it), and the acyclic direction resolution_types <- resolution_failure_renderer +<- identify_feature_group. """ from __future__ import annotations @@ -10,7 +11,7 @@ import importlib import subprocess # nosec B404 import sys -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from pathlib import Path from types import ModuleType @@ -45,7 +46,7 @@ "render_resolution_failure", ) -# What stays in the matcher module, with its definition, not as a re-export. +# What stays in the matcher module, with its definition, and the only names a call site may import from it. MATCHER_KEPT_NAMES = ( "matches_feature_group_scope", "FeatureResolutionError", @@ -55,24 +56,12 @@ "resolve_or_raise", ) -# Every name a call site imports from the matcher module today; the split must keep all of them importable. -CALL_SITE_NAMES = ( - "CandidateFrameworks", - "ComputeFrameworkPinError", - "Elimination", - "EvaluationResult", - "FeatureResolutionError", - "IdentifyFeatureGroupClass", - "PARTIAL_RECORDS_CAP", - "RenderFacts", - "ResolutionDiagnosis", - "ResolutionRecord", - "evaluate_and_render", - "matches_feature_group_scope", - "render_resolution_failure", - "resolve_or_raise", - "scope_callout", -) +# Directories swept for imports of the matcher, relative to the repo root. docs/ is in because the marimo +# notebooks under docs/docs/examples/ are excluded from both mypy and ruff F401, so nothing else gates them. +SWEPT_DIRS = ("mloda", "mloda_plugins", "tests", "docs") + +# Last segment of MATCHER_MODULE: what the sweep gates on, see the gate comment below. +MATCHER_BASENAME = MATCHER_MODULE.rsplit(".", 1)[-1] _SUBPROCESS_TIMEOUT = 8.0 @@ -91,12 +80,31 @@ def _prepare_dir() -> Path: return Path(matcher_file).parent +def _repo_root() -> Path: + """Repo root, derived from this file's location at /tests/test_core/test_prepare. + + Deliberately not off _prepare_dir(): under `tox -e installed` the matcher resolves to site-packages, + which has no tests tree, so a module-derived root would sweep nothing and still pass. The test file + is always in the checkout, whichever import mode is in play. + """ + return Path(__file__).parents[3] + + def _missing_names(module: ModuleType, names: Sequence[str]) -> list[str]: return [name for name in names if not hasattr(module, name)] -def _public(names: Iterable[str]) -> list[str]: - return [name for name in names if not name.startswith("_")] +def _foreign_matcher_imports(path: Path, source: str) -> list[str]: + """`path:line -> name` for every name imported from the matcher that the matcher does not own.""" + tree = ast.parse(source, filename=str(path)) + offenders: list[str] = [] + for node in ast.walk(tree): # walk, not tree.body: function-local imports count too + if not isinstance(node, ast.ImportFrom) or node.module != MATCHER_MODULE: + continue + offenders.extend( + f"{path}:{node.lineno} -> {alias.name}" for alias in node.names if alias.name not in MATCHER_KEPT_NAMES + ) + return offenders def _imported_modules(path: Path) -> set[str]: @@ -132,17 +140,6 @@ def test_resolution_failure_renderer_module_defines_its_names() -> None: assert missing == [], f"{RENDERER_MODULE} does not define {missing}" -def test_public_moved_names_are_reexported_by_identity() -> None: - """The matcher must re-export the same objects, not copies, so isinstance and `is` checks keep working.""" - for module_name, names in ((TYPES_MODULE, TYPES_NAMES), (RENDERER_MODULE, RENDERER_NAMES)): - owner = importlib.import_module(module_name) - for name in _public(names): - assert hasattr(identify_feature_group, name), f"{MATCHER_MODULE} no longer re-exports {name}" - assert getattr(identify_feature_group, name) is getattr(owner, name), ( - f"{MATCHER_MODULE}.{name} is not the same object as {module_name}.{name}" - ) - - def test_kept_names_stay_defined_in_the_matcher_module() -> None: for name in MATCHER_KEPT_NAMES: assert hasattr(identify_feature_group, name), f"{MATCHER_MODULE} no longer defines {name}" @@ -150,17 +147,38 @@ def test_kept_names_stay_defined_in_the_matcher_module() -> None: assert owner == MATCHER_MODULE, f"{name} moved out of {MATCHER_MODULE} to {owner}" -def test_call_site_names_remain_importable_from_the_matcher() -> None: - matcher = importlib.import_module(MATCHER_MODULE) - missing = _missing_names(matcher, CALL_SITE_NAMES) - assert missing == [], f"{MATCHER_MODULE} no longer exposes {missing}, so its call sites break" - # hasattr alone is blind to __all__, but mypy --strict implies --no-implicit-reexport: the re-export - # surface it checks against is __all__, so a name missing there breaks type checking at every call site - # even while the runtime attribute is still there. - unexported = [name for name in CALL_SITE_NAMES if name not in matcher.__all__] - assert unexported == [], ( - f"{MATCHER_MODULE}.__all__ omits {unexported}; mypy --strict's no-implicit-reexport checks against " - f"__all__, so its call sites fail to type-check even though the attributes still exist" +def test_matcher_declares_no_all() -> None: + """A module always exports what it defines, so __all__ here would only re-export imports: the facade is back.""" + declared = getattr(identify_feature_group, "__all__", None) + assert declared is None, f"{MATCHER_MODULE} declares __all__ = {declared}, re-exporting names it does not own" + + +def test_no_call_site_imports_a_foreign_name_from_the_matcher() -> None: + """No call site imports a name from the matcher that the matcher does not own. + + Not the main line of defence: `mypy --strict` sweeps the same trees and its no_implicit_reexport + rejects every static evasion (aliased imports, module-object attribute access, relative imports). + Its one blind spot is what this test exists for: re-adding __all__ to the matcher makes those + re-exports legal again for mypy, and the call sites route through the facade unchallenged. + """ + root = _repo_root() + offenders: list[str] = [] + for directory in SWEPT_DIRS: + base = root / directory + if not base.is_dir(): # a directory may be absent (slim checkout, sdist layout); the rest still gets swept + continue + for path in sorted(base.rglob("*.py")): + source = path.read_text(encoding="utf-8") + # Substring gate: parses 27 of 907 files, ~0.15s instead of ~1.5s. A 10x saving well inside the + # 10s timeout either way, so drop it freely if it ever gets in the way. Gating on the bare last + # segment rather than the dotted path keeps `from a.b . c import X` spellings in scope. + if MATCHER_BASENAME not in source: + continue + offenders.extend(_foreign_matcher_imports(path, source)) + listed = "\n".join(offenders) + assert offenders == [], ( + f"these imports take a name from {MATCHER_MODULE} that it does not own; import each from " + f"{TYPES_MODULE} or {RENDERER_MODULE} instead:\n{listed}" ) diff --git a/tests/test_core/test_prepare/test_resolve_or_raise.py b/tests/test_core/test_prepare/test_resolve_or_raise.py index 1321f3f4..5fe08dfa 100644 --- a/tests/test_core/test_prepare/test_resolve_or_raise.py +++ b/tests/test_core/test_prepare/test_resolve_or_raise.py @@ -40,16 +40,17 @@ from mloda.core.api.plugin_info import ResolvedFeature from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping, PreFilterPlugins from mloda.core.prepare.identify_feature_group import ( - PARTIAL_RECORDS_CAP, ComputeFrameworkPinError, - EvaluationResult, FeatureResolutionError, IdentifyFeatureGroupClass, - ResolutionRecord, evaluate_and_render, - render_resolution_failure, resolve_or_raise, - scope_callout, +) +from mloda.core.prepare.resolution_failure_renderer import render_resolution_failure, scope_callout +from mloda.core.prepare.resolution_types import ( + PARTIAL_RECORDS_CAP, + EvaluationResult, + ResolutionRecord, ) from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_prepare/test_single_pass_exactly_once.py b/tests/test_core/test_prepare/test_single_pass_exactly_once.py index cf84b384..1800a51d 100644 --- a/tests/test_core/test_prepare/test_single_pass_exactly_once.py +++ b/tests/test_core/test_prepare/test_single_pass_exactly_once.py @@ -44,10 +44,8 @@ from mloda.core.api.plugin_docs import resolve_feature from mloda.core.prepare import identify_feature_group from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping, PreFilterPlugins -from mloda.core.prepare.identify_feature_group import ( - IdentifyFeatureGroupClass, - render_resolution_failure, -) +from mloda.core.prepare.identify_feature_group import IdentifyFeatureGroupClass +from mloda.core.prepare.resolution_failure_renderer import render_resolution_failure from tests.test_core.test_prepare.identify_seam import evaluate_or_raise diff --git a/tests/test_core/test_prepare/test_subtype_matching.py b/tests/test_core/test_prepare/test_subtype_matching.py index edbc0946..c24c4081 100644 --- a/tests/test_core/test_prepare/test_subtype_matching.py +++ b/tests/test_core/test_prepare/test_subtype_matching.py @@ -11,7 +11,8 @@ from mloda.core.abstract_plugins.components.options import Options from mloda.core.abstract_plugins.compute_framework import ComputeFramework from mloda.core.prepare.accessible_plugins import FeatureGroupEnvironmentMapping -from mloda.core.prepare.identify_feature_group import CandidateFrameworks, IdentifyFeatureGroupClass +from mloda.core.prepare.identify_feature_group import IdentifyFeatureGroupClass +from mloda.core.prepare.resolution_types import CandidateFrameworks from mloda.provider import FeatureChainParserMixin, FeatureGroup, SubtypeDeclaration, property_spec from tests.test_core.test_prepare.identify_seam import identify_winner diff --git a/tests/test_core/test_resolution_parity/test_environment_build_failure_parity.py b/tests/test_core/test_resolution_parity/test_environment_build_failure_parity.py index 62a2b1a3..d87084bb 100644 --- a/tests/test_core/test_resolution_parity/test_environment_build_failure_parity.py +++ b/tests/test_core/test_resolution_parity/test_environment_build_failure_parity.py @@ -49,7 +49,7 @@ PreFilterPlugins, RedefinitionConflictError, ) -from mloda.core.prepare.identify_feature_group import ResolutionDiagnosis +from mloda.core.prepare.resolution_types import ResolutionDiagnosis from mloda_plugins.compute_framework.base_implementations.pandas.dataframe import PandasDataFrame