Enforce the import layering with import-linter - #709
Draft
ecomodeller wants to merge 3 commits into
Draft
ecomodeller wants to merge 3 commits into
ecomodeller wants to merge 3 commits into
Conversation
The package had no check on which module may import which, so the layering existed only by convention. .importlinter writes it down: configuration and data on top, then matching, comparison, the skill tables, plotting, model and network, obs, timeseries, and the leaf modules at the bottom. Four modules reached the root package for a name it re-exports (from . import Quantity), which makes every import an edge to the whole package and hides the real dependency. They now import from the defining module. timeseries._align imported Observation only for an annotation, so it moves under TYPE_CHECKING. Three imports stay ignored, each with its reason in the config. The one that is a real violation is plot.scatter(skill_table=True), which builds a Comparer to compute its table and so reaches up from plotting into matching. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ecomodeller
marked this pull request as draft
September 14, 2026 06:39
Includes the module dependency graph as a mermaid diagram, transitively reduced so the 44 edges read as 17. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine unlabelled rows of module names did not say what each layer is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nothing stopped a module importing anything else, so the layering was convention only. import-linter checks it on every build.
flowchart TD subgraph L1["Entry points"] configuration data end subgraph L2["Matching"] matching end subgraph L3["Comparison"] comparison end subgraph L4["Skill tables"] skill skill_grid skill_profile end subgraph L5["Plotting"] plotting end subgraph L6["Model results"] model network end subgraph L7["Observations"] obs end subgraph L8["Time series"] timeseries end subgraph L9["Foundations"] metrics quantity settings types utils end configuration --> matching data --> comparison matching --> comparison comparison --> skill comparison --> skill_grid comparison --> skill_profile skill --> plotting plotting --> model plotting --> metrics model <--> network model --> obs obs --> timeseries timeseries --> quantity timeseries --> types timeseries --> utils metrics --> settings plotting -. "scatter(skill_table=True)" .-> matchingEach box is a layer, named for what it contributes; modules inside a layer may import each other. Arrows point from importer to imported, and transitively implied edges are omitted (44 edges reduce to 17). The dotted arrow is the one accepted violation.
Two source changes were needed to make the real structure visible:
from . import Quantity(andfrom . import model_result, match) — importing a name the root__init__re-exports. Every such import is an edge to the entire package, which both hides the real dependency and creates an import cycle through__init__. They now import from the module that defines the name.timeseries._alignimportedObservationonly to annotate a parameter; moved underTYPE_CHECKING, and the config setsexclude_type_checking_imports.Three imports are ignored, each with its reason in
.importlinter. Two are__version__reads, which pull in the root package because that is where it is defined. The third is the real violation:plot.scatter(skill_table=True)callsfrom_matchedto build aComparerfor its skill table, soplottingreaches up intomatching— deferred into the function body to keep the module importable.Reasoning is recorded in ADR-013.
Runs as
just layers, wired intojust checkand the build job.🤖 Generated with Claude Code