smelt-cli 0.1.0__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.
- smelt_cli-0.1.0/PKG-INFO +199 -0
- smelt_cli-0.1.0/README.md +187 -0
- smelt_cli-0.1.0/pyproject.toml +117 -0
- smelt_cli-0.1.0/pyproject.toml.orig +116 -0
- smelt_cli-0.1.0/smelt/__init__.py +6 -0
- smelt_cli-0.1.0/smelt/__main__.py +5 -0
- smelt_cli-0.1.0/smelt/analysis/__init__.py +27 -0
- smelt_cli-0.1.0/smelt/analysis/context.py +106 -0
- smelt_cli-0.1.0/smelt/analysis/files.py +198 -0
- smelt_cli-0.1.0/smelt/analysis/graphs.py +77 -0
- smelt_cli-0.1.0/smelt/analysis/imports.py +303 -0
- smelt_cli-0.1.0/smelt/analysis/parsing.py +110 -0
- smelt_cli-0.1.0/smelt/analysis/roles.py +122 -0
- smelt_cli-0.1.0/smelt/analysis/syntax.py +246 -0
- smelt_cli-0.1.0/smelt/analysis/types.py +181 -0
- smelt_cli-0.1.0/smelt/cli/__init__.py +3 -0
- smelt_cli-0.1.0/smelt/cli/app.py +140 -0
- smelt_cli-0.1.0/smelt/cli/commands/__init__.py +18 -0
- smelt_cli-0.1.0/smelt/cli/commands/adopt.py +116 -0
- smelt_cli-0.1.0/smelt/cli/commands/check.py +80 -0
- smelt_cli-0.1.0/smelt/cli/commands/discover.py +45 -0
- smelt_cli-0.1.0/smelt/cli/commands/info.py +107 -0
- smelt_cli-0.1.0/smelt/cli/commands/verify.py +60 -0
- smelt_cli-0.1.0/smelt/cli/support.py +64 -0
- smelt_cli-0.1.0/smelt/config/__init__.py +24 -0
- smelt_cli-0.1.0/smelt/config/discovery.py +49 -0
- smelt_cli-0.1.0/smelt/config/errors.py +51 -0
- smelt_cli-0.1.0/smelt/config/loader.py +182 -0
- smelt_cli-0.1.0/smelt/config/models.py +452 -0
- smelt_cli-0.1.0/smelt/config/patterns.py +47 -0
- smelt_cli-0.1.0/smelt/config/schema.py +23 -0
- smelt_cli-0.1.0/smelt/config/validation.py +193 -0
- smelt_cli-0.1.0/smelt/diagnostics/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/diagnostics/debt.py +119 -0
- smelt_cli-0.1.0/smelt/diagnostics/dedupe.py +44 -0
- smelt_cli-0.1.0/smelt/diagnostics/render/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/diagnostics/render/machine.py +137 -0
- smelt_cli-0.1.0/smelt/diagnostics/render/text.py +197 -0
- smelt_cli-0.1.0/smelt/diagnostics/report.py +61 -0
- smelt_cli-0.1.0/smelt/diagnostics/suppressions.py +80 -0
- smelt_cli-0.1.0/smelt/diagnostics/violation.py +109 -0
- smelt_cli-0.1.0/smelt/docs.py +97 -0
- smelt_cli-0.1.0/smelt/engine/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/engine/architecture_map.py +125 -0
- smelt_cli-0.1.0/smelt/engine/briefing.py +364 -0
- smelt_cli-0.1.0/smelt/engine/changes.py +81 -0
- smelt_cli-0.1.0/smelt/engine/check.py +393 -0
- smelt_cli-0.1.0/smelt/engine/inference.py +457 -0
- smelt_cli-0.1.0/smelt/engine/verify.py +77 -0
- smelt_cli-0.1.0/smelt/model/__init__.py +9 -0
- smelt_cli-0.1.0/smelt/model/architecture.py +237 -0
- smelt_cli-0.1.0/smelt/py.typed +0 -0
- smelt_cli-0.1.0/smelt/rules/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/rules/base.py +142 -0
- smelt_cli-0.1.0/smelt/rules/code/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/rules/code/common.py +119 -0
- smelt_cli-0.1.0/smelt/rules/code/construction.py +172 -0
- smelt_cli-0.1.0/smelt/rules/code/container.py +104 -0
- smelt_cli-0.1.0/smelt/rules/code/inheritance.py +99 -0
- smelt_cli-0.1.0/smelt/rules/code/roles.py +100 -0
- smelt_cli-0.1.0/smelt/rules/code/self_reference.py +104 -0
- smelt_cli-0.1.0/smelt/rules/common.py +127 -0
- smelt_cli-0.1.0/smelt/rules/dependencies/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/rules/dependencies/composition_root.py +123 -0
- smelt_cli-0.1.0/smelt/rules/dependencies/cycles.py +207 -0
- smelt_cli-0.1.0/smelt/rules/dependencies/features.py +132 -0
- smelt_cli-0.1.0/smelt/rules/dependencies/layers.py +220 -0
- smelt_cli-0.1.0/smelt/rules/dependencies/third_party.py +107 -0
- smelt_cli-0.1.0/smelt/rules/meta.py +72 -0
- smelt_cli-0.1.0/smelt/rules/registry.py +135 -0
- smelt_cli-0.1.0/smelt/rules/structure/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/rules/structure/layout.py +213 -0
- smelt_cli-0.1.0/smelt/rules/structure/naming.py +63 -0
- smelt_cli-0.1.0/smelt/rules/structure/roles.py +63 -0
- smelt_cli-0.1.0/smelt/rules/testing/__init__.py +0 -0
- smelt_cli-0.1.0/smelt/rules/testing/api.py +115 -0
- smelt_cli-0.1.0/smelt/rules/testing/bloat.py +84 -0
- smelt_cli-0.1.0/smelt/rules/testing/common.py +238 -0
- smelt_cli-0.1.0/smelt/rules/testing/location.py +253 -0
- smelt_cli-0.1.0/smelt/rules/testing/mocks.py +294 -0
- smelt_cli-0.1.0/smelt/rules/testing/patching.py +152 -0
smelt_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: smelt-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Static guardrails for Python codebases maintained by humans and coding agents.
|
|
5
|
+
Author: Mathis Arends
|
|
6
|
+
Author-email: Mathis Arends <mathisarends27@gmail.com>
|
|
7
|
+
Requires-Dist: grimp>=3.17
|
|
8
|
+
Requires-Dist: pydantic>=2.13.5
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# smelt
|
|
14
|
+
|
|
15
|
+
Static architecture guardrails for Python. Describe your features, layers and roles in
|
|
16
|
+
`smelt.yaml`; `smelt check` reports every import and construct that breaks them, with the
|
|
17
|
+
exact location, the allowed alternative and a hint on how to fix it.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install smelt-cli
|
|
23
|
+
# or install into your project's environment:
|
|
24
|
+
pip install smelt-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The PyPI package is named `smelt-cli`; the CLI command and Python package are both
|
|
28
|
+
named `smelt`. To run without installing, use `uvx --from smelt-cli smelt check`.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
smelt check # whole project, text output
|
|
34
|
+
smelt check --changed # only files changed against HEAD (incl. untracked)
|
|
35
|
+
smelt check --changed --base origin/main --format json
|
|
36
|
+
smelt context voice # architecture briefing for a feature or path
|
|
37
|
+
smelt explain SMT101 # rationale, examples and config knobs of a rule
|
|
38
|
+
smelt rules # all rules with defaults
|
|
39
|
+
smelt debt # record today's violations as known debt
|
|
40
|
+
smelt debt --prune # drop debt entries that were fixed
|
|
41
|
+
smelt init # draft a config from packages or uv workspace members
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`smelt debt` lets an existing project adopt smelt incrementally: with
|
|
45
|
+
`findings.debt: .smelt/debt.json` in `smelt.yaml`, `smelt check` only fails on new violations, and SMT903 reports entries that
|
|
46
|
+
were fixed and can leave the file.
|
|
47
|
+
|
|
48
|
+
Exit codes: `0` clean, `1` violations at or above `--fail-on`, `2` config or usage error.
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
`smelt.yaml` has one section per question:
|
|
53
|
+
|
|
54
|
+
| Section | Answers |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `project` | Where is the code? (`source_roots`, `test_roots`; packages are discovered) |
|
|
57
|
+
| `architecture` | What shape should it have? (features, layers, shared, composition root, cross-feature relationships, cycles, roles) |
|
|
58
|
+
| `conventions` | How is code named, placed and tested? (`naming`, `packages`, `tests`) |
|
|
59
|
+
| `integrations` | Where does a framework get special treatment? (`dependency_injection`) |
|
|
60
|
+
| `analysis` | How does smelt read the code? (`imports`, `types`) |
|
|
61
|
+
| `rules` | How loud is a finding? Severities only, by rule name or code |
|
|
62
|
+
| `findings` | How are existing findings handled? (`debt`, `ignore`, `suppressions`) |
|
|
63
|
+
|
|
64
|
+
`smelt config show` prints the resolved config with all defaults; [docs/configuration.md](docs/configuration.md) explains the layout.
|
|
65
|
+
|
|
66
|
+
For a uv workspace, run `smelt init` at the workspace root. It reads
|
|
67
|
+
`tool.uv.workspace.members`, finds each member's source and test roots, and drafts one
|
|
68
|
+
configuration for all packages. Review the generated policy before adopting its findings:
|
|
69
|
+
feature/layer boundaries are inferred, not a declaration of your intended architecture.
|
|
70
|
+
For example, feature-local DI providers can be allowed to use the DI framework while
|
|
71
|
+
remaining in their original feature and layer:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
architecture:
|
|
75
|
+
features: {root: backend.features}
|
|
76
|
+
composition_root: [backend.main, backend.lifespan]
|
|
77
|
+
cross_feature:
|
|
78
|
+
default: deny
|
|
79
|
+
allow:
|
|
80
|
+
- from: session.presentation
|
|
81
|
+
to: auth.presentation
|
|
82
|
+
|
|
83
|
+
integrations:
|
|
84
|
+
dependency_injection:
|
|
85
|
+
frameworks: [dishka] # always allowed in the composition root
|
|
86
|
+
allowed_in: ["backend.features.*.infrastructure.di"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The object form permits only that directional feature/layer relationship. The shorter
|
|
90
|
+
`"presentation -> presentation"` form remains available when a global layer-pair
|
|
91
|
+
exception is intended. `allowed_in` entries are dotted module patterns (`*` is one
|
|
92
|
+
segment, `**` any number) and also cover their submodules; unlike the composition root,
|
|
93
|
+
those modules keep all feature and layer rules.
|
|
94
|
+
|
|
95
|
+
For an architecture-first adoption, `smelt check --select SMT1,SMT3` focuses on
|
|
96
|
+
dependency and structure findings. To silence noisier testing rules persistently, use
|
|
97
|
+
severity overrides such as `rules: {private-access: off, interaction-assertion: off}` (rule names or codes) and review them later.
|
|
98
|
+
|
|
99
|
+
Silence a single finding inline, always with a reason:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from gateway.infra.sql import Repo # smelt: ignore[SMT101] -- migration tracked in #123
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
With `conventions.tests.layout: mirror`, every test file must mirror a source module by its path:
|
|
106
|
+
`tests/billing/test_invoice.py` needs `app/billing/invoice.py`, and a package test
|
|
107
|
+
`tests/billing/test_billing.py` needs `app/billing/`. Not every module needs a test, but a
|
|
108
|
+
test whose source is missing or elsewhere is an error. Deliberately unmirrored tests go in
|
|
109
|
+
`conventions.tests.unmirrored` (e.g. `["tests/integration/**"]`);
|
|
110
|
+
`conventions.tests.mirror_suffixes: true` also allows `test_invoice_<topic>.py`.
|
|
111
|
+
`conventions.tests.mirror` sets the convention relative to the test
|
|
112
|
+
root: the default `{path}/test_{module}.py` drops the root package, `{root}/{path}/test_{module}.py`
|
|
113
|
+
keeps it, and `unit/{path}/{module}_test.py` puts tests under `tests/unit/` with a suffix.
|
|
114
|
+
|
|
115
|
+
Every rule has a page under [docs/rules](docs/rules/), and `smelt.schema.json` gives editors
|
|
116
|
+
autocompletion for `smelt.yaml`.
|
|
117
|
+
|
|
118
|
+
## Python versions
|
|
119
|
+
|
|
120
|
+
Smelt runs on Python 3.12 to 3.14 and parses your code with the Python it runs on. Code
|
|
121
|
+
that uses newer syntax (3.14's `except A, B:` or t-strings, 3.13's type parameter defaults)
|
|
122
|
+
needs smelt on that version, e.g. `uvx -p 3.14 --from smelt-cli smelt check`; the syntax error says so when
|
|
123
|
+
`requires-python` or `.python-version` targets a newer Python.
|
|
124
|
+
|
|
125
|
+
## Optional type information
|
|
126
|
+
|
|
127
|
+
Role detection is nominal by default: a class is an adapter when it inherits a port. With
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
analysis:
|
|
131
|
+
types: pyright # needs pyright on PATH; pyright_command overrides how it is run
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Smelt also asks pyright whether a class satisfies a port structurally, so a duck-typed
|
|
135
|
+
adapter is found too. It is never required: without it, every rule still runs.
|
|
136
|
+
|
|
137
|
+
## Using Smelt with coding agents
|
|
138
|
+
|
|
139
|
+
Add this to your `AGENTS.md` or `CLAUDE.md`:
|
|
140
|
+
|
|
141
|
+
```md
|
|
142
|
+
Before implementing, run `smelt context <feature>` to see where code belongs.
|
|
143
|
+
After every change, run `smelt check --changed --format json`.
|
|
144
|
+
Do not finish while errors remain. Use `smelt explain <code>` when unsure.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Suggested loop: `smelt context <feature>` → edit → `smelt check --changed` → fix → tests →
|
|
148
|
+
pre-commit → CI (full check). Prefer the cheapest verification that gives sufficient
|
|
149
|
+
confidence: a rename needs smelt plus a type checker, new behavior needs one focused
|
|
150
|
+
regression test.
|
|
151
|
+
|
|
152
|
+
## pre-commit
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
repos:
|
|
156
|
+
- repo: https://github.com/mathisarends/smelt
|
|
157
|
+
rev: v0.1.0
|
|
158
|
+
hooks:
|
|
159
|
+
- id: smelt
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## GitHub Actions
|
|
163
|
+
|
|
164
|
+
CI always checks the whole repository, because cycles and transitive rules cannot be judged
|
|
165
|
+
from a diff alone.
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
- uses: astral-sh/setup-uv@v6
|
|
169
|
+
- run: uvx --from smelt-cli smelt check --format github
|
|
170
|
+
# optional: code scanning
|
|
171
|
+
- run: uvx --from smelt-cli smelt check --format sarif > smelt.sarif || true
|
|
172
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
173
|
+
with:
|
|
174
|
+
sarif_file: smelt.sarif
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
Requires [uv](https://docs.astral.sh/uv/).
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
uv sync # create .venv and install dev dependencies
|
|
183
|
+
uv run pre-commit install
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Common commands:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
uv run pytest # run tests
|
|
190
|
+
uv run pytest --cov # run tests with coverage
|
|
191
|
+
uv run ruff check --fix . # lint
|
|
192
|
+
uv run ruff format . # format
|
|
193
|
+
uv run mypy # type-check
|
|
194
|
+
uv run pre-commit run --all-files # run all hooks
|
|
195
|
+
uv run smelt check # smelt checks itself
|
|
196
|
+
uv run python scripts/generate.py # refresh smelt.schema.json and docs/rules/
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# smelt
|
|
2
|
+
|
|
3
|
+
Static architecture guardrails for Python. Describe your features, layers and roles in
|
|
4
|
+
`smelt.yaml`; `smelt check` reports every import and construct that breaks them, with the
|
|
5
|
+
exact location, the allowed alternative and a hint on how to fix it.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install smelt-cli
|
|
11
|
+
# or install into your project's environment:
|
|
12
|
+
pip install smelt-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The PyPI package is named `smelt-cli`; the CLI command and Python package are both
|
|
16
|
+
named `smelt`. To run without installing, use `uvx --from smelt-cli smelt check`.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
smelt check # whole project, text output
|
|
22
|
+
smelt check --changed # only files changed against HEAD (incl. untracked)
|
|
23
|
+
smelt check --changed --base origin/main --format json
|
|
24
|
+
smelt context voice # architecture briefing for a feature or path
|
|
25
|
+
smelt explain SMT101 # rationale, examples and config knobs of a rule
|
|
26
|
+
smelt rules # all rules with defaults
|
|
27
|
+
smelt debt # record today's violations as known debt
|
|
28
|
+
smelt debt --prune # drop debt entries that were fixed
|
|
29
|
+
smelt init # draft a config from packages or uv workspace members
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`smelt debt` lets an existing project adopt smelt incrementally: with
|
|
33
|
+
`findings.debt: .smelt/debt.json` in `smelt.yaml`, `smelt check` only fails on new violations, and SMT903 reports entries that
|
|
34
|
+
were fixed and can leave the file.
|
|
35
|
+
|
|
36
|
+
Exit codes: `0` clean, `1` violations at or above `--fail-on`, `2` config or usage error.
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
`smelt.yaml` has one section per question:
|
|
41
|
+
|
|
42
|
+
| Section | Answers |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `project` | Where is the code? (`source_roots`, `test_roots`; packages are discovered) |
|
|
45
|
+
| `architecture` | What shape should it have? (features, layers, shared, composition root, cross-feature relationships, cycles, roles) |
|
|
46
|
+
| `conventions` | How is code named, placed and tested? (`naming`, `packages`, `tests`) |
|
|
47
|
+
| `integrations` | Where does a framework get special treatment? (`dependency_injection`) |
|
|
48
|
+
| `analysis` | How does smelt read the code? (`imports`, `types`) |
|
|
49
|
+
| `rules` | How loud is a finding? Severities only, by rule name or code |
|
|
50
|
+
| `findings` | How are existing findings handled? (`debt`, `ignore`, `suppressions`) |
|
|
51
|
+
|
|
52
|
+
`smelt config show` prints the resolved config with all defaults; [docs/configuration.md](docs/configuration.md) explains the layout.
|
|
53
|
+
|
|
54
|
+
For a uv workspace, run `smelt init` at the workspace root. It reads
|
|
55
|
+
`tool.uv.workspace.members`, finds each member's source and test roots, and drafts one
|
|
56
|
+
configuration for all packages. Review the generated policy before adopting its findings:
|
|
57
|
+
feature/layer boundaries are inferred, not a declaration of your intended architecture.
|
|
58
|
+
For example, feature-local DI providers can be allowed to use the DI framework while
|
|
59
|
+
remaining in their original feature and layer:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
architecture:
|
|
63
|
+
features: {root: backend.features}
|
|
64
|
+
composition_root: [backend.main, backend.lifespan]
|
|
65
|
+
cross_feature:
|
|
66
|
+
default: deny
|
|
67
|
+
allow:
|
|
68
|
+
- from: session.presentation
|
|
69
|
+
to: auth.presentation
|
|
70
|
+
|
|
71
|
+
integrations:
|
|
72
|
+
dependency_injection:
|
|
73
|
+
frameworks: [dishka] # always allowed in the composition root
|
|
74
|
+
allowed_in: ["backend.features.*.infrastructure.di"]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The object form permits only that directional feature/layer relationship. The shorter
|
|
78
|
+
`"presentation -> presentation"` form remains available when a global layer-pair
|
|
79
|
+
exception is intended. `allowed_in` entries are dotted module patterns (`*` is one
|
|
80
|
+
segment, `**` any number) and also cover their submodules; unlike the composition root,
|
|
81
|
+
those modules keep all feature and layer rules.
|
|
82
|
+
|
|
83
|
+
For an architecture-first adoption, `smelt check --select SMT1,SMT3` focuses on
|
|
84
|
+
dependency and structure findings. To silence noisier testing rules persistently, use
|
|
85
|
+
severity overrides such as `rules: {private-access: off, interaction-assertion: off}` (rule names or codes) and review them later.
|
|
86
|
+
|
|
87
|
+
Silence a single finding inline, always with a reason:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from gateway.infra.sql import Repo # smelt: ignore[SMT101] -- migration tracked in #123
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
With `conventions.tests.layout: mirror`, every test file must mirror a source module by its path:
|
|
94
|
+
`tests/billing/test_invoice.py` needs `app/billing/invoice.py`, and a package test
|
|
95
|
+
`tests/billing/test_billing.py` needs `app/billing/`. Not every module needs a test, but a
|
|
96
|
+
test whose source is missing or elsewhere is an error. Deliberately unmirrored tests go in
|
|
97
|
+
`conventions.tests.unmirrored` (e.g. `["tests/integration/**"]`);
|
|
98
|
+
`conventions.tests.mirror_suffixes: true` also allows `test_invoice_<topic>.py`.
|
|
99
|
+
`conventions.tests.mirror` sets the convention relative to the test
|
|
100
|
+
root: the default `{path}/test_{module}.py` drops the root package, `{root}/{path}/test_{module}.py`
|
|
101
|
+
keeps it, and `unit/{path}/{module}_test.py` puts tests under `tests/unit/` with a suffix.
|
|
102
|
+
|
|
103
|
+
Every rule has a page under [docs/rules](docs/rules/), and `smelt.schema.json` gives editors
|
|
104
|
+
autocompletion for `smelt.yaml`.
|
|
105
|
+
|
|
106
|
+
## Python versions
|
|
107
|
+
|
|
108
|
+
Smelt runs on Python 3.12 to 3.14 and parses your code with the Python it runs on. Code
|
|
109
|
+
that uses newer syntax (3.14's `except A, B:` or t-strings, 3.13's type parameter defaults)
|
|
110
|
+
needs smelt on that version, e.g. `uvx -p 3.14 --from smelt-cli smelt check`; the syntax error says so when
|
|
111
|
+
`requires-python` or `.python-version` targets a newer Python.
|
|
112
|
+
|
|
113
|
+
## Optional type information
|
|
114
|
+
|
|
115
|
+
Role detection is nominal by default: a class is an adapter when it inherits a port. With
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
analysis:
|
|
119
|
+
types: pyright # needs pyright on PATH; pyright_command overrides how it is run
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Smelt also asks pyright whether a class satisfies a port structurally, so a duck-typed
|
|
123
|
+
adapter is found too. It is never required: without it, every rule still runs.
|
|
124
|
+
|
|
125
|
+
## Using Smelt with coding agents
|
|
126
|
+
|
|
127
|
+
Add this to your `AGENTS.md` or `CLAUDE.md`:
|
|
128
|
+
|
|
129
|
+
```md
|
|
130
|
+
Before implementing, run `smelt context <feature>` to see where code belongs.
|
|
131
|
+
After every change, run `smelt check --changed --format json`.
|
|
132
|
+
Do not finish while errors remain. Use `smelt explain <code>` when unsure.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Suggested loop: `smelt context <feature>` → edit → `smelt check --changed` → fix → tests →
|
|
136
|
+
pre-commit → CI (full check). Prefer the cheapest verification that gives sufficient
|
|
137
|
+
confidence: a rename needs smelt plus a type checker, new behavior needs one focused
|
|
138
|
+
regression test.
|
|
139
|
+
|
|
140
|
+
## pre-commit
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
repos:
|
|
144
|
+
- repo: https://github.com/mathisarends/smelt
|
|
145
|
+
rev: v0.1.0
|
|
146
|
+
hooks:
|
|
147
|
+
- id: smelt
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## GitHub Actions
|
|
151
|
+
|
|
152
|
+
CI always checks the whole repository, because cycles and transitive rules cannot be judged
|
|
153
|
+
from a diff alone.
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
- uses: astral-sh/setup-uv@v6
|
|
157
|
+
- run: uvx --from smelt-cli smelt check --format github
|
|
158
|
+
# optional: code scanning
|
|
159
|
+
- run: uvx --from smelt-cli smelt check --format sarif > smelt.sarif || true
|
|
160
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
161
|
+
with:
|
|
162
|
+
sarif_file: smelt.sarif
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
Requires [uv](https://docs.astral.sh/uv/).
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv sync # create .venv and install dev dependencies
|
|
171
|
+
uv run pre-commit install
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Common commands:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv run pytest # run tests
|
|
178
|
+
uv run pytest --cov # run tests with coverage
|
|
179
|
+
uv run ruff check --fix . # lint
|
|
180
|
+
uv run ruff format . # format
|
|
181
|
+
uv run mypy # type-check
|
|
182
|
+
uv run pre-commit run --all-files # run all hooks
|
|
183
|
+
uv run smelt check # smelt checks itself
|
|
184
|
+
uv run python scripts/generate.py # refresh smelt.schema.json and docs/rules/
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smelt-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Static guardrails for Python codebases maintained by humans and coding agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"grimp>=3.17",
|
|
9
|
+
"pydantic>=2.13.5",
|
|
10
|
+
"pyyaml>=6.0.3",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[project.authors]]
|
|
14
|
+
name = "Mathis Arends"
|
|
15
|
+
email = "mathisarends27@gmail.com"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
smelt = "smelt.cli:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.12.15,<0.13"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[tool.uv.build-backend]
|
|
25
|
+
module-root = ""
|
|
26
|
+
module-name = "smelt"
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 88
|
|
30
|
+
target-version = "py312"
|
|
31
|
+
extend-exclude = [
|
|
32
|
+
"tests/fixtures",
|
|
33
|
+
"docs",
|
|
34
|
+
]
|
|
35
|
+
force-exclude = true
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = [
|
|
39
|
+
"E",
|
|
40
|
+
"W",
|
|
41
|
+
"F",
|
|
42
|
+
"I",
|
|
43
|
+
"N",
|
|
44
|
+
"UP",
|
|
45
|
+
"B",
|
|
46
|
+
"A",
|
|
47
|
+
"C4",
|
|
48
|
+
"DTZ",
|
|
49
|
+
"T20",
|
|
50
|
+
"PT",
|
|
51
|
+
"RET",
|
|
52
|
+
"SIM",
|
|
53
|
+
"TC",
|
|
54
|
+
"PTH",
|
|
55
|
+
"ERA",
|
|
56
|
+
"PL",
|
|
57
|
+
"PERF",
|
|
58
|
+
"FURB",
|
|
59
|
+
"RUF",
|
|
60
|
+
"S",
|
|
61
|
+
]
|
|
62
|
+
ignore = ["E501"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint.per-file-ignores]
|
|
65
|
+
"tests/**" = [
|
|
66
|
+
"S101",
|
|
67
|
+
"PLR2004",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.format]
|
|
71
|
+
docstring-code-format = true
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.12"
|
|
75
|
+
strict = true
|
|
76
|
+
warn_unreachable = true
|
|
77
|
+
pretty = true
|
|
78
|
+
files = [
|
|
79
|
+
"smelt",
|
|
80
|
+
"tests",
|
|
81
|
+
]
|
|
82
|
+
exclude = ["^tests/fixtures/"]
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
minversion = "9.0"
|
|
86
|
+
testpaths = ["tests"]
|
|
87
|
+
norecursedirs = ["tests/fixtures"]
|
|
88
|
+
addopts = [
|
|
89
|
+
"-ra",
|
|
90
|
+
"--strict-markers",
|
|
91
|
+
"--strict-config",
|
|
92
|
+
]
|
|
93
|
+
xfail_strict = true
|
|
94
|
+
filterwarnings = ["error"]
|
|
95
|
+
|
|
96
|
+
[tool.coverage.run]
|
|
97
|
+
source = ["smelt"]
|
|
98
|
+
branch = true
|
|
99
|
+
|
|
100
|
+
[tool.coverage.report]
|
|
101
|
+
show_missing = true
|
|
102
|
+
skip_covered = true
|
|
103
|
+
exclude_also = [
|
|
104
|
+
"if TYPE_CHECKING:",
|
|
105
|
+
"raise NotImplementedError",
|
|
106
|
+
"@overload",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
[dependency-groups]
|
|
110
|
+
dev = [
|
|
111
|
+
"mypy>=2.3",
|
|
112
|
+
"pre-commit>=4.6",
|
|
113
|
+
"pytest>=9.1",
|
|
114
|
+
"pytest-cov>=7.1",
|
|
115
|
+
"ruff>=0.16.8",
|
|
116
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
117
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smelt-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Static guardrails for Python codebases maintained by humans and coding agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Mathis Arends", email = "mathisarends27@gmail.com" }]
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"grimp>=3.17",
|
|
10
|
+
"pydantic>=2.13.5",
|
|
11
|
+
"pyyaml>=6.0.3",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
smelt = "smelt.cli:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.12.15,<0.13"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[tool.uv.build-backend]
|
|
22
|
+
module-root = ""
|
|
23
|
+
module-name = "smelt"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"mypy>=2.3",
|
|
28
|
+
"pre-commit>=4.6",
|
|
29
|
+
"pytest>=9.1",
|
|
30
|
+
"pytest-cov>=7.1",
|
|
31
|
+
"ruff>=0.16.8",
|
|
32
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# --------------------------------------------------------------------------- #
|
|
36
|
+
# Ruff
|
|
37
|
+
# --------------------------------------------------------------------------- #
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 88
|
|
40
|
+
target-version = "py312"
|
|
41
|
+
# docs/ and smelt.schema.json are generated by scripts/generate.py.
|
|
42
|
+
extend-exclude = ["tests/fixtures", "docs"]
|
|
43
|
+
force-exclude = true
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = [
|
|
47
|
+
"E", # pycodestyle errors
|
|
48
|
+
"W", # pycodestyle warnings
|
|
49
|
+
"F", # pyflakes
|
|
50
|
+
"I", # isort
|
|
51
|
+
"N", # pep8-naming
|
|
52
|
+
"UP", # pyupgrade
|
|
53
|
+
"B", # flake8-bugbear
|
|
54
|
+
"A", # flake8-builtins
|
|
55
|
+
"C4", # flake8-comprehensions
|
|
56
|
+
"DTZ", # flake8-datetimez
|
|
57
|
+
"T20", # flake8-print
|
|
58
|
+
"PT", # flake8-pytest-style
|
|
59
|
+
"RET", # flake8-return
|
|
60
|
+
"SIM", # flake8-simplify
|
|
61
|
+
"TC", # flake8-type-checking
|
|
62
|
+
"PTH", # flake8-use-pathlib
|
|
63
|
+
"ERA", # eradicate (commented-out code)
|
|
64
|
+
"PL", # pylint
|
|
65
|
+
"PERF", # perflint
|
|
66
|
+
"FURB", # refurb
|
|
67
|
+
"RUF", # ruff-specific
|
|
68
|
+
"S", # flake8-bandit
|
|
69
|
+
]
|
|
70
|
+
ignore = [
|
|
71
|
+
"E501", # line length is handled by the formatter
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"tests/**" = [
|
|
76
|
+
"S101", # assert is fine in tests
|
|
77
|
+
"PLR2004", # magic values are fine in tests
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.format]
|
|
81
|
+
docstring-code-format = true
|
|
82
|
+
|
|
83
|
+
# --------------------------------------------------------------------------- #
|
|
84
|
+
# Mypy
|
|
85
|
+
# --------------------------------------------------------------------------- #
|
|
86
|
+
[tool.mypy]
|
|
87
|
+
python_version = "3.12"
|
|
88
|
+
strict = true
|
|
89
|
+
warn_unreachable = true
|
|
90
|
+
pretty = true
|
|
91
|
+
files = ["smelt", "tests"]
|
|
92
|
+
exclude = ["^tests/fixtures/"]
|
|
93
|
+
|
|
94
|
+
# --------------------------------------------------------------------------- #
|
|
95
|
+
# Pytest / Coverage
|
|
96
|
+
# --------------------------------------------------------------------------- #
|
|
97
|
+
[tool.pytest.ini_options]
|
|
98
|
+
minversion = "9.0"
|
|
99
|
+
testpaths = ["tests"]
|
|
100
|
+
norecursedirs = ["tests/fixtures"]
|
|
101
|
+
addopts = ["-ra", "--strict-markers", "--strict-config"]
|
|
102
|
+
xfail_strict = true
|
|
103
|
+
filterwarnings = ["error"]
|
|
104
|
+
|
|
105
|
+
[tool.coverage.run]
|
|
106
|
+
source = ["smelt"]
|
|
107
|
+
branch = true
|
|
108
|
+
|
|
109
|
+
[tool.coverage.report]
|
|
110
|
+
show_missing = true
|
|
111
|
+
skip_covered = true
|
|
112
|
+
exclude_also = [
|
|
113
|
+
"if TYPE_CHECKING:",
|
|
114
|
+
"raise NotImplementedError",
|
|
115
|
+
"@overload",
|
|
116
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from smelt.analysis.context import AnalysisContext, ChangeSet, FileChange, Index
|
|
2
|
+
from smelt.analysis.files import FileIndex, SourceFile, TestFile
|
|
3
|
+
from smelt.analysis.imports import ImportDetail, ImportIndex, is_stdlib
|
|
4
|
+
from smelt.analysis.parsing import AnalysisError
|
|
5
|
+
from smelt.analysis.roles import RoleIndex
|
|
6
|
+
from smelt.analysis.syntax import ClassInfo, ModuleSyntax, SyntaxIndex
|
|
7
|
+
from smelt.analysis.types import PyrightTypes, TypeIndex
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AnalysisContext",
|
|
11
|
+
"AnalysisError",
|
|
12
|
+
"ChangeSet",
|
|
13
|
+
"ClassInfo",
|
|
14
|
+
"FileChange",
|
|
15
|
+
"FileIndex",
|
|
16
|
+
"ImportDetail",
|
|
17
|
+
"ImportIndex",
|
|
18
|
+
"Index",
|
|
19
|
+
"ModuleSyntax",
|
|
20
|
+
"PyrightTypes",
|
|
21
|
+
"RoleIndex",
|
|
22
|
+
"SourceFile",
|
|
23
|
+
"SyntaxIndex",
|
|
24
|
+
"TestFile",
|
|
25
|
+
"TypeIndex",
|
|
26
|
+
"is_stdlib",
|
|
27
|
+
]
|