INW002 context-independence¶
What it does¶
Reports an import from one context into another that the importing context doesn't list in depends-on. Contexts are declared in [[tool.inwards.contexts]]; the context whose prefix matches the most of a module owns it.
- A declared dependency is direct.
ordersdepending onbillingletsordersimportbilling, not the other way round, and not whateverbillingdepends on. - A context nested in another is a context of its own. Depending on
billingdoesn't cover ataxcontext declared insideshop.billing, andtaxitself needsdepends-on = ["billing"]to import its parent. - Imports within a context, and imports where either end belongs to no context (a shared kernel, a library), are left alone.
- Which of the other context's modules is imported doesn't matter here: a declared dependency allows any of them. Keeping callers to a context's
publicmodules is INW003's job.
Every import counts, as for INW001: relative ones, from shop import shipping, imports inside functions or behind TYPE_CHECKING, and dynamic imports whose target Inwards can read. The rule applies to every file a context owns, whether or not a layer owns it too. A file outside every layer still gets its INW006 warning, which then says that only context rules check its imports. Contexts and layers add up, so one import can be both INW001 and INW002.
Why is this bad¶
A slice that reaches into another slice's code couples the two: a change in shipping breaks orders, and neither can be moved, split out or owned by another team on its own. An agent sees only the name it needs, finds it in the other slice and imports it, which is the fastest way to finish the task and the one that erodes the boundaries you drew. depends-on makes each allowed dependency a decision someone wrote down.
Example¶
Three slices in one layer. Orders may use billing; nothing may use orders, and shipping is on its own:
[tool.inwards]
layers = [
{ name = "slices", modules = ["shop.orders", "shop.billing", "shop.shipping"] },
]
[[tool.inwards.contexts]]
name = "orders"
modules = ["shop.orders"]
depends-on = ["billing"]
[[tool.inwards.contexts]]
name = "billing"
modules = ["shop.billing"]
public = ["shop.billing.invoices"]
[[tool.inwards.contexts]]
name = "shipping"
modules = ["shop.shipping"]
from shop.billing.invoices import issue_invoice
from shop.shipping.labels import print_label
shop/orders/checkout.py:2:34: INW002 Context "orders" imports "shop.shipping.labels.print_label" from context "shipping", which it doesn't declare in depends-on.
fix: Keep "orders" independent of "shipping", or ask the user whether it should depend on it.
1. Delete `from shop.shipping.labels import print_label`. Do not move the import into a function or behind TYPE_CHECKING; Inwards checks those too.
2. Define what "orders" needs as an interface (a Protocol) inside "orders", and let the composition root pass in the "shipping" implementation. "orders" may import from "billing".
3. If "orders" should use "shipping" directly, ask the user to add "shipping" to its depends-on in [tool.inwards]. Don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/INW002/
The import of billing passes: orders declares it. Fixed: checkout asks for a label printer instead of importing shipping's.
from typing import Protocol
from shop.billing.invoices import issue_invoice
class LabelPrinter(Protocol):
def print_label(self, order_id: int) -> None: ...
def check_out(order_id: int, labels: LabelPrinter) -> None:
issue_invoice(order_id)
labels.print_label(order_id)
How to fix¶
- Delete the import. Moving it into a function, behind
TYPE_CHECKINGor intoimportlibdoesn't help: the rule checks those too. - Declare what the importing context needs as a
typing.Protocolof its own, and receive the implementation from the composition root. INW002 checks the root too: put it in a module no context owns (an entry point such asshop.main), or in a context whosedepends-onlists both. The layer order still applies to it, and INW003 keeps it to each context's public modules. - If the dependency is intended, ask the user to add the other context to
depends-on. The hooks don't let an agent edit[tool.inwards].
Fix safety¶
No automatic fix (autofix: false). Whether a dependency between two contexts is a mistake or a decision not yet written down is the user's call.
Configuration¶
Severity error. The rule reads [[tool.inwards.contexts]] and does nothing without it; each context's depends-on lists the contexts it may import, and the configuration reference has every key. [tool.inwards.rules] can turn the rule off or make it a warning (Configure rules).
One import can be suppressed on its line, with a reason:
from shop.shipping.labels import print_label # inwards: ignore[INW002] reason="until shipping publishes events, #88"
Known limitations¶
- A context is a list of literal prefixes, so twenty slices mean twenty tables. Layers take selectors since #191; contexts don't yet.
- A name re-exported through a module no context owns passes:
from shop.shared import print_labelis an import ofshop.shared, whatevershop.sharedimports in turn. - A dynamic import whose target Inwards can't read isn't reported here. In a layer, INW011 reports it.
References¶
- INW001: the same idea for layers, which contexts add to.
- ADR-030: how contexts are declared, and why ownership is the longest prefix.