lanorme 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lanorme-0.5.1/.gitignore +9 -0
- lanorme-0.5.1/CHANGELOG.md +250 -0
- lanorme-0.5.1/LICENSE +21 -0
- lanorme-0.5.1/PKG-INFO +220 -0
- lanorme-0.5.1/README.md +195 -0
- lanorme-0.5.1/benchmarks/README.md +30 -0
- lanorme-0.5.1/docs/RULES.md +521 -0
- lanorme-0.5.1/pyproject.toml +98 -0
- lanorme-0.5.1/src/lanorme/__init__.py +139 -0
- lanorme-0.5.1/src/lanorme/__main__.py +8 -0
- lanorme-0.5.1/src/lanorme/checks/__init__.py +5 -0
- lanorme-0.5.1/src/lanorme/checks/attribute_access.py +188 -0
- lanorme-0.5.1/src/lanorme/checks/comments.py +368 -0
- lanorme-0.5.1/src/lanorme/checks/domain_terms.py +231 -0
- lanorme-0.5.1/src/lanorme/checks/duplication.py +237 -0
- lanorme-0.5.1/src/lanorme/checks/file_limits.py +400 -0
- lanorme-0.5.1/src/lanorme/checks/forbidden_paths.py +87 -0
- lanorme-0.5.1/src/lanorme/checks/layer_deps.py +280 -0
- lanorme-0.5.1/src/lanorme/checks/meta.py +191 -0
- lanorme-0.5.1/src/lanorme/checks/named_args.py +240 -0
- lanorme-0.5.1/src/lanorme/checks/naming_consistency.py +345 -0
- lanorme-0.5.1/src/lanorme/checks/pattern_divergence.py +299 -0
- lanorme-0.5.1/src/lanorme/checks/port_coverage.py +462 -0
- lanorme-0.5.1/src/lanorme/checks/prose.py +256 -0
- lanorme-0.5.1/src/lanorme/checks/restating.py +336 -0
- lanorme-0.5.1/src/lanorme/checks/secrets.py +280 -0
- lanorme-0.5.1/src/lanorme/checks/security_calls.py +397 -0
- lanorme-0.5.1/src/lanorme/checks/security_patterns.py +406 -0
- lanorme-0.5.1/src/lanorme/checks/stale_paths.py +165 -0
- lanorme-0.5.1/src/lanorme/checks/stray_artifacts.py +180 -0
- lanorme-0.5.1/src/lanorme/checks/strong_types.py +278 -0
- lanorme-0.5.1/src/lanorme/checks/test_coverage.py +176 -0
- lanorme-0.5.1/src/lanorme/checks/test_style.py +287 -0
- lanorme-0.5.1/src/lanorme/cli.py +518 -0
- lanorme-0.5.1/src/lanorme/discovery.py +101 -0
- lanorme-0.5.1/tests/fixtures/configured/pyproject.toml +2 -0
- lanorme-0.5.1/tests/fixtures/prose/pyproject.toml +2 -0
lanorme-0.5.1/.gitignore
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable user-facing changes to LaNorme. The public API is the set of
|
|
4
|
+
rule codes that may appear in `select` / `ignore` / `per-file-ignores`
|
|
5
|
+
and the configuration keys under `[tool.lanorme]`. See the Versioning
|
|
6
|
+
section in the README for the breaking-change policy.
|
|
7
|
+
|
|
8
|
+
This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.5.1]
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- First release published to PyPI: `uv tool install lanorme` (or
|
|
17
|
+
`pipx install lanorme` / `pip install lanorme`). Publishing runs through
|
|
18
|
+
PyPI Trusted Publishing from GitHub Actions on each GitHub Release, so no
|
|
19
|
+
API token is stored.
|
|
20
|
+
- The project moved to the `lanorme` GitHub organisation; all project URLs
|
|
21
|
+
now point to `github.com/lanorme/lanorme` (the old paths redirect).
|
|
22
|
+
|
|
23
|
+
## [0.5.0]
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `attribute_access` check, opt-in (default-off), advisory warnings:
|
|
28
|
+
- `ATTR-001`: `hasattr(x, "name")` with a literal identifier name (duck
|
|
29
|
+
typing; prefer a `runtime_checkable` Protocol with `isinstance`, or
|
|
30
|
+
EAFP).
|
|
31
|
+
- `ATTR-002`: `getattr(x, "name")` (no default), `setattr(x, "name", v)`,
|
|
32
|
+
or `delattr(x, "name")` with a literal identifier name (use direct
|
|
33
|
+
attribute access `x.name`).
|
|
34
|
+
- High-confidence cases only: three-argument `getattr` with a default,
|
|
35
|
+
dunder names, non-identifier names, and files under `tests/` are exempt.
|
|
36
|
+
Dynamic (non-literal) names are exempt unless `flag_dynamic = true`.
|
|
37
|
+
Enable via `[tool.lanorme.attribute_access] enabled = true`. The default
|
|
38
|
+
was chosen by measuring the rule against the Python standard library,
|
|
39
|
+
where `hasattr` is dominated by legitimate platform/feature detection
|
|
40
|
+
(no Protocol fix), so the check ships off.
|
|
41
|
+
- `Configurable` protocol in the public API: a `runtime_checkable` Protocol
|
|
42
|
+
for checks that accept a `[tool.lanorme.<name>]` settings table. The CLI
|
|
43
|
+
now selects configurable checks with `isinstance(check, Configurable)`.
|
|
44
|
+
|
|
45
|
+
## [0.4.0]
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Top-level `[tool.lanorme] source_root` key. It decouples the architectural
|
|
50
|
+
root from the scan target for the two layout-aware checks (`layer_deps` and
|
|
51
|
+
`port_coverage`) only. With `source_root = "src/pkg"`, a single
|
|
52
|
+
`lanorme check .` from the repo root classifies layers under
|
|
53
|
+
`src/pkg/domain/`, `src/pkg/api/`, etc., while every other check keeps
|
|
54
|
+
scanning the whole tree. `composition_root`, `ports_dir`, and
|
|
55
|
+
`adapter_roots` are interpreted relative to `source_root`. A scanned file
|
|
56
|
+
that is not under `source_root` is layer-exempt (skipped by `layer_deps` /
|
|
57
|
+
`port_coverage`, never flagged), but is still seen by every other check.
|
|
58
|
+
Reported violation paths stay relative to the scan target, so `--exclude`,
|
|
59
|
+
`[tool.lanorme.per-file-ignores]`, and `# noqa` line up unchanged.
|
|
60
|
+
`source_root` is resolved against the scan target (for the intended
|
|
61
|
+
`lanorme check .` from the repo root these are the same directory).
|
|
62
|
+
- `exclude` globs are now honoured at file-discovery time, not only in the
|
|
63
|
+
post-filter. An excluded directory is pruned during the walk, so a large
|
|
64
|
+
excluded subtree (`postman/**`, `docs/generated/**`, ...) is no longer read.
|
|
65
|
+
The CLI still post-filters by the same globs as a safety net.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- A built-in set of never-source directories is pruned during every check's
|
|
70
|
+
tree walk regardless of configuration: `.git`, `.venv`, `venv`,
|
|
71
|
+
`node_modules`, `__pycache__`, `dist`, `build`, `.ruff_cache`,
|
|
72
|
+
`.pytest_cache`, `.mypy_cache`. This makes `lanorme check .` fast out of the
|
|
73
|
+
box (it no longer descends into a virtualenv or build tree). This is a
|
|
74
|
+
behaviour change not gated behind a config key: a project that deliberately
|
|
75
|
+
kept first-party `.py` files under one of these directory names would no
|
|
76
|
+
longer have them scanned. The `stray_artifacts` check already pruned the
|
|
77
|
+
same set, so its `JUNK` rules are unaffected.
|
|
78
|
+
|
|
79
|
+
## [0.3.0]
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- `layer_deps` and `port_coverage` are now configurable via
|
|
84
|
+
`[tool.lanorme.layer_deps]` and `[tool.lanorme.port_coverage]`. All keys
|
|
85
|
+
are optional and the built-in defaults reproduce the previous behaviour.
|
|
86
|
+
- `layer_deps`: `composition_root` (path globs), `layers`, and a nested
|
|
87
|
+
`[tool.lanorme.layer_deps.allowed]` table mapping each layer to the
|
|
88
|
+
layers it may import.
|
|
89
|
+
- `port_coverage`: `ports_dir`, `adapter_roots`, `composition_root`
|
|
90
|
+
(path globs), `skip_files`, `ports_without_impl`.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- The composition root is now matched with `fnmatch` globs against the
|
|
95
|
+
source-relative path, in both `layer_deps` (LAYER-005) and
|
|
96
|
+
`port_coverage` (PORT-003), instead of a directory `startswith` /
|
|
97
|
+
substring test. A module **file** such as `api/dependencies.py` can now
|
|
98
|
+
be a composition root; previously only an `api/dependencies/` **directory**
|
|
99
|
+
was recognised. The defaults match the same paths as before, so projects
|
|
100
|
+
that do not set the new keys see no change.
|
|
101
|
+
- LAYER-005 rule text changed from "only `api/dependencies/` may import
|
|
102
|
+
from infrastructure" to "only the composition root may import from
|
|
103
|
+
infrastructure" (it is now configurable).
|
|
104
|
+
- `port_coverage` now scans each adapter root **recursively** (`rglob`),
|
|
105
|
+
where it previously scanned only the top level of
|
|
106
|
+
`infrastructure/services/`. A project with adapter files in
|
|
107
|
+
subdirectories of an adapter root may now see those files evaluated by
|
|
108
|
+
PORT-001 and contribute to PORT-002 coverage. For a flat
|
|
109
|
+
`infrastructure/services/*.py` layout there is no change. This is the one
|
|
110
|
+
behaviour change not gated behind a config key.
|
|
111
|
+
|
|
112
|
+
## [0.2.0]
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- `CMT-001` recall pushed from 0.667 to 1.000 by extending the comment-as-
|
|
117
|
+
code parser with wrapping strategies for the shapes `ast.parse` rejects
|
|
118
|
+
standalone: block headers (`if x:`, `for x in y:`, `def f():`, `class C:`)
|
|
119
|
+
are tried with a `pass` body; `try:` adds a synthetic `except`; `elif` /
|
|
120
|
+
`else` / `except` / `finally` are tried inside their parent block; bare
|
|
121
|
+
`return` / `yield` / `raise` are tried inside a synthetic `def _():`; and
|
|
122
|
+
decorator lines (`@foo`) are tried followed by `def _(): pass`. Also
|
|
123
|
+
added `ast.If` / `ast.Try` / `ast.Match` / `ast.Return` to the code-node
|
|
124
|
+
whitelist. Scored: **F1 = 0.793 -> 0.992** (P = 0.978 -> 0.985,
|
|
125
|
+
R = 0.667 -> 1.000).
|
|
126
|
+
- `CMT-005` moved from the `comments` check to a new `restating` check.
|
|
127
|
+
Same rule code, same precision-funnel detector (P = 1.000, R = 0.418,
|
|
128
|
+
F1 = 0.589). Configuration key changed from `[tool.lanorme.comments]
|
|
129
|
+
restating = true` to `[tool.lanorme.restating] enabled = true`.
|
|
130
|
+
- `SQL-001` rewritten AST-based: only flags raw SQL that reaches a
|
|
131
|
+
database execution sink (`.execute` / `.executemany` /
|
|
132
|
+
`.executescript` on a DB-shaped receiver, or `read_sql` /
|
|
133
|
+
`read_sql_query`). Unwraps `text(...)` constructors, resolves
|
|
134
|
+
module-level and function-local string constants, treats `+` /
|
|
135
|
+
`%`-formatted / `.format`-built SQL as interpolated, and recognises
|
|
136
|
+
parameterised executes (placeholder + params arg) as safe.
|
|
137
|
+
Scored against the bundled corpus: **F1 = 0.761 -> 1.000**
|
|
138
|
+
(P = 0.686 -> 1.000, R = 0.854 -> 1.000).
|
|
139
|
+
- `SECRETPY-001` rewritten AST-based: flags credential-named
|
|
140
|
+
assignments (variable, dict-literal key, call kwarg) plus shape-only
|
|
141
|
+
matches (PEM, JWT, Bearer, DB-URL-with-creds, vendor-prefixed
|
|
142
|
+
tokens for AWS / GitHub / Slack / Stripe). Placeholder markers
|
|
143
|
+
(`<your-...>`, `REPLACE_ME`, `example`, ...) skip a value unless it
|
|
144
|
+
is high-entropy enough (32+ chars, mixed case, digits) to defeat the
|
|
145
|
+
marker. Scored: **F1 = 0.658 -> 1.000** (P = 0.758 -> 1.000,
|
|
146
|
+
R = 0.581 -> 1.000).
|
|
147
|
+
- `SECRETPY-001` moved from the `security_patterns` check to a new
|
|
148
|
+
`secrets` check. Rule code unchanged; the check name changed.
|
|
149
|
+
Users running `lanorme check . --check=security_patterns` no longer
|
|
150
|
+
get SECRETPY-001 flags; run `--check=secrets` instead, or rely on
|
|
151
|
+
the default-on full run.
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
|
|
155
|
+
- `lanorme rule <CODE>` CLI subcommand. Prints the matching section of
|
|
156
|
+
`docs/RULES.md` (bundled into the wheel) for one rule code; exits 2
|
|
157
|
+
with a helpful pointer if the code is unknown.
|
|
158
|
+
- Project metadata for distribution: `authors`, `[project.urls]`
|
|
159
|
+
(homepage / repository / issues / changelog), `Development Status ::
|
|
160
|
+
4 - Beta`, Python 3.14 classifier, `Typing :: Typed`, additional
|
|
161
|
+
keywords.
|
|
162
|
+
- `[dependency-groups] dev = ["pytest>=8"]`: contributors install with
|
|
163
|
+
`uv sync --group dev` and run the unit suite with
|
|
164
|
+
`uv run pytest tests/unit`.
|
|
165
|
+
- `[tool.hatch.build.targets.sdist]` selects the publishable artefacts
|
|
166
|
+
(src, README, CHANGELOG, LICENSE, docs/RULES.md, pyproject) for the
|
|
167
|
+
source distribution.
|
|
168
|
+
- `security_calls` check, six default-on dangerous-call rules, each one
|
|
169
|
+
AST node and precision-first: `SHELL-001` (subprocess `shell=True`,
|
|
170
|
+
`os.system`, `os.popen`), `DESERIAL-001` (pickle / marshal / dill /
|
|
171
|
+
`yaml.load` without `SafeLoader`), `EVAL-001` (`eval` / `exec` /
|
|
172
|
+
`compile` on a non-literal first argument), `CRYPTO-001` (`md5` / `sha1`
|
|
173
|
+
used for security, deprecated TLS protocol constants), `TLS-001`
|
|
174
|
+
(`verify=False` in `requests` / `httpx` / `aiohttp`, `ssl.CERT_NONE`,
|
|
175
|
+
`ssl._create_unverified_context`), `DEBUG-001` (`Flask` / `FastAPI`
|
|
176
|
+
constructors and `app.run` calls with `debug=True`, `DEBUG = True` at
|
|
177
|
+
module scope in `*settings.py` / `*config.py`).
|
|
178
|
+
- `[tool.lanorme.per-file-ignores]` TOML table: map a path glob to a
|
|
179
|
+
list of rule codes (full code such as `SQL-001` or a category prefix
|
|
180
|
+
such as `SQL`) that should never fire for matching files.
|
|
181
|
+
- `# noqa` inline suppression. Bare `# noqa` silences any rule on the
|
|
182
|
+
line it sits on; `# noqa: CODE1,CODE2` silences only the listed codes
|
|
183
|
+
(full codes or category prefixes). Case-insensitive.
|
|
184
|
+
- `test_style` check with `AAA-001` (test functions must carry inline
|
|
185
|
+
Arrange / Act / Assert section comments, or Given / When / Then) and
|
|
186
|
+
`AAA-002` (test functions in the same file must not share an identical
|
|
187
|
+
arrange prefix). Default-off; enable via
|
|
188
|
+
`[tool.lanorme.test_style] enabled = true`.
|
|
189
|
+
- `CHANGELOG.md`: `docs/RULES.md` per-rule reference, versioning policy
|
|
190
|
+
in README.
|
|
191
|
+
|
|
192
|
+
### Changed (breaking)
|
|
193
|
+
|
|
194
|
+
- **Rule renames** so each rule's code matches its actual scope:
|
|
195
|
+
- `AUTH-001` → `AUTHN-001` (the check verifies authentication
|
|
196
|
+
presence; it does not measure authorisation).
|
|
197
|
+
- `TEST-001` → `TESTFILE-001` (it verifies that a `test_*.py`
|
|
198
|
+
partner exists; it does not measure coverage).
|
|
199
|
+
- `SECRET-001` → `SECRETPY-001` (Python-source scope only;
|
|
200
|
+
`.env` / `*.yaml` / `*.ipynb` are out of scope until a future
|
|
201
|
+
`SECRET-002` / `SECRET-003` lands).
|
|
202
|
+
- **Default demotions** based on the multi-reviewer audit
|
|
203
|
+
(`docs/audit/SUMMARY.md`). All previously default-on, now opt-in:
|
|
204
|
+
- `NAMING-001` (repository CRUD prefixes). Opt-in via
|
|
205
|
+
`[tool.lanorme.naming_consistency] repo_crud = true`.
|
|
206
|
+
- `NAMING-002` (service CRUD prefixes). Opt-in via
|
|
207
|
+
`service_crud = true`. These two rules actively conflicted with
|
|
208
|
+
`TERM-NNN` on a serious DDD project; the audit's biggest single
|
|
209
|
+
finding.
|
|
210
|
+
- `KWARG-001` (`bare *` on every multi-argument function). Opt-in
|
|
211
|
+
via `[tool.lanorme.named_args] enabled = true`.
|
|
212
|
+
- `AAA-001` / `AAA-002`. Opt-in via
|
|
213
|
+
`[tool.lanorme.test_style] enabled = true`.
|
|
214
|
+
- **Earlier rename pass** (commit `4703490`):
|
|
215
|
+
- `SEC-001` → `AUTH-001` (later renamed again, see above).
|
|
216
|
+
- `SEC-002` → `SQL-001`, `SEC-003` → `SECRET-001` (later
|
|
217
|
+
renamed again, see above).
|
|
218
|
+
- `SIZE-004` → `COMPLEXITY-001` (cyclomatic complexity is not a
|
|
219
|
+
size).
|
|
220
|
+
- `SIZE-005` → `PARAM-001` (parameter count is not a size).
|
|
221
|
+
- `PATTERN-001` → `IMPORT-001`, `PATTERN-002` → `TYPING-001`
|
|
222
|
+
(later removed), `PATTERN-004` → `ENDPOINT-001`.
|
|
223
|
+
- `PROJ-001` → `PATH-001`.
|
|
224
|
+
- `ART-001` / `ART-002` → `JUNK-001` / `JUNK-002`.
|
|
225
|
+
- `NAMED-001` → `KWARG-001`.
|
|
226
|
+
- **CMT-003 / CMT-004 unified under PROSE-001 / PROSE-003** (one rule
|
|
227
|
+
code per intent regardless of file type). The `comments` check now
|
|
228
|
+
emits `PROSE-001` and `PROSE-003` on Python comments and docstrings
|
|
229
|
+
when `em_dash` / `emoji` are enabled; the `prose` check still emits
|
|
230
|
+
the same codes on Markdown.
|
|
231
|
+
|
|
232
|
+
### Removed (breaking)
|
|
233
|
+
|
|
234
|
+
- `TEST-002` (weak blank-line AAA heuristic). Superseded by `AAA-001`
|
|
235
|
+
(comment-marker AAA in the `test_style` check, stronger signal).
|
|
236
|
+
- `TYPING-001` (no `TYPE_CHECKING` outside model files). Three of four
|
|
237
|
+
reviewers in the audit flagged the rule's premise as inverting the
|
|
238
|
+
community typing consensus (ruff's `TCH` family actively promotes
|
|
239
|
+
`TYPE_CHECKING` guards). The helper was also unwired in practice. May
|
|
240
|
+
return as an opt-in with the premise inverted (encourage rather than
|
|
241
|
+
forbid).
|
|
242
|
+
|
|
243
|
+
## [0.1.0] - initial commit `aaef1f5`
|
|
244
|
+
|
|
245
|
+
First public-shape release. Eighteen checks extracted from a private
|
|
246
|
+
codebase, fully de-identified, with a configurable CLI (`lanorme check`,
|
|
247
|
+
`lanorme rules`), TOML-driven configuration (`[tool.lanorme]` in
|
|
248
|
+
`pyproject.toml` or a dedicated `lanorme.toml`), and a plugin model
|
|
249
|
+
through `[project.entry-points."lanorme.checks"]` or
|
|
250
|
+
`[tool.lanorme] plugins = [...]`.
|
lanorme-0.5.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LaNorme contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lanorme-0.5.1/PKG-INFO
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lanorme
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: La norme: a configurable, pluggable architecture and code-quality linter for Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lanorme/lanorme
|
|
6
|
+
Project-URL: Repository, https://github.com/lanorme/lanorme
|
|
7
|
+
Project-URL: Issues, https://github.com/lanorme/lanorme/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/lanorme/lanorme/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: architecture,checks,code-quality,ddd,hexagonal,linter,static-analysis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# LaNorme
|
|
27
|
+
|
|
28
|
+
A linter for Python. It checks the usual things, dead code, file and function
|
|
29
|
+
size, complexity, weak types, hardcoded secrets, dangerous calls, and a few
|
|
30
|
+
things most linters do not: hexagonal layer boundaries, ports-and-adapters
|
|
31
|
+
wiring, and a project's own naming vocabulary.
|
|
32
|
+
|
|
33
|
+
Standard library only. No runtime dependencies. Python 3.13+.
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
$ lanorme check .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
From PyPI:
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
uv tool install lanorme # or: pipx install lanorme, pip install lanorme
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run it once without installing anything:
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
uvx lanorme check .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install straight from source:
|
|
54
|
+
|
|
55
|
+
```console
|
|
56
|
+
uv tool install "git+https://github.com/lanorme/lanorme@v0.5.1"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Releases are tagged `vX.Y.Z`; see the [releases page](https://github.com/lanorme/lanorme/releases) for notes.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```console
|
|
64
|
+
lanorme check [PATHS...] # run every enabled check (default path: .)
|
|
65
|
+
lanorme check . --check=secrets # run one check by name
|
|
66
|
+
lanorme check . --select TYPE,AUTHN # only these rule codes or categories
|
|
67
|
+
lanorme check . --ignore NAMING-003 # skip specific rules
|
|
68
|
+
lanorme check . --output-format json
|
|
69
|
+
lanorme rules # list every registered rule
|
|
70
|
+
lanorme rule SQL-001 # show the reference for one rule
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Exit code is `1` when any check fails, `0` when the tree is clean.
|
|
74
|
+
|
|
75
|
+
A run looks like this:
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
$ lanorme check src/
|
|
79
|
+
[FAIL] secrets
|
|
80
|
+
VIOLATION: app.py:8 — Hardcoded credential value bound to 'API_KEY'
|
|
81
|
+
Rule: SECRETPY-001: No hardcoded secrets in source code
|
|
82
|
+
Fix: Read the value from an environment variable, secrets manager, or settings module
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Suppressing a finding
|
|
86
|
+
|
|
87
|
+
A `# noqa` at the end of a line silences every rule on that line; `# noqa: CODE`
|
|
88
|
+
silences only the listed codes (a full code like `SQL-001` or a category like
|
|
89
|
+
`SQL`):
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
def legacy_handler(req): # noqa: KWARG-001
|
|
93
|
+
return req.text # noqa
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For whole directories, use the per-file table in your config (below).
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
LaNorme walks up from the target path looking for config: a dedicated
|
|
101
|
+
`lanorme.toml`, otherwise a `[tool.lanorme]` table in `pyproject.toml`. Command
|
|
102
|
+
line flags win over both.
|
|
103
|
+
|
|
104
|
+
```toml
|
|
105
|
+
[tool.lanorme]
|
|
106
|
+
select = ["ALL"] # rule codes or categories to run
|
|
107
|
+
ignore = ["NAMING-003"] # rule codes or categories to skip
|
|
108
|
+
exclude = ["postman/**", "vendor/*"] # path globs, pruned at walk time
|
|
109
|
+
source_root = "src/myproject" # architectural root for layer_deps/port_coverage
|
|
110
|
+
plugins = ["myproject.checks.house_rules"] # extra check modules to load
|
|
111
|
+
|
|
112
|
+
# Silence specific rules for matching paths (the file is still scanned).
|
|
113
|
+
[tool.lanorme.per-file-ignores]
|
|
114
|
+
"tests/**/*.py" = ["AAA", "SECRETPY"]
|
|
115
|
+
"alembic/**/*.py" = ["SQL"]
|
|
116
|
+
"notebooks/*.py" = ["KWARG", "DRY"]
|
|
117
|
+
|
|
118
|
+
# Each per-check table is handed to that check.
|
|
119
|
+
[tool.lanorme.stray_artifacts]
|
|
120
|
+
extensions = [".zip", ".pdf"] # also flag these (JUNK-002)
|
|
121
|
+
allow = ["docs/diagram.png"] # never flag these (globs)
|
|
122
|
+
|
|
123
|
+
[tool.lanorme.forbidden_paths]
|
|
124
|
+
dirs = ["legacy_src", "build_artifacts"] # these directories must not exist
|
|
125
|
+
|
|
126
|
+
[[tool.lanorme.domain_terms.rules]]
|
|
127
|
+
id = "TERM-001"
|
|
128
|
+
canonical = "Account"
|
|
129
|
+
forbidden = ["Acct", "Acnt"]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`exclude` globs are pruned during the walk, not just filtered from output, so a
|
|
133
|
+
large excluded subtree is never read. A built-in set of never-source
|
|
134
|
+
directories (`.git`, `.venv`, `venv`, `node_modules`, `__pycache__`, `dist`,
|
|
135
|
+
`build`, `.ruff_cache`, `.pytest_cache`, `.mypy_cache`) is always pruned, so
|
|
136
|
+
`lanorme check .` is fast out of the box.
|
|
137
|
+
|
|
138
|
+
`source_root` applies only to the two layout-aware checks (`layer_deps`,
|
|
139
|
+
`port_coverage`). It lets you run `lanorme check .` from the repo root while the
|
|
140
|
+
hexagonal layers live under a nested package: layers are classified relative to
|
|
141
|
+
`source_root`, files outside it are layer-exempt, and `composition_root` /
|
|
142
|
+
`ports_dir` / `adapter_roots` are read relative to it. Every other check still
|
|
143
|
+
scans the whole tree.
|
|
144
|
+
|
|
145
|
+
## What it checks
|
|
146
|
+
|
|
147
|
+
`lanorme rules` prints the live list. Each rule, what it catches and does not,
|
|
148
|
+
its config, and where measured its precision and recall on the bundled test
|
|
149
|
+
corpora, is in [`docs/RULES.md`](docs/RULES.md).
|
|
150
|
+
|
|
151
|
+
On by default, on any project, no config needed:
|
|
152
|
+
|
|
153
|
+
| Rule | Catches |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `CMT-001/002` | commented-out code, over-long comment blocks |
|
|
156
|
+
| `DRY-001` | near-duplicate function bodies |
|
|
157
|
+
| `SIZE-001..003` / `COMPLEXITY-001` / `PARAM-001` | file, function and class size; cyclomatic complexity; parameter count |
|
|
158
|
+
| `IMPORT-001` / `ENDPOINT-001` | imports inside function bodies; deeply nested endpoints |
|
|
159
|
+
| `NAMING-003/004` | HTTP-verb-to-handler match; boolean-prefix predicates |
|
|
160
|
+
| `TYPE-001..003` | `dict[str, Any]`, bare containers, untyped `**kwargs` |
|
|
161
|
+
| `AUTHN-001` / `SQL-001` / `SECRETPY-001` | mutation endpoints without an auth dependency; raw SQL at a database call; hardcoded secrets in `.py` |
|
|
162
|
+
| `SHELL-001` / `DESERIAL-001` / `EVAL-001` / `CRYPTO-001` / `TLS-001` / `DEBUG-001` | shell injection, unsafe deserialisation, `eval`/`exec`, weak hashes, disabled TLS, debug mode |
|
|
163
|
+
| `JUNK-001/002` | screenshots, scratch files, OS junk, stray binaries |
|
|
164
|
+
| `TESTFILE-001` | a production module with no `test_*.py` partner |
|
|
165
|
+
| `META-001..005` | the checks themselves emit well-formed output |
|
|
166
|
+
|
|
167
|
+
Off until you turn them on:
|
|
168
|
+
|
|
169
|
+
| Rule | Why |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `LAYER-001..005` | needs a layered layout (`domain/ application/ infrastructure/ api/`) |
|
|
172
|
+
| `PORT-001..003` | needs an `application/ports/` directory |
|
|
173
|
+
| `TERM-NNN` | needs a vocabulary in `[tool.lanorme.domain_terms]` |
|
|
174
|
+
| `PATH-001` / `STALE-001` | need forbidden dirs / stale tokens configured |
|
|
175
|
+
| `KWARG-001` | keyword-only call sites; a strong house style |
|
|
176
|
+
| `NAMING-001/002` | CRUD method prefixes; conflicts with domain naming |
|
|
177
|
+
| `AAA-001/002` | Arrange-Act-Assert markers and DRY in tests |
|
|
178
|
+
| `CMT-005` | restating-comment detector; experimental, precision-first |
|
|
179
|
+
| `ATTR-001/002` | `hasattr`/`getattr`/`setattr` with a literal attribute name; a missing-type smell |
|
|
180
|
+
| `PROSE-001..003` | em dashes, US spelling and emoji in Markdown or comments |
|
|
181
|
+
|
|
182
|
+
## Writing a check
|
|
183
|
+
|
|
184
|
+
A check is any object with `name`, `description`, `rules`, and a `run` method:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from lanorme import CheckResult, Status, Violation, register
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MyCheck:
|
|
191
|
+
name = "my_check"
|
|
192
|
+
description = "What it enforces"
|
|
193
|
+
rules = ["MYCODE-001: the rule, in one line"]
|
|
194
|
+
|
|
195
|
+
def run(self, *, src_root: str) -> CheckResult:
|
|
196
|
+
violations: list[Violation] = []
|
|
197
|
+
# inspect files under src_root
|
|
198
|
+
status = Status.FAIL if violations else Status.PASS
|
|
199
|
+
return CheckResult(check=self.name, status=status, violations=violations)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
register(MyCheck())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Drop it in `lanorme/checks/`, ship it under the `lanorme.checks` entry-point
|
|
206
|
+
group, or point at it with `[tool.lanorme] plugins = [...]`. LaNorme finds it
|
|
207
|
+
and runs it.
|
|
208
|
+
|
|
209
|
+
## Versioning
|
|
210
|
+
|
|
211
|
+
The public surface is the rule codes you put in `select` / `ignore` /
|
|
212
|
+
`per-file-ignores` and the config keys under `[tool.lanorme]`. Renaming a rule,
|
|
213
|
+
dropping one, or turning a default-on rule off is a breaking change; adding a
|
|
214
|
+
rule or a new config key with a sensible default is not. Before 1.0, breaking
|
|
215
|
+
changes land in minor releases and are listed in
|
|
216
|
+
[`CHANGELOG.md`](CHANGELOG.md).
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT. See [`LICENSE`](LICENSE).
|