Usage¶
cleanporter is one command. By default it checks; passing --fix makes it
rewrite.
cleanporter [--fix] [--diff] [--python PATH] [--exempt MODULE] [--root PATH]
[--strict] [--version] [paths ...]
Positional arguments¶
| Argument | Meaning |
|---|---|
paths |
Files or directories to process. Defaults to .. Directories are walked recursively for *.py files; a path you name explicitly is always processed, even if it is excluded by configuration. |
Flags¶
| Flag | Meaning |
|---|---|
--fix |
Rewrite violations in place, but only where the rewrite is provably safe. Files that cannot be proven safe are left byte-for-byte unchanged and reported as CP003. |
--diff |
Show the rewrite as a unified diff on stdout without writing anything. Ignored when --fix is also given: --fix wins and writes (it prints the same diff on its way through). |
--python PATH |
Interpreter used to classify stdlib and third-party names. Default: the interpreter running cleanporter. |
--exempt MODULE |
An additional module whose members may be imported by name. Repeatable. Adds to, never replaces, the default exemptions and anything in exempt_modules. |
--root PATH |
An additional first-party import root — a directory that is on sys.path for the code being analysed. Repeatable. Adds to whatever the analysed paths themselves imply, and to source_roots. A relative value is resolved against the directory holding your pyproject.toml, not against the current directory. |
--strict |
Also fail (exit 1) on imports that could not be classified (CP002). Equivalent to turning on treat_unresolved_as_error for this run. |
--show-skipped |
List the imports a skip rule took out of the run (CP004). They are counted in the summary either way; this prints them, which is how you check what a pattern actually swallowed. |
--version |
Print the version and exit. |
--help |
Print usage and exit. |
Every flag except --python and --version is additive with the
configuration file rather than overriding it — see
Configuration.
Finding codes¶
Each reported line has the shape
PATH:LINE:COLUMN: CODE message.
| Code | Status | Meaning |
|---|---|---|
CP001 |
VIOLATION |
An object is imported by name. This is the rule being enforced, and it is what blocks CI. |
CP002 |
UNRESOLVED |
cleanporter could not determine whether the symbol is a module. Never rewritten. Only counts toward the failure exit code under --strict / treat_unresolved_as_error. |
CP003 |
SKIPPED |
Structurally a violation, deliberately not rewritten. This is the "declined, because…" note that explains why --fix or --diff left a file alone. |
CP004 |
SKIPPED_BY_CONFIG |
Matched a skip rule, so it was never analysed. Counted in the summary, printed only under --show-skipped, and never part of the exit code — you asked for it. |
Examples of each:
src/mypkg/consumer.py:3:0: CP001 imports object 'Widget' from module 'mypkg.helpers'; import the module and use 'helpers.Widget'
src/mypkg/gpu.py:5:0: CP002 could not determine whether 'cupy.ndarray' is a module: 'cupy' is not importable in the target interpreter
src/mypkg/api.py:11:0: CP003 file not rewritten: local 'Widget' is rebound in the same scope
src/mypkg/stencils.py:4:0: CP004 'broadcast' from 'gt4py.next' skipped by configuration: skip rule #1 (decorator='field_operator'): DSL bodies are re-parsed by the frontend
CP002 findings are only produced for imports cleanporter actually looked at:
exempt modules and (under scope = "first-party") third-party modules are
skipped before resolution is attempted.
CP003 findings count toward the failure exit code
A file the fixer declined still contains a violation, so CP003 is not
purely informational: like CP001, it makes the run exit 1.
Most CP003 findings are the fixer explaining a decision, so they only
appear under --fix or --diff. The one exception is a wildcard import
(from x import *), which is reported as CP003 in every mode — there is
no module import that reproduces it, so it can never be rewritten.
CP004 findings never count
A CP004 is your own configuration reporting back, not a problem found in
your code, so it never contributes to the failure exit code — not even
under --strict. It is still counted in the summary line, because a rule
broad enough to swallow a project should be visible without having to go
looking for it.
That is a claim about CP004, not about skip rules in general. A rule
can still change a run's exit code, in one direction: if it matches the
spelling --fix is about to write rather than the one in your source,
the file is declined with a CP003, which does count. See
when a rewrite would create its own skipped region.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Clean — nothing remains to report. |
1 |
Violations found (or left behind after --fix). |
2 |
Operational error: a file that could not be parsed or decoded, or a malformed [tool.cleanporter] table. |
In short: 0 = clean, 1 = violations, 2 = operational error.
Exit 2 takes precedence: if any input file failed to parse, the run reports
2 regardless of what else it found. A path on the command line that does not
exist is a warning, not an error — it is reported and skipped.
Where output goes¶
This matters if you intend to pipe anything.
- Plain check mode (no
--fix, no--diff): there is no patch, so warnings, findings and the summary all go to stdout, as usual. --diffor--fix: stdout carries only the patch. Warnings, parse errors, findings, thefixed: <path>lines and the summary are all redirected to stderr. Diff headers are relative to the current working directory, so the stream is a valid patch thatgit applyaccepts.
That is what makes this work:
All diffs for a run are concatenated into that one stream rather than written as separate patch files.
Workflows¶
As a CI gate¶
Check mode is the gate. Exit 1 on any CP001, so no extra scripting is
needed:
If you want unresolvable imports to fail the build too — useful once your
dependency set is stable and every import should be classifiable in CI —
add --strict:
If CI runs in a different environment from the one your code targets, point the classifier at the interpreter that actually has your dependencies installed:
Reviewing before applying¶
--diff never writes. Read the patch, then apply it in one step if you like
it:
Because findings go to stderr in this mode, add 2>/dev/null if you want the
patch alone on your terminal, or 2>&1 >/dev/null if you want only the
findings.
Doing a --fix sweep¶
git switch -c chore/import-style # a dedicated branch: the diff can be large
cleanporter --fix src/ tests/ # rewrite what is provably safe
git diff # review
uv run pytest # re-run the suite -- see the warning below
--fix prints the diff for every changed file to stdout and a
fixed: <path> line to stderr, then a summary:
Anything still reported after the sweep is a CP001 the fixer never planned
(a semicolon-joined or one-line import), a CP003 it deliberately declined,
or a CP002 it could not classify. All three need a human. A CP004 does
not — that one is your own skip rule, and it is only printed if you ask for
it with --show-skipped.
Guards are per file — re-run your tests
cleanporter proves safety by analysing the file it is rewriting. A string
in a different file that names the rewritten binding by its dotted path
— monkeypatch.setattr("pkg.cli.helper", ...), an entry point in
pyproject.toml, an importlib lookup — is invisible to that analysis,
so --fix can make such a reference stale even though the rewritten file
itself is correct. Whenever it writes a file, --fix prints a note to
stderr saying exactly this.
Import layout¶
cleanporter does not re-sort or reflow imports; it inserts or replaces a statement in place. Run your formatter afterwards if the new import lands somewhere you would rather it did not: