python-constricter 0.2.9__tar.gz → 0.3.2__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.
- python_constricter-0.3.2/.gitignore +47 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/PKG-INFO +22 -15
- {python_constricter-0.2.9 → python_constricter-0.3.2}/README.md +15 -7
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/__init__.py +1 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/command.py +46 -13
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/config.py +64 -11
- python_constricter-0.3.2/constricter/cli/edits.py +188 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/explain.py +2 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/hints.py +122 -29
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/options.py +60 -3
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/protocol.py +6 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/report.py +1 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/runs.py +1 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/schedule.py +19 -7
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/workers.py +2 -2
- python_constricter-0.3.2/constricter/fix/core/__init__.py +2 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/fixes.py +27 -21
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/imports.py +58 -4
- python_constricter-0.3.2/constricter/fix/core/inherited.py +197 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/known.py +145 -26
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/signatures.py +24 -7
- python_constricter-0.3.2/constricter/fix/index/__init__.py +2 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/atoms.py +7 -7
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/callers.py +3 -3
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/classnames.py +5 -5
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/declared.py +1 -1
- python_constricter-0.3.2/constricter/fix/index/decorated.py +119 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/installed.py +2 -2
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/modules.py +156 -10
- python_constricter-0.3.2/constricter/fix/index/offers.py +135 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/order.py +2 -2
- python_constricter-0.3.2/constricter/fix/index/plain.py +120 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/project.py +268 -81
- python_constricter-0.3.2/constricter/fix/index/sides.py +65 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/stubbed.py +25 -21
- python_constricter-0.3.2/constricter/fix/index/tuples.py +86 -0
- python_constricter-0.3.2/constricter/fix/libraries/__init__.py +2 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/library.py +40 -32
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/opened.py +2 -2
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/overloads.py +6 -5
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/stdlib.py +93 -27
- python_constricter-0.3.2/constricter/fix/tables/functions.json +44 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/method_overloads.json +96 -35
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/method_signatures.json +52 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/methods.json +157 -85
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/overloads.json +18 -5
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/returns.json +34 -0
- python_constricter-0.3.2/constricter/fix/tables/source.json +23 -0
- python_constricter-0.3.2/constricter/fix/values/__init__.py +2 -0
- python_constricter-0.3.2/constricter/fix/values/aliased.py +182 -0
- python_constricter-0.3.2/constricter/fix/values/called.py +108 -0
- python_constricter-0.3.2/constricter/fix/values/classvars.py +365 -0
- python_constricter-0.3.2/constricter/fix/values/decided.py +333 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/doubts.py +175 -27
- python_constricter-0.3.2/constricter/fix/values/entered.py +152 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/fills.py +46 -18
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/guesses.py +74 -41
- python_constricter-0.3.2/constricter/fix/values/hinted.py +558 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/inference.py +298 -106
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/members.py +73 -10
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/returned.py +18 -15
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/returns.py +1 -1
- python_constricter-0.3.2/constricter/fix/values/shapes.py +279 -0
- python_constricter-0.3.2/constricter/fix/values/targets.py +350 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/notebook.py +1 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/offences.py +27 -3
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/plugins/flake8.py +11 -1
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/plugins/pylint.py +11 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/annotations.py +298 -74
- python_constricter-0.3.2/constricter/rules/binding.py +457 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/calls.py +79 -27
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/checker.py +121 -98
- python_constricter-0.3.2/constricter/rules/decorators.py +209 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/flow.py +18 -3
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/late.py +59 -24
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/parsed.py +2 -2
- python_constricter-0.3.2/constricter/rules/quoted.py +88 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/rebinding.py +1 -1
- python_constricter-0.3.2/constricter/rules/recorded.py +124 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/scope.py +231 -78
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/syntax.py +123 -5
- python_constricter-0.3.2/constricter/rules/tables.py +99 -0
- python_constricter-0.3.2/hatch_build.py +78 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/pyproject.toml +38 -13
- python_constricter-0.3.2/stdlib_tables/__init__.py +16 -0
- python_constricter-0.3.2/stdlib_tables/__main__.py +8 -0
- python_constricter-0.3.2/stdlib_tables/generate.py +836 -0
- python_constricter-0.3.2/stdlib_tables/overloads.py +910 -0
- python_constricter-0.3.2/stdlib_tables/partial.json +999 -0
- python_constricter-0.3.2/stdlib_tables/reading.py +801 -0
- python_constricter-0.3.2/stdlib_tables/stubs.py +532 -0
- python_constricter-0.3.2/stdlib_tables/templates.py +291 -0
- python_constricter-0.2.9/constricter/fix/hinted.py +0 -242
- python_constricter-0.2.9/constricter/fix/tables/source.json +0 -4
- python_constricter-0.2.9/constricter/fix/targets.py +0 -184
- python_constricter-0.2.9/constricter/rules/binding.py +0 -251
- {python_constricter-0.2.9 → python_constricter-0.3.2}/LICENSE.md +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/__main__.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/__init__.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/baseline.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/collecting.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/guard.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/paths.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/__init__.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/aliases.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/attributes.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/bases.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/classes.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/generic_attributes.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/scalar_members.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/scalars.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/subscriptable.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/type_parameters.json +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/variables.json +0 -0
- {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/narrowed.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/jsonc.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/noqa.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/plugins/__init__.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/py.typed +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/__init__.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/narrowing.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/redundant.py +0 -0
- {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/walked.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Everything generated locally (the dev venv, tool caches, coverage data, build output) lives under local/.
|
|
2
|
+
local/
|
|
3
|
+
|
|
4
|
+
# Written next to the sources by Python.
|
|
5
|
+
__pycache__/
|
|
6
|
+
|
|
7
|
+
# Generated by a build (hatch_build.py, or `python -m stdlib_tables`): the standard-library tables, and
|
|
8
|
+
# the tests' list of what only some platforms and Pythons have.
|
|
9
|
+
constricter/fix/tables/
|
|
10
|
+
stdlib_tables/partial.json
|
|
11
|
+
|
|
12
|
+
# npm installs of the pinned CI Markdown tools (.github/package.json)
|
|
13
|
+
.github/node_modules/
|
|
14
|
+
|
|
15
|
+
.hypothesis
|
|
16
|
+
|
|
17
|
+
# Claude Code's agent worktrees: other checkouts of this repository.
|
|
18
|
+
.claude/worktrees/
|
|
19
|
+
|
|
20
|
+
# Claude Code's sandbox bind-mounts /dev/null over these denied paths while a
|
|
21
|
+
# session runs; none is a file of this repo
|
|
22
|
+
/.bash_profile
|
|
23
|
+
/.bashrc
|
|
24
|
+
/.gitconfig
|
|
25
|
+
/.gitmodules
|
|
26
|
+
/.idea
|
|
27
|
+
/.mcp.json
|
|
28
|
+
/.profile
|
|
29
|
+
/.ripgreprc
|
|
30
|
+
/.zprofile
|
|
31
|
+
/.zshrc
|
|
32
|
+
/.claude/agents
|
|
33
|
+
/.claude/commands
|
|
34
|
+
/.claude/hooks
|
|
35
|
+
/.claude/launch.json
|
|
36
|
+
/.claude/loop.md
|
|
37
|
+
/.claude/output-styles
|
|
38
|
+
/.claude/routines
|
|
39
|
+
/.claude/scheduled_tasks.json
|
|
40
|
+
/.claude/settings.json
|
|
41
|
+
/.claude/settings.local.json
|
|
42
|
+
/.claude/skills
|
|
43
|
+
/.claude/workflows
|
|
44
|
+
/.claude/.cc-writes
|
|
45
|
+
|
|
46
|
+
# Backups merge tools leave next to a conflicted file.
|
|
47
|
+
*.orig
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-constricter
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Import-Name: constricter
|
|
4
5
|
Summary: Lint rules: every local variable is typed where it's first bound. A flake8 plugin, a pylint plugin and a standalone CLI.
|
|
5
6
|
Author: Ivy Duggan
|
|
6
|
-
Requires-Python: >=3.11
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
7
|
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE.md
|
|
9
9
|
Classifier: Framework :: Flake8
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
|
-
|
|
22
|
-
Requires-Dist: flake8>=7 ; extra == "flake8"
|
|
23
|
-
Requires-Dist: pylint>=3 ; extra == "pylint"
|
|
21
|
+
Requires-Python: >=3.11
|
|
24
22
|
Provides-Extra: flake8
|
|
23
|
+
Requires-Dist: flake8>=7; extra == 'flake8'
|
|
25
24
|
Provides-Extra: pylint
|
|
26
|
-
|
|
25
|
+
Requires-Dist: pylint>=3; extra == 'pylint'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
# python-con`strict`er
|
|
29
29
|
|
|
@@ -97,7 +97,7 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
97
97
|
| `LVA002` | an untyped `for` target or `match` capture | `name: T` first (or a type comment) |
|
|
98
98
|
| `LVA003` | a `for` target typed only by `# type: T` | `name: T` first |
|
|
99
99
|
| `LVA004` | with `all-scopes`: the same as `LVA001`, in a module or class body | `name: T = ...` (`ClassVar[T]` in a dataclass) |
|
|
100
|
-
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters
|
|
100
|
+
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters, past `vague` | name the real type |
|
|
101
101
|
| `LVA006` | an annotation nested `nesting` deep (3 by default) | a `type` alias for a part of it |
|
|
102
102
|
| `LVA007` | a name annotated again with the type it already has, in the same block | drop the second annotation |
|
|
103
103
|
| `LVA008` | with every value the name ever holds known: an annotation that could narrow to them | narrow it (`total: int`) |
|
|
@@ -109,7 +109,10 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
109
109
|
Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
|
|
110
110
|
`global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
|
|
111
111
|
enum members (a base imported from `enum`, however it's aliased, or else whose name ends in `Enum`
|
|
112
|
-
or `Flag`).
|
|
112
|
+
or `Flag`). And what makes a type, not a value, which an annotation would turn into a variable to a
|
|
113
|
+
type checker: `T = TypeVar("T")`, a `ParamSpec`, a `TypeVarTuple`, a `NewType`, and a functional
|
|
114
|
+
`NamedTuple`, `TypedDict`, `Enum` or `collections.namedtuple`, however `typing`,
|
|
115
|
+
`typing_extensions`, `enum` or `collections` is imported.
|
|
113
116
|
|
|
114
117
|
A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
|
|
115
118
|
`type-comments`, or automatically in a module written to run on Python 2: one that imports
|
|
@@ -174,16 +177,19 @@ Options:
|
|
|
174
177
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
|
|
175
178
|
| level | `--level` | `level` | `--constricter-level` | `constricter-level` |
|
|
176
179
|
| strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
|
|
177
|
-
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright and
|
|
180
|
+
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
|
|
178
181
|
| type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
|
|
179
182
|
| all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
|
|
180
183
|
| nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
|
|
181
184
|
| max length | `--max-length N` (LVA011) | `max-length` | `--constricter-max-length` | `constricter-max-length` |
|
|
185
|
+
| vague | `--vague LEVEL` (LVA005, and the fixes offered): -1 none (the default), 0 one `Any` inside a type that says the rest (`tuple[str, Any]`), N from 1 N + 1, or one alone | `vague` | `--constricter-vague` | `constricter-vague` |
|
|
186
|
+
| min python | `--min-python VERSION` (the oldest Python the code runs on, whose syntax `--fix` writes; default: `requires-python`'s) | `min-python` | - | - |
|
|
182
187
|
| type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
|
|
183
188
|
| fix | `--fix` (`--unsafe-fixes` for guesses), `--diff` to preview | - | - | - |
|
|
184
|
-
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`,
|
|
189
|
+
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`, `pyrefly`, several: inferred types, as guesses) | `infer-with` | - | - |
|
|
185
190
|
| show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
|
|
186
191
|
| fix levels | `--fix-select`, `--fix-ignore`, `--unsafe-fix-select` (mechanisms: [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#fix-levels)) | `fix-select`, `fix-ignore`, `unsafe-fix-select` | - | - |
|
|
192
|
+
| fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
|
|
187
193
|
| select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
|
|
188
194
|
| extend select | `--extend-select CODES` (also report these; an opt-in code by its full code) | `extend-select` | flake8's own `extend-select` | pylint's own `enable` |
|
|
189
195
|
| ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
|
|
@@ -212,6 +218,8 @@ type-comments = false
|
|
|
212
218
|
all-scopes = true
|
|
213
219
|
nesting = 3
|
|
214
220
|
max-length = 4
|
|
221
|
+
vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
|
|
222
|
+
min-python = "3.11" # the default: your `requires-python`'s lower bound
|
|
215
223
|
jobs = 0
|
|
216
224
|
baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
|
|
217
225
|
|
|
@@ -237,8 +245,8 @@ return type or whose `return`s agree (in another checked file too), a copy, subs
|
|
|
237
245
|
method call of a local whose type is known, and values computed from those; a loop's target or an
|
|
238
246
|
unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
|
|
239
247
|
`--show-fixes` lists each fix and how its value decided it. `--infer-with basedpyright` (or `ty`, or
|
|
240
|
-
`basedpyright,ty
|
|
241
|
-
as guesses. The full list is in
|
|
248
|
+
`pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
|
|
249
|
+
what `--fix` can't type itself, as guesses. The full list is in
|
|
242
250
|
[docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
|
|
243
251
|
|
|
244
252
|
### Installing and running
|
|
@@ -264,7 +272,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
264
272
|
|
|
265
273
|
```yaml
|
|
266
274
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
267
|
-
rev: v0.2
|
|
275
|
+
rev: v0.3.2
|
|
268
276
|
hooks:
|
|
269
277
|
- id: constricter
|
|
270
278
|
```
|
|
@@ -378,4 +386,3 @@ I have used generative AI to write large parts of this code. All of the code her
|
|
|
378
386
|
_responsibility_ regardless: AI is a tool, not an owner of a project. I have personally understood,
|
|
379
387
|
reviewed, and approved all of the AI-generated code in this repository, and **mainline releases**
|
|
380
388
|
carry the same accountability to me as anything I write and publish myself.
|
|
381
|
-
|
|
@@ -70,7 +70,7 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
70
70
|
| `LVA002` | an untyped `for` target or `match` capture | `name: T` first (or a type comment) |
|
|
71
71
|
| `LVA003` | a `for` target typed only by `# type: T` | `name: T` first |
|
|
72
72
|
| `LVA004` | with `all-scopes`: the same as `LVA001`, in a module or class body | `name: T = ...` (`ClassVar[T]` in a dataclass) |
|
|
73
|
-
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters
|
|
73
|
+
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters, past `vague` | name the real type |
|
|
74
74
|
| `LVA006` | an annotation nested `nesting` deep (3 by default) | a `type` alias for a part of it |
|
|
75
75
|
| `LVA007` | a name annotated again with the type it already has, in the same block | drop the second annotation |
|
|
76
76
|
| `LVA008` | with every value the name ever holds known: an annotation that could narrow to them | narrow it (`total: int`) |
|
|
@@ -82,7 +82,10 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
82
82
|
Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
|
|
83
83
|
`global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
|
|
84
84
|
enum members (a base imported from `enum`, however it's aliased, or else whose name ends in `Enum`
|
|
85
|
-
or `Flag`).
|
|
85
|
+
or `Flag`). And what makes a type, not a value, which an annotation would turn into a variable to a
|
|
86
|
+
type checker: `T = TypeVar("T")`, a `ParamSpec`, a `TypeVarTuple`, a `NewType`, and a functional
|
|
87
|
+
`NamedTuple`, `TypedDict`, `Enum` or `collections.namedtuple`, however `typing`,
|
|
88
|
+
`typing_extensions`, `enum` or `collections` is imported.
|
|
86
89
|
|
|
87
90
|
A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
|
|
88
91
|
`type-comments`, or automatically in a module written to run on Python 2: one that imports
|
|
@@ -147,16 +150,19 @@ Options:
|
|
|
147
150
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
|
|
148
151
|
| level | `--level` | `level` | `--constricter-level` | `constricter-level` |
|
|
149
152
|
| strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
|
|
150
|
-
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright and
|
|
153
|
+
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
|
|
151
154
|
| type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
|
|
152
155
|
| all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
|
|
153
156
|
| nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
|
|
154
157
|
| max length | `--max-length N` (LVA011) | `max-length` | `--constricter-max-length` | `constricter-max-length` |
|
|
158
|
+
| vague | `--vague LEVEL` (LVA005, and the fixes offered): -1 none (the default), 0 one `Any` inside a type that says the rest (`tuple[str, Any]`), N from 1 N + 1, or one alone | `vague` | `--constricter-vague` | `constricter-vague` |
|
|
159
|
+
| min python | `--min-python VERSION` (the oldest Python the code runs on, whose syntax `--fix` writes; default: `requires-python`'s) | `min-python` | - | - |
|
|
155
160
|
| type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
|
|
156
161
|
| fix | `--fix` (`--unsafe-fixes` for guesses), `--diff` to preview | - | - | - |
|
|
157
|
-
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`,
|
|
162
|
+
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`, `pyrefly`, several: inferred types, as guesses) | `infer-with` | - | - |
|
|
158
163
|
| show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
|
|
159
164
|
| fix levels | `--fix-select`, `--fix-ignore`, `--unsafe-fix-select` (mechanisms: [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#fix-levels)) | `fix-select`, `fix-ignore`, `unsafe-fix-select` | - | - |
|
|
165
|
+
| fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
|
|
160
166
|
| select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
|
|
161
167
|
| extend select | `--extend-select CODES` (also report these; an opt-in code by its full code) | `extend-select` | flake8's own `extend-select` | pylint's own `enable` |
|
|
162
168
|
| ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
|
|
@@ -185,6 +191,8 @@ type-comments = false
|
|
|
185
191
|
all-scopes = true
|
|
186
192
|
nesting = 3
|
|
187
193
|
max-length = 4
|
|
194
|
+
vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
|
|
195
|
+
min-python = "3.11" # the default: your `requires-python`'s lower bound
|
|
188
196
|
jobs = 0
|
|
189
197
|
baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
|
|
190
198
|
|
|
@@ -210,8 +218,8 @@ return type or whose `return`s agree (in another checked file too), a copy, subs
|
|
|
210
218
|
method call of a local whose type is known, and values computed from those; a loop's target or an
|
|
211
219
|
unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
|
|
212
220
|
`--show-fixes` lists each fix and how its value decided it. `--infer-with basedpyright` (or `ty`, or
|
|
213
|
-
`basedpyright,ty
|
|
214
|
-
as guesses. The full list is in
|
|
221
|
+
`pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
|
|
222
|
+
what `--fix` can't type itself, as guesses. The full list is in
|
|
215
223
|
[docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
|
|
216
224
|
|
|
217
225
|
### Installing and running
|
|
@@ -237,7 +245,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
237
245
|
|
|
238
246
|
```yaml
|
|
239
247
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
240
|
-
rev: v0.2
|
|
248
|
+
rev: v0.3.2
|
|
241
249
|
hooks:
|
|
242
250
|
- id: constricter
|
|
243
251
|
```
|
|
@@ -23,8 +23,9 @@ from constricter.cli.protocol import HintError
|
|
|
23
23
|
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
24
24
|
from constricter.cli.runs import BaselineRun, CheckRun, CoverageRun, FileRun
|
|
25
25
|
from constricter.cli.workers import Workers
|
|
26
|
-
from constricter.fix import
|
|
27
|
-
from constricter.fix.known import Callee, Hints, Outside, Returns
|
|
26
|
+
from constricter.fix.core import fixes
|
|
27
|
+
from constricter.fix.core.known import Callee, Hints, Outside, Returns
|
|
28
|
+
from constricter.fix.index import callers, decorated, installed, plain, project
|
|
28
29
|
from constricter.noqa import lines, unsuppressed
|
|
29
30
|
from constricter.offences import (
|
|
30
31
|
DEFAULT_CHECKS,
|
|
@@ -397,6 +398,11 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
397
398
|
for index, run in zip(again, redone, strict=True):
|
|
398
399
|
runs[index] = _merged(runs[index], run)
|
|
399
400
|
again = [index for index, run in zip(again, redone, strict=True) if cast("CheckRun", run).fixed]
|
|
401
|
+
checker: str
|
|
402
|
+
path: Path
|
|
403
|
+
for checker, path in session.abandoned:
|
|
404
|
+
name: Path = options.input.name(path)
|
|
405
|
+
_ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
|
|
400
406
|
return names, runs
|
|
401
407
|
|
|
402
408
|
|
|
@@ -430,16 +436,40 @@ def _called_again(
|
|
|
430
436
|
if not (again := [at for at, name in enumerate(names) if name in found and names.count(name) == 1]):
|
|
431
437
|
return runs, modules
|
|
432
438
|
calling: list[int] = _calling(runs, found, again)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
at
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
439
|
+
before: project.Index = callers.with_parameters(modules, found)
|
|
440
|
+
modules = _checked_more(paths, runs, again, (check, options, session), before)
|
|
441
|
+
# A caller is checked again only if a function it imports now returns something else.
|
|
442
|
+
calling = [
|
|
443
|
+
at
|
|
444
|
+
for at in calling
|
|
445
|
+
if project.returned(modules, paths[at], {}) != project.returned(before, paths[at], {})
|
|
446
|
+
]
|
|
447
|
+
return runs, _checked_more(paths, runs, calling, (check, options, session), modules)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _checked_more(
|
|
451
|
+
paths: Sequence[Path],
|
|
452
|
+
runs: list[FileRun],
|
|
453
|
+
which: Sequence[int],
|
|
454
|
+
how: "tuple[Callable[[Path, Outside], FileRun], Options, hints.Session | None]",
|
|
455
|
+
modules: project.Index,
|
|
456
|
+
) -> project.Index:
|
|
457
|
+
"""Check the files of `paths` at `which` again, knowing `modules`, into `runs`.
|
|
458
|
+
|
|
459
|
+
`how`: the check, the options and the type checkers' session. With `--fix` a file's rounds are
|
|
460
|
+
joined; otherwise the later one stands.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
The index, with what their functions now return.
|
|
464
|
+
|
|
465
|
+
"""
|
|
466
|
+
redone: list[FileRun]
|
|
467
|
+
redone, modules = _checked_all([paths[at] for at in which], how[0], how[1], how[2], modules)
|
|
468
|
+
at: int
|
|
469
|
+
run: FileRun
|
|
470
|
+
for at, run in zip(which, redone, strict=True):
|
|
471
|
+
runs[at] = _merged(runs[at], run) if how[1].mode is Mode.FIX else run
|
|
472
|
+
return modules
|
|
443
473
|
|
|
444
474
|
|
|
445
475
|
def _calling(
|
|
@@ -487,7 +517,10 @@ def _checked_all(
|
|
|
487
517
|
modules = project.Index({}, []) if coverage else project.index(paths)
|
|
488
518
|
collecting.indexed()
|
|
489
519
|
if not coverage:
|
|
490
|
-
modules =
|
|
520
|
+
modules = plain.settled(
|
|
521
|
+
decorated.passed(installed.with_installed(modules, installed.search_path())),
|
|
522
|
+
options.checks.plain_bases,
|
|
523
|
+
)
|
|
491
524
|
return schedule.checked(
|
|
492
525
|
paths,
|
|
493
526
|
check,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
2
|
"""The command's defaults, from the nearest `pyproject.toml`'s `[tool.constricter]` table."""
|
|
3
3
|
|
|
4
|
+
import re
|
|
4
5
|
import tomllib
|
|
5
6
|
from collections.abc import Callable, Sequence
|
|
6
7
|
from functools import partial
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
9
|
+
from typing import TYPE_CHECKING, Final, TypeAlias, cast
|
|
9
10
|
|
|
10
11
|
from constricter.cli.protocol import SERVERS
|
|
11
12
|
from constricter.jsonc import is_int
|
|
@@ -30,6 +31,10 @@ def unknown_codes(codes: Sequence[str]) -> list[str]:
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
DEFAULT_BASELINE: Final = "constricter-baseline.json"
|
|
34
|
+
_MIN_PYTHON: Final = "min_python"
|
|
35
|
+
_VERSION: Final = re.compile(r"\d+\.\d+") # `min-python`'s: `3.11`
|
|
36
|
+
# A lower bound among `requires-python`'s specifiers: `>=3.11`, `~=3.11`, `==3.11.*`, `>3.10`.
|
|
37
|
+
_LOWER_BOUND: Final = re.compile(r"(?:>=|~=|==|>)\s*(\d+)(?:\.(\d+))?")
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
def project_root(start: Path) -> Path:
|
|
@@ -58,36 +63,65 @@ def _pyproject(start: Path) -> Path | None:
|
|
|
58
63
|
return None
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
def
|
|
62
|
-
"""
|
|
66
|
+
def _document(path: Path) -> dict[str, _Toml]:
|
|
67
|
+
"""Read `path`, a `pyproject.toml`.
|
|
63
68
|
|
|
64
69
|
Returns:
|
|
65
|
-
|
|
70
|
+
Its tables, as TOML parsed them.
|
|
66
71
|
|
|
67
72
|
Raises:
|
|
68
|
-
ValueError: The file isn't TOML
|
|
73
|
+
ValueError: The file isn't TOML.
|
|
69
74
|
|
|
70
75
|
"""
|
|
71
76
|
file: BufferedReader
|
|
72
|
-
document: dict[str, _Toml]
|
|
73
|
-
message: str
|
|
74
77
|
with path.open("rb") as file:
|
|
75
78
|
try:
|
|
76
|
-
|
|
79
|
+
return tomllib.load(file)
|
|
77
80
|
except tomllib.TOMLDecodeError as error:
|
|
78
|
-
message = f"{path}: {error}"
|
|
81
|
+
message: str = f"{path}: {error}"
|
|
79
82
|
raise ValueError(message) from error
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _table(path: Path, document: dict[str, _Toml]) -> dict[str, _Toml]:
|
|
86
|
+
"""Return the `[tool.constricter]` table of `document` (`path`'s), or an empty one.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The table's keys and values, as TOML parsed them.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: `tool.constricter` isn't a table.
|
|
93
|
+
|
|
94
|
+
"""
|
|
80
95
|
table: dict[str, _Toml]
|
|
81
96
|
match document.get("tool"):
|
|
82
97
|
case {"constricter": dict() as table}:
|
|
83
98
|
return table
|
|
84
99
|
case {"constricter": _}:
|
|
85
|
-
message = f"{path}: [tool.constricter] isn't a table"
|
|
100
|
+
message: str = f"{path}: [tool.constricter] isn't a table"
|
|
86
101
|
raise ValueError(message)
|
|
87
102
|
case _:
|
|
88
103
|
return {}
|
|
89
104
|
|
|
90
105
|
|
|
106
|
+
def _required(document: dict[str, _Toml]) -> str | None:
|
|
107
|
+
"""Read the oldest Python a project's `requires-python` allows: its highest lower bound.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
It (`3.11`, for `>=3.11,<4`), or `None` if the project doesn't say.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
required: str
|
|
114
|
+
match document.get("project"):
|
|
115
|
+
case {"requires-python": str() as required}:
|
|
116
|
+
pass
|
|
117
|
+
case _:
|
|
118
|
+
return None
|
|
119
|
+
found: list[tuple[str, str]] = cast("list[tuple[str, str]]", _LOWER_BOUND.findall(required))
|
|
120
|
+
bounds: list[tuple[int, int]] = [(int(major), int(minor or 0)) for major, minor in found]
|
|
121
|
+
oldest: tuple[int, int] | None = max(bounds, default=None)
|
|
122
|
+
return None if oldest is None else f"{oldest[0]}.{oldest[1]}"
|
|
123
|
+
|
|
124
|
+
|
|
91
125
|
def _level(value: _Toml) -> str | None:
|
|
92
126
|
"""Read a level's name or number as the options take it.
|
|
93
127
|
|
|
@@ -135,6 +169,16 @@ def _flag(value: _Toml) -> bool | None:
|
|
|
135
169
|
return value if isinstance(value, bool) else None
|
|
136
170
|
|
|
137
171
|
|
|
172
|
+
def _version(value: _Toml) -> str | None:
|
|
173
|
+
"""Read `min-python`: a Python version, as text (`"3.11"`).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
It, or `None` for anything else.
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
return value if isinstance(value, str) and _VERSION.fullmatch(value) else None
|
|
180
|
+
|
|
181
|
+
|
|
138
182
|
def _gigabytes(value: _Toml) -> float | None:
|
|
139
183
|
"""Read `infer-memory`: a positive number of gigabytes.
|
|
140
184
|
|
|
@@ -193,6 +237,7 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
193
237
|
"level": _level,
|
|
194
238
|
"nesting": partial(_whole, minimum=1),
|
|
195
239
|
"max-length": partial(_whole, minimum=1),
|
|
240
|
+
"vague": partial(_whole, minimum=-1),
|
|
196
241
|
"jobs": partial(_whole, minimum=0),
|
|
197
242
|
"exclude": _strings,
|
|
198
243
|
"select": _codes,
|
|
@@ -201,6 +246,7 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
201
246
|
"fix-select": _fix_kinds,
|
|
202
247
|
"fix-ignore": _fix_kinds,
|
|
203
248
|
"unsafe-fix-select": _fix_kinds,
|
|
249
|
+
"fix-plain-bases": _strings,
|
|
204
250
|
"type-comments": _flag,
|
|
205
251
|
"all-scopes": _flag,
|
|
206
252
|
"per-path-levels": _levels,
|
|
@@ -208,12 +254,15 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
208
254
|
"narrower": partial(_lists, read=_strings), # a type hierarchy: each type, and those it's narrower than
|
|
209
255
|
"infer-with": _checkers,
|
|
210
256
|
"infer-memory": _gigabytes,
|
|
257
|
+
"min-python": _version,
|
|
211
258
|
}
|
|
212
259
|
|
|
213
260
|
|
|
214
261
|
def config_defaults(start: Path) -> dict[str, Default]:
|
|
215
262
|
"""Return the option defaults in the nearest `pyproject.toml`'s `[tool.constricter]`.
|
|
216
263
|
|
|
264
|
+
And `min-python`'s from the project's `requires-python`, where the table doesn't set it.
|
|
265
|
+
|
|
217
266
|
Returns:
|
|
218
267
|
Each option's default, keyed by its `argparse` name (`type_comments`, not `type-comments`).
|
|
219
268
|
|
|
@@ -231,10 +280,14 @@ def config_defaults(start: Path) -> dict[str, Default]:
|
|
|
231
280
|
defaults: dict[str, Default] = {}
|
|
232
281
|
key: str
|
|
233
282
|
value: _Toml
|
|
234
|
-
|
|
283
|
+
document: dict[str, _Toml] = _document(path)
|
|
284
|
+
for key, value in _table(path, document).items():
|
|
235
285
|
default: Default | None
|
|
236
286
|
if key not in readers or (default := readers[key](value)) is None:
|
|
237
287
|
message: str = f"{path}: [tool.constricter] has an invalid {key} = {value!r}"
|
|
238
288
|
raise ValueError(message)
|
|
239
289
|
defaults[key.replace("-", "_")] = default
|
|
290
|
+
required: str | None
|
|
291
|
+
if _MIN_PYTHON not in defaults and (required := _required(document)) is not None:
|
|
292
|
+
defaults[_MIN_PYTHON] = required
|
|
240
293
|
return defaults
|