Skip to content

← Modules, Packaging and Tooling step 14 of 41

Hard Framework

Check That Imports Follow Your Layers

Write the check from the article. Given each module's layer and the imports between modules, report every import that points outward, from an inner layer to an outer one. The imports arrive as plain data.

def check_layering(
    module_layers: Mapping[str, str],
    layer_order: Sequence[str],
    imports: Mapping[str, Sequence[str]],
) -> list[tuple[str, str]]:
    ...
  • module_layers maps a module name to its layer name.
  • layer_order lists the layers from innermost to outermost, such as ["domain", "application", "adapters", "entrypoints"].
  • imports maps a module to the modules it imports directly.
layers = {"domain.money": "domain", "app.quote": "application",
          "adapters.db": "adapters"}
order = ["domain", "application", "adapters"]
check_layering(layers, order, {
    "app.quote": ["domain.money", "adapters.db", "adapters.db"],
    "domain.money": ["app.quote"],
})
# [("app.quote", "adapters.db"), ("domain.money", "app.quote")]

check_layering(layers, order, {"domain.money": ["sqlite3"]})    # []
check_layering(layers, [], {"domain.money": ["adapters.db"]})   # []

Rules

  • A layer's rank is its index in layer_order. If a layer name appears twice, use its last index.
  • Report (importer, imported) when the imported module's rank is strictly greater than the importer's. Do not report an import inward, within one layer, or of a module by itself.
  • Skip an import if either module is missing from module_layers, or if either module's layer is missing from layer_order. An empty layer_order therefore gives no findings.
  • Match module names exactly. Names are case-sensitive, and an entry for "domain" in module_layers does not cover "domain.money".
  • A module with no entry in imports imports nothing.
  • Report each pair once, and return the pairs sorted with normal tuple ordering, so by importer and then by imported module.
  • Do not change the inputs.

The sqlite3 example returns [] because sqlite3 has no layer. If your design bans some third-party packages from the domain, that needs a separate rule.

The starter compares layer names instead of ranks, raises KeyError on an unknown module, and keeps duplicate pairs. Fix all three. Your solution must pass mypy --strict. module_layers.get() returns None for a module to skip, so check for None before you compare.