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.
Files changed (123) hide show
  1. python_constricter-0.3.2/.gitignore +47 -0
  2. {python_constricter-0.2.9 → python_constricter-0.3.2}/PKG-INFO +22 -15
  3. {python_constricter-0.2.9 → python_constricter-0.3.2}/README.md +15 -7
  4. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/__init__.py +1 -1
  5. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/command.py +46 -13
  6. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/config.py +64 -11
  7. python_constricter-0.3.2/constricter/cli/edits.py +188 -0
  8. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/explain.py +2 -1
  9. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/hints.py +122 -29
  10. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/options.py +60 -3
  11. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/protocol.py +6 -1
  12. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/report.py +1 -1
  13. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/runs.py +1 -1
  14. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/schedule.py +19 -7
  15. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/workers.py +2 -2
  16. python_constricter-0.3.2/constricter/fix/core/__init__.py +2 -0
  17. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/fixes.py +27 -21
  18. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/imports.py +58 -4
  19. python_constricter-0.3.2/constricter/fix/core/inherited.py +197 -0
  20. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/known.py +145 -26
  21. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/core}/signatures.py +24 -7
  22. python_constricter-0.3.2/constricter/fix/index/__init__.py +2 -0
  23. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/atoms.py +7 -7
  24. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/callers.py +3 -3
  25. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/classnames.py +5 -5
  26. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/declared.py +1 -1
  27. python_constricter-0.3.2/constricter/fix/index/decorated.py +119 -0
  28. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/installed.py +2 -2
  29. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/modules.py +156 -10
  30. python_constricter-0.3.2/constricter/fix/index/offers.py +135 -0
  31. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/order.py +2 -2
  32. python_constricter-0.3.2/constricter/fix/index/plain.py +120 -0
  33. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/project.py +268 -81
  34. python_constricter-0.3.2/constricter/fix/index/sides.py +65 -0
  35. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/index}/stubbed.py +25 -21
  36. python_constricter-0.3.2/constricter/fix/index/tuples.py +86 -0
  37. python_constricter-0.3.2/constricter/fix/libraries/__init__.py +2 -0
  38. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/library.py +40 -32
  39. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/opened.py +2 -2
  40. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/overloads.py +6 -5
  41. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/libraries}/stdlib.py +93 -27
  42. python_constricter-0.3.2/constricter/fix/tables/functions.json +44 -0
  43. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/method_overloads.json +96 -35
  44. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/method_signatures.json +52 -0
  45. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/methods.json +157 -85
  46. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/overloads.json +18 -5
  47. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/returns.json +34 -0
  48. python_constricter-0.3.2/constricter/fix/tables/source.json +23 -0
  49. python_constricter-0.3.2/constricter/fix/values/__init__.py +2 -0
  50. python_constricter-0.3.2/constricter/fix/values/aliased.py +182 -0
  51. python_constricter-0.3.2/constricter/fix/values/called.py +108 -0
  52. python_constricter-0.3.2/constricter/fix/values/classvars.py +365 -0
  53. python_constricter-0.3.2/constricter/fix/values/decided.py +333 -0
  54. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/doubts.py +175 -27
  55. python_constricter-0.3.2/constricter/fix/values/entered.py +152 -0
  56. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/fills.py +46 -18
  57. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/guesses.py +74 -41
  58. python_constricter-0.3.2/constricter/fix/values/hinted.py +558 -0
  59. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/inference.py +298 -106
  60. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/members.py +73 -10
  61. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/returned.py +18 -15
  62. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/returns.py +1 -1
  63. python_constricter-0.3.2/constricter/fix/values/shapes.py +279 -0
  64. python_constricter-0.3.2/constricter/fix/values/targets.py +350 -0
  65. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/notebook.py +1 -1
  66. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/offences.py +27 -3
  67. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/plugins/flake8.py +11 -1
  68. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/plugins/pylint.py +11 -0
  69. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/annotations.py +298 -74
  70. python_constricter-0.3.2/constricter/rules/binding.py +457 -0
  71. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/calls.py +79 -27
  72. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/checker.py +121 -98
  73. python_constricter-0.3.2/constricter/rules/decorators.py +209 -0
  74. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/flow.py +18 -3
  75. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/late.py +59 -24
  76. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/parsed.py +2 -2
  77. python_constricter-0.3.2/constricter/rules/quoted.py +88 -0
  78. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/rebinding.py +1 -1
  79. python_constricter-0.3.2/constricter/rules/recorded.py +124 -0
  80. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/scope.py +231 -78
  81. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/syntax.py +123 -5
  82. python_constricter-0.3.2/constricter/rules/tables.py +99 -0
  83. python_constricter-0.3.2/hatch_build.py +78 -0
  84. {python_constricter-0.2.9 → python_constricter-0.3.2}/pyproject.toml +38 -13
  85. python_constricter-0.3.2/stdlib_tables/__init__.py +16 -0
  86. python_constricter-0.3.2/stdlib_tables/__main__.py +8 -0
  87. python_constricter-0.3.2/stdlib_tables/generate.py +836 -0
  88. python_constricter-0.3.2/stdlib_tables/overloads.py +910 -0
  89. python_constricter-0.3.2/stdlib_tables/partial.json +999 -0
  90. python_constricter-0.3.2/stdlib_tables/reading.py +801 -0
  91. python_constricter-0.3.2/stdlib_tables/stubs.py +532 -0
  92. python_constricter-0.3.2/stdlib_tables/templates.py +291 -0
  93. python_constricter-0.2.9/constricter/fix/hinted.py +0 -242
  94. python_constricter-0.2.9/constricter/fix/tables/source.json +0 -4
  95. python_constricter-0.2.9/constricter/fix/targets.py +0 -184
  96. python_constricter-0.2.9/constricter/rules/binding.py +0 -251
  97. {python_constricter-0.2.9 → python_constricter-0.3.2}/LICENSE.md +0 -0
  98. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/__main__.py +0 -0
  99. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/__init__.py +0 -0
  100. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/baseline.py +0 -0
  101. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/collecting.py +0 -0
  102. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/guard.py +0 -0
  103. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/cli/paths.py +0 -0
  104. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/__init__.py +0 -0
  105. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/aliases.json +0 -0
  106. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/attributes.json +0 -0
  107. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/bases.json +0 -0
  108. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/classes.json +0 -0
  109. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/generic_attributes.json +0 -0
  110. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/scalar_members.json +0 -0
  111. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/scalars.json +0 -0
  112. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/subscriptable.json +0 -0
  113. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/type_parameters.json +0 -0
  114. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/fix/tables/variables.json +0 -0
  115. {python_constricter-0.2.9/constricter/fix → python_constricter-0.3.2/constricter/fix/values}/narrowed.py +0 -0
  116. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/jsonc.py +0 -0
  117. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/noqa.py +0 -0
  118. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/plugins/__init__.py +0 -0
  119. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/py.typed +0 -0
  120. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/__init__.py +0 -0
  121. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/narrowing.py +0 -0
  122. {python_constricter-0.2.9 → python_constricter-0.3.2}/constricter/rules/redundant.py +0 -0
  123. {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.9
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
- License-File: LICENSE.md
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
- Import-Name: constricter
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 | name the real type |
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 ty that's installed and runs | — | — | — |
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`, both: inferred types, as guesses) | `infer-with` | - | - |
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`, installed alongside) asks those type checkers for what `--fix` can't type itself,
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.9
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 | name the real type |
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 ty that's installed and runs | — | — | — |
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`, both: inferred types, as guesses) | `infer-with` | - | - |
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`, installed alongside) asks those type checkers for what `--fix` can't type itself,
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.9
248
+ rev: v0.3.2
241
249
  hooks:
242
250
  - id: constricter
243
251
  ```
@@ -25,7 +25,7 @@ from constricter.offences import (
25
25
  )
26
26
  from constricter.rules.checker import Coverage, annotation_coverage, check_source, check_tree
27
27
 
28
- __version__ = "0.2.9"
28
+ __version__ = "0.3.2"
29
29
  __all__ = [
30
30
  "COMMENT_TYPED_TARGET",
31
31
  "DEFAULT_CHECKS",
@@ -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 callers, fixes, installed, project
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
- modules = callers.with_parameters(modules, found)
434
- which: list[int]
435
- for which in (again, calling):
436
- redone: list[FileRun]
437
- redone, modules = _checked_all([paths[at] for at in which], check, options, session, modules)
438
- at: int
439
- run: FileRun
440
- for at, run in zip(which, redone, strict=True):
441
- runs[at] = _merged(runs[at], run) if options.mode is Mode.FIX else run
442
- return runs, modules
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 = installed.with_installed(modules, installed.search_path())
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 _table(path: Path) -> dict[str, _Toml]:
62
- """Return `path`'s `[tool.constricter]` table, or an empty one.
66
+ def _document(path: Path) -> dict[str, _Toml]:
67
+ """Read `path`, a `pyproject.toml`.
63
68
 
64
69
  Returns:
65
- The table's keys and values, as TOML parsed them.
70
+ Its tables, as TOML parsed them.
66
71
 
67
72
  Raises:
68
- ValueError: The file isn't TOML, or `tool.constricter` isn't a table.
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
- document = tomllib.load(file)
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
- for key, value in _table(path).items():
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