import-linter-probe
Two answers import-linter does not give you:
probe— if I add module X, which contract can host it?audit— which modules is no contract looking at?
Both exist because import-linter is a checker, not a planner. It tells you whether the code you have obeys the contracts you wrote. It has nothing to say about code you are about to write, and nothing at all to say about code no contract mentions.
pip install import-linter-probe # or: uv tool install import-linter-probe
Python 3.11+. No runtime dependencies — probe shells out to whichever
lint-imports your project already has, and audit needs nothing but the
config and the filesystem.
probe — find a contract home for a module you have not written
A module's contract home is a property of its import graph, so it cannot be looked up. It can only be measured. Doing that by hand means writing a stub, editing the config, running the linter, reading the output, and then undoing two edits correctly — which is the step people get wrong.
probe does all five, and guarantees the undo:
import-linter-probe probe \
--module app.usecases.signing_keys \
--imports app.services.signing_key_deps app.services.auth_service \
--contract key-exchange-isolation
Probing: app.usecases.signing_keys added to existing contract 'key-exchange-isolation'
Stub imports: app.services.signing_key_deps, app.services.auth_service
Contracts: 11 kept, 1 broken.
VERDICT: app.usecases.signing_keys is NOT accepted as written.
Broken contracts
----------------
...
--imports is the whole experiment. An empty stub passes every contract and
proves nothing, so the answer is only as honest as that list.
Probing a contract that does not exist yet
Pass the constraints and the contract is synthesised for the run, then removed:
# forbidden
import-linter-probe probe --module app.usecases.signing_keys \
--imports app.services.signing_key_deps \
--contract signing-keys-isolation \
--forbidden app.persistence.family app.persistence.evidence
# independence
import-linter-probe probe --module app.usecases.billing \
--imports app.domain.billing \
--contract context-independence \
--independence app.domain.orders app.domain.shipping
# layers — highest first, with --module in the position you are proposing
import-linter-probe probe --module app.usecases.billing \
--imports app.domain.billing \
--contract app-layers \
--layers app.api app.usecases.billing app.domain
Nothing is left behind
The probe writes a stub module (plus any missing package directories and their
__init__.py files) and one config edit. Every one of those is undone in a
finally, and the config is asserted byte-identical afterwards — if the
restore ever failed, you would get a loud message telling you to check
git diff before doing anything else, not a quiet mess.
Config edits are textual inserts rather than a parse-and-reserialise round trip, so even mid-probe the file keeps its comments, key order and formatting.
Exit code 0 means the host contract held; 1 means something broke; 2
means the probe could not be set up and nothing was written.
audit — find the modules no contract is looking at
import-linter only reports on modules named in some contract's
source_modules. A module in no contract is not passing — it is unexamined,
and lint-imports stays green while it drifts.
import-linter-probe audit
84 code modules under app: 71 in a contract, 12 declared unconstrained, 1 unexamined.
lint-imports is green on these by omission, not by inspection:
app.workflows.reconcile
By package:
1 app.workflows
A blind spot, not necessarily a bug — some of these are deliberately
cross-context. Decide each one; do not bulk-add. Give a module a
contract, or an unconstrained entry saying why it has none.
Empty __init__.py files and docstring-only modules are skipped: they cannot
import anything, so they cannot break a contract, and listing them would bury
the real findings.
Declaring a module unconstrained
Some modules genuinely are cross-context, and no contract can describe them honestly — settings, the database session factory, an exception-to-status map. Declare those with a reason, and the audit reports them separately instead of as a blind spot.
The reason is required. A bare list is a mute button; a reason is a record of a decision.
In .importlinter, setup.cfg or tox.ini:
[unconstrained]
app.config = infrastructure; settings only, imports no context
app.database = infrastructure; engine and session factory, imports no context
app.exception_map = maps every context's exceptions to status codes; one table by design
In pyproject.toml:
[tool.importlinter_probe.unconstrained]
"app.config" = "infrastructure; settings only, imports no context"
"app.database" = "infrastructure; engine and session factory, imports no context"
The audit also fails on declarations that have gone stale (the module was deleted) or contradict themselves (a contract covers it after all), so the list cannot rot silently.
A note on the INI form.
[unconstrained]sits in import-linter's own config file, and works because import-linter ignores sections it does not recognise. That holds for every version tested, but it is not a documented guarantee.[importlinter-probe:unconstrained]is accepted as an equivalent, slightly better-behaved spelling — worth preferring in a sharedsetup.cfgortox.ini. Thepyproject.tomlform is outside[tool.importlinter]entirely and carries no such risk.
Wiring the audit into your test suite
The audit is most useful as a ratchet: a number that may fall and must never rise. Everything the CLI uses is public API.
# tests/test_import_contract_coverage.py
from import_linter_probe import audit, load_config
# Every module is accounted for. A new module must arrive with a contract or an
# unconstrained entry saying why it has none; this must never rise.
MAX_UNEXAMINED = 0
def test_contract_coverage_does_not_regress() -> None:
result = audit(load_config())
assert result.problems == []
assert len(result.unexamined) <= MAX_UNEXAMINED, result.unexamined
Asserting an exact number rather than zero is deliberate when you are adopting this on an existing codebase: it turns the blind spot into a worklist you burn down, without blocking the first commit.
Or just add it to your lint step:
lint = "ruff check . && lint-imports && import-linter-probe audit"
Project layouts and configuration
The config is found the way git finds a repo — the nearest setup.cfg,
.importlinter, tox.ini or pyproject.toml at or above the working
directory that actually declares import-linter. Override with --config.
Root packages come from root_package / root_packages in that config, and
are looked for in ./<pkg> and ./src/<pkg>, so flat and src layouts both
work. src/app/util.py is reported as app.util, never src.app.util.
Override with --package-root.
probe runs lint-imports --no-cache, falling back to
uv run lint-imports --no-cache when the former is not on PATH. For anything
else — Poetry, Hatch, tox, a venv path — pass it explicitly:
import-linter-probe probe ... --lint-cmd "poetry run lint-imports --no-cache"
Note that --lint-cmd is used verbatim, so include --no-cache yourself: a
cached run will happily answer a question about the code as it was before the
stub existed.
Limitations
probeedits the config textually. For INI,source_modulesmust be in the indented multi-line form. For TOML, contracts are addressed by theirname, since an array-of-tables entry has no other identity.--layersbuilds the contract from the list you give; the probed module must be one of the layers, and where you put it is the proposal being tested.- The verdict is parsed from
lint-importsconsole output. It is pinned by tests against real import-linter, but it is not a stable API.
Development
uv sync
uv run pytest # the end-to-end tests run real import-linter
uv run ruff check src tests
uv run mypy src
See CONTRIBUTING.md for the ground rules, SECURITY.md for the trust model, and CHANGELOG.md for release notes.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file import_linter_probe-0.1.0.tar.gz.
File metadata
- Download URL: import_linter_probe-0.1.0.tar.gz
- Upload date:
- Size: 21.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4de0aa43d2c6c127107152959d74d92febb1d2df690a85bd2688f40109b6d992
|
|
| MD5 |
b65acf9efe2414e5ef6dd7aefbbfb330
|
|
| BLAKE2b-256 |
fa0415355c0ba5297d66228e52f216e804b1971ea97485d30123bf734d3ffa7c
|
Provenance
The following attestation bundles were made for import_linter_probe-0.1.0.tar.gz:
Publisher:
release.yml on geuben/import-linter-probe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
import_linter_probe-0.1.0.tar.gz -
Subject digest:
4de0aa43d2c6c127107152959d74d92febb1d2df690a85bd2688f40109b6d992 - Sigstore transparency entry: 2546300787
- Sigstore integration time:
-
Permalink:
geuben/import-linter-probe@d626ac243e6019cec7daba2afca621b3a5c66aff -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/geuben
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d626ac243e6019cec7daba2afca621b3a5c66aff -
Trigger Event:
release
-
Statement type:
File details
Details for the file import_linter_probe-0.1.0-py3-none-any.whl.
File metadata
- Download URL: import_linter_probe-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2435875f17ab023f4eeaa293895e0de4b1636f220cee447f6a491740d871005
|
|
| MD5 |
a7b22dc8da8635d6bbe735cf7a2b8939
|
|
| BLAKE2b-256 |
2aead5c4125facc3b5bf65bf74a3b8187358f8e2f93558d01fc0c99b28b7df53
|
Provenance
The following attestation bundles were made for import_linter_probe-0.1.0-py3-none-any.whl:
Publisher:
release.yml on geuben/import-linter-probe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
import_linter_probe-0.1.0-py3-none-any.whl -
Subject digest:
b2435875f17ab023f4eeaa293895e0de4b1636f220cee447f6a491740d871005 - Sigstore transparency entry: 2546300849
- Sigstore integration time:
-
Permalink:
geuben/import-linter-probe@d626ac243e6019cec7daba2afca621b3a5c66aff -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/geuben
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d626ac243e6019cec7daba2afca621b3a5c66aff -
Trigger Event:
release
-
Statement type: