Skip to content

Enforce the import layering with import-linter - #709

Draft
ecomodeller wants to merge 3 commits into
mainfrom
import-layers
Draft

ecomodeller wants to merge 3 commits into
mainfrom
import-layers

Conversation

@ecomodeller

@ecomodeller ecomodeller commented Sep 14, 2026

Copy link
Copy Markdown
Member

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)" .-> matching
Loading

Each 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:

  • Four modules used from . import Quantity (and from . 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._align imported Observation only to annotate a parameter; moved under TYPE_CHECKING, and the config sets exclude_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) calls from_matched to build a Comparer for its skill table, so plotting reaches up into matching — deferred into the function body to keep the module importable.

Reasoning is recorded in ADR-013.

Runs as just layers, wired into just check and the build job.

🤖 Generated with Claude Code

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
ecomodeller marked this pull request as draft September 14, 2026 06:39
ecomodeller and others added 2 commits September 14, 2026 08:40
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant