python-constricter 0.3.1__tar.gz → 0.3.3__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 (138) hide show
  1. {python_constricter-0.3.1 → python_constricter-0.3.3}/.gitignore +27 -0
  2. {python_constricter-0.3.1 → python_constricter-0.3.3}/PKG-INFO +18 -8
  3. {python_constricter-0.3.1 → python_constricter-0.3.3}/README.md +17 -7
  4. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/__init__.py +1 -1
  5. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/command.py +55 -15
  6. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/config.py +66 -12
  7. python_constricter-0.3.3/constricter/cli/edits.py +188 -0
  8. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/explain.py +2 -1
  9. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/hints.py +122 -29
  10. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/options.py +88 -5
  11. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/protocol.py +6 -1
  12. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/report.py +1 -1
  13. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/runs.py +1 -1
  14. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/schedule.py +41 -9
  15. python_constricter-0.3.3/constricter/cli/traced.py +289 -0
  16. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/workers.py +2 -2
  17. python_constricter-0.3.3/constricter/fix/core/__init__.py +2 -0
  18. python_constricter-0.3.3/constricter/fix/core/asked.py +56 -0
  19. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/fixes.py +38 -22
  20. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/imports.py +153 -11
  21. python_constricter-0.3.3/constricter/fix/core/inherited.py +210 -0
  22. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/known.py +235 -28
  23. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/signatures.py +14 -9
  24. python_constricter-0.3.3/constricter/fix/index/__init__.py +2 -0
  25. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/atoms.py +11 -8
  26. python_constricter-0.3.3/constricter/fix/index/beyond.py +90 -0
  27. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/callers.py +4 -4
  28. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/classnames.py +5 -5
  29. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/declared.py +17 -1
  30. python_constricter-0.3.3/constricter/fix/index/decorated.py +119 -0
  31. python_constricter-0.3.3/constricter/fix/index/fixtures.py +176 -0
  32. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/installed.py +36 -15
  33. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/modules.py +196 -12
  34. python_constricter-0.3.3/constricter/fix/index/offers.py +135 -0
  35. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/order.py +6 -3
  36. python_constricter-0.3.3/constricter/fix/index/own_overloads.py +57 -0
  37. python_constricter-0.3.3/constricter/fix/index/plain.py +123 -0
  38. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/project.py +339 -94
  39. python_constricter-0.3.3/constricter/fix/index/read.py +70 -0
  40. python_constricter-0.3.3/constricter/fix/index/sides.py +65 -0
  41. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/stubbed.py +58 -87
  42. python_constricter-0.3.3/constricter/fix/index/tuples.py +86 -0
  43. python_constricter-0.3.3/constricter/fix/libraries/__init__.py +2 -0
  44. python_constricter-0.3.3/constricter/fix/libraries/library.py +286 -0
  45. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/libraries}/opened.py +35 -3
  46. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/libraries}/overloads.py +128 -13
  47. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/libraries}/stdlib.py +303 -18
  48. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/attributes.json +23 -0
  49. python_constricter-0.3.3/constricter/fix/tables/awaited.json +69 -0
  50. python_constricter-0.3.3/constricter/fix/tables/awaited_overloads.json +14 -0
  51. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/classes.json +2 -1
  52. python_constricter-0.3.3/constricter/fix/tables/elements.json +156 -0
  53. python_constricter-0.3.3/constricter/fix/tables/functions.json +43 -0
  54. python_constricter-0.3.3/constricter/fix/tables/generic_attributes.json +252 -0
  55. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/method_overloads.json +165 -65
  56. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/method_signatures.json +230 -19
  57. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/methods.json +184 -88
  58. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/overloads.json +70 -33
  59. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/returns.json +34 -0
  60. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/scalars.json +3 -0
  61. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/source.json +5 -1
  62. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/subscriptable.json +1 -0
  63. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/type_parameters.json +2 -0
  64. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/variables.json +21 -0
  65. python_constricter-0.3.3/constricter/fix/values/__init__.py +2 -0
  66. python_constricter-0.3.3/constricter/fix/values/aliased.py +182 -0
  67. python_constricter-0.3.3/constricter/fix/values/called.py +108 -0
  68. python_constricter-0.3.3/constricter/fix/values/classvars.py +438 -0
  69. python_constricter-0.3.3/constricter/fix/values/decided.py +376 -0
  70. python_constricter-0.3.3/constricter/fix/values/displays.py +87 -0
  71. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/doubts.py +222 -27
  72. python_constricter-0.3.3/constricter/fix/values/entered.py +173 -0
  73. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/fills.py +93 -34
  74. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/guesses.py +104 -44
  75. python_constricter-0.3.3/constricter/fix/values/hinted.py +645 -0
  76. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/inference.py +377 -180
  77. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/members.py +129 -18
  78. python_constricter-0.3.3/constricter/fix/values/operated.py +199 -0
  79. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/returned.py +202 -33
  80. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/returns.py +2 -1
  81. python_constricter-0.3.3/constricter/fix/values/shapes.py +394 -0
  82. python_constricter-0.3.3/constricter/fix/values/targets.py +356 -0
  83. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/notebook.py +1 -1
  84. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/offences.py +30 -3
  85. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/plugins/flake8.py +11 -1
  86. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/plugins/pylint.py +11 -0
  87. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/annotations.py +319 -76
  88. python_constricter-0.3.3/constricter/rules/binding.py +454 -0
  89. python_constricter-0.3.3/constricter/rules/calls.py +409 -0
  90. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/checker.py +256 -116
  91. python_constricter-0.3.3/constricter/rules/decorators.py +221 -0
  92. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/flow.py +18 -3
  93. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/late.py +61 -26
  94. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/parsed.py +44 -5
  95. python_constricter-0.3.3/constricter/rules/quoted.py +88 -0
  96. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/rebinding.py +5 -3
  97. python_constricter-0.3.3/constricter/rules/recorded.py +163 -0
  98. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/scope.py +276 -87
  99. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/syntax.py +123 -5
  100. python_constricter-0.3.3/constricter/rules/tables.py +155 -0
  101. python_constricter-0.3.3/constricter/trace.py +385 -0
  102. {python_constricter-0.3.1 → python_constricter-0.3.3}/pyproject.toml +2 -2
  103. python_constricter-0.3.3/stdlib_tables/elements.py +77 -0
  104. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/generate.py +238 -27
  105. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/overloads.py +123 -18
  106. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/partial.json +25 -0
  107. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/reading.py +78 -10
  108. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/stubs.py +11 -4
  109. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/templates.py +42 -7
  110. python_constricter-0.3.1/constricter/fix/hinted.py +0 -242
  111. python_constricter-0.3.1/constricter/fix/library.py +0 -164
  112. python_constricter-0.3.1/constricter/fix/tables/generic_attributes.json +0 -55
  113. python_constricter-0.3.1/constricter/fix/targets.py +0 -184
  114. python_constricter-0.3.1/constricter/rules/binding.py +0 -251
  115. python_constricter-0.3.1/constricter/rules/calls.py +0 -211
  116. {python_constricter-0.3.1 → python_constricter-0.3.3}/LICENSE.md +0 -0
  117. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/__main__.py +0 -0
  118. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/__init__.py +0 -0
  119. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/baseline.py +0 -0
  120. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/collecting.py +0 -0
  121. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/guard.py +0 -0
  122. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/paths.py +0 -0
  123. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/__init__.py +0 -0
  124. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/aliases.json +0 -0
  125. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/bases.json +0 -0
  126. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/scalar_members.json +0 -0
  127. {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/narrowed.py +0 -0
  128. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/jsonc.py +0 -0
  129. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/noqa.py +0 -0
  130. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/plugins/__init__.py +0 -0
  131. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/py.typed +0 -0
  132. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/__init__.py +0 -0
  133. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/narrowing.py +0 -0
  134. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/redundant.py +0 -0
  135. {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/walked.py +0 -0
  136. {python_constricter-0.3.1 → python_constricter-0.3.3}/hatch_build.py +0 -0
  137. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/__init__.py +0 -0
  138. {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/__main__.py +0 -0
@@ -17,5 +17,32 @@ stdlib_tables/partial.json
17
17
  # Claude Code's agent worktrees: other checkouts of this repository.
18
18
  .claude/worktrees/
19
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
+ /.vscode
46
+
20
47
  # Backups merge tools leave next to a conflicted file.
21
48
  *.orig
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-constricter
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Import-Name: constricter
5
5
  Summary: Lint rules: every local variable is typed where it's first bound. A flake8 plugin, a pylint plugin and a standalone CLI.
6
6
  Author: Ivy Duggan
@@ -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,20 @@ 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` | - | - |
190
+ | infer from | `--infer-from FILE` (the types `python -m constricter.trace -m pytest` recorded from a run, as guesses) | `infer-from` | - | - |
185
191
  | show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
186
192
  | 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` | - | - |
193
+ | fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
187
194
  | select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
188
195
  | 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
196
  | ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
@@ -212,6 +219,8 @@ type-comments = false
212
219
  all-scopes = true
213
220
  nesting = 3
214
221
  max-length = 4
222
+ vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
223
+ min-python = "3.11" # the default: your `requires-python`'s lower bound
215
224
  jobs = 0
216
225
  baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
217
226
 
@@ -237,8 +246,9 @@ return type or whose `return`s agree (in another checked file too), a copy, subs
237
246
  method call of a local whose type is known, and values computed from those; a loop's target or an
238
247
  unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
239
248
  `--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
249
+ `pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
250
+ what `--fix` can't type itself, as guesses; `--infer-from FILE` takes the types a run bound each
251
+ name to, recorded by `python -m constricter.trace -m pytest`, the same way. The full list is in
242
252
  [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
243
253
 
244
254
  ### Installing and running
@@ -264,7 +274,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
264
274
 
265
275
  ```yaml
266
276
  - repo: https://github.com/ivylikethevine/python-constricter
267
- rev: v0.3.1
277
+ rev: v0.3.3
268
278
  hooks:
269
279
  - id: constricter
270
280
  ```
@@ -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,20 @@ 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` | - | - |
163
+ | infer from | `--infer-from FILE` (the types `python -m constricter.trace -m pytest` recorded from a run, as guesses) | `infer-from` | - | - |
158
164
  | show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
159
165
  | 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` | - | - |
166
+ | fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
160
167
  | select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
161
168
  | 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
169
  | ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
@@ -185,6 +192,8 @@ type-comments = false
185
192
  all-scopes = true
186
193
  nesting = 3
187
194
  max-length = 4
195
+ vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
196
+ min-python = "3.11" # the default: your `requires-python`'s lower bound
188
197
  jobs = 0
189
198
  baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
190
199
 
@@ -210,8 +219,9 @@ return type or whose `return`s agree (in another checked file too), a copy, subs
210
219
  method call of a local whose type is known, and values computed from those; a loop's target or an
211
220
  unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
212
221
  `--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
222
+ `pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
223
+ what `--fix` can't type itself, as guesses; `--infer-from FILE` takes the types a run bound each
224
+ name to, recorded by `python -m constricter.trace -m pytest`, the same way. The full list is in
215
225
  [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
216
226
 
217
227
  ### Installing and running
@@ -237,7 +247,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
237
247
 
238
248
  ```yaml
239
249
  - repo: https://github.com/ivylikethevine/python-constricter
240
- rev: v0.3.1
250
+ rev: v0.3.3
241
251
  hooks:
242
252
  - id: constricter
243
253
  ```
@@ -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.3.1"
28
+ __version__ = "0.3.3"
29
29
  __all__ = [
30
30
  "COMMENT_TYPED_TARGET",
31
31
  "DEFAULT_CHECKS",
@@ -16,15 +16,16 @@ from pathlib import Path
16
16
  from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
17
17
 
18
18
  from constricter import notebook
19
- from constricter.cli import baseline, collecting, schedule
19
+ from constricter.cli import baseline, collecting, schedule, traced
20
20
  from constricter.cli.options import Mode, Options, Output
21
21
  from constricter.cli.paths import STDIN, python_files
22
22
  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,
@@ -32,6 +33,7 @@ from constricter.offences import (
32
33
  Edit,
33
34
  Offence,
34
35
  )
36
+ from constricter.rules import parsed
35
37
  from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
36
38
 
37
39
  if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
@@ -296,13 +298,17 @@ def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], option
296
298
  left: list[Result] = [r for r in results if r.offence not in fixing]
297
299
  if path == STDIN: # the fixed source goes to stdout
298
300
  return CheckRun(left, baselined, len(fixing), _fixed(raw, name, fixing).text)
301
+ fixed: int
299
302
  try:
300
- return CheckRun(left, baselined, fix_file(path, fixing))
303
+ fixed = fix_file(path, fixing)
301
304
  except UnicodeEncodeError as failure: # the file is left as it was, its offences unfixed
302
305
  # Its canonical name: PyPy reports `latin1` where CPython says `latin-1`.
303
306
  encoding: str = codecs.lookup(failure.encoding).name
304
307
  message: str = f"an annotation can't be written in its encoding, {encoding}; left as it was"
305
308
  return CheckRun(results, baselined, error=f"{name}: error: {message}")
309
+ if fixed:
310
+ _ = parsed.take(raw) # the tree of the text it had: no check will ask for it again
311
+ return CheckRun(left, baselined, fixed)
306
312
 
307
313
 
308
314
  def _baseline_path(path: Path, outside: Outside, options: Options) -> BaselineRun:
@@ -397,6 +403,11 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
397
403
  for index, run in zip(again, redone, strict=True):
398
404
  runs[index] = _merged(runs[index], run)
399
405
  again = [index for index, run in zip(again, redone, strict=True) if cast("CheckRun", run).fixed]
406
+ checker: str
407
+ path: Path
408
+ for checker, path in session.abandoned:
409
+ name: Path = options.input.name(path)
410
+ _ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
400
411
  return names, runs
401
412
 
402
413
 
@@ -430,16 +441,40 @@ def _called_again(
430
441
  if not (again := [at for at, name in enumerate(names) if name in found and names.count(name) == 1]):
431
442
  return runs, modules
432
443
  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
444
+ before: project.Index = callers.with_parameters(modules, found)
445
+ modules = _checked_more(paths, runs, again, (check, options, session), before)
446
+ # A caller is checked again only if a function it imports now returns something else.
447
+ calling = [
448
+ at
449
+ for at in calling
450
+ if project.returned(modules, paths[at], {}) != project.returned(before, paths[at], {})
451
+ ]
452
+ return runs, _checked_more(paths, runs, calling, (check, options, session), modules)
453
+
454
+
455
+ def _checked_more(
456
+ paths: Sequence[Path],
457
+ runs: list[FileRun],
458
+ which: Sequence[int],
459
+ how: "tuple[Callable[[Path, Outside], FileRun], Options, hints.Session | None]",
460
+ modules: project.Index,
461
+ ) -> project.Index:
462
+ """Check the files of `paths` at `which` again, knowing `modules`, into `runs`.
463
+
464
+ `how`: the check, the options and the type checkers' session. With `--fix` a file's rounds are
465
+ joined; otherwise the later one stands.
466
+
467
+ Returns:
468
+ The index, with what their functions now return.
469
+
470
+ """
471
+ redone: list[FileRun]
472
+ redone, modules = _checked_all([paths[at] for at in which], how[0], how[1], how[2], modules)
473
+ at: int
474
+ run: FileRun
475
+ for at, run in zip(which, redone, strict=True):
476
+ runs[at] = _merged(runs[at], run) if how[1].mode is Mode.FIX else run
477
+ return modules
443
478
 
444
479
 
445
480
  def _calling(
@@ -475,6 +510,8 @@ def _checked_all(
475
510
 
476
511
  """
477
512
  hinted: dict[Path, tuple[Hints, ...]] = {} if session is None else session.hints(_texts(paths))
513
+ if options.input.trace is not None:
514
+ hinted = traced.merged(hinted, traced.load(options.input.trace), paths)
478
515
  coverage: bool = options.mode is Mode.COVERAGE # needs nothing from the other files
479
516
  stack: contextlib.ExitStack
480
517
  with contextlib.ExitStack() as stack:
@@ -487,7 +524,10 @@ def _checked_all(
487
524
  modules = project.Index({}, []) if coverage else project.index(paths)
488
525
  collecting.indexed()
489
526
  if not coverage:
490
- modules = installed.with_installed(modules, installed.search_path())
527
+ modules = plain.settled(
528
+ decorated.passed(installed.with_installed(modules, installed.search_path())),
529
+ options.checks.plain_bases,
530
+ )
491
531
  return schedule.checked(
492
532
  paths,
493
533
  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
 
@@ -179,7 +223,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
179
223
 
180
224
 
181
225
  def _baseline(value: _Toml, root: Path) -> str | None:
182
- """Read a baseline path, relative to `root` (the pyproject.toml that names it).
226
+ """Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
183
227
 
184
228
  Returns:
185
229
  The resolved path, or `None` if `value` isn't a non-empty string.
@@ -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
 
@@ -227,14 +276,19 @@ def config_defaults(start: Path) -> dict[str, Default]:
227
276
  readers: dict[str, Callable[[_Toml], Default | None]] = {
228
277
  **_READERS,
229
278
  "baseline": partial(_baseline, root=path.parent),
279
+ "infer-from": partial(_baseline, root=path.parent),
230
280
  }
231
281
  defaults: dict[str, Default] = {}
232
282
  key: str
233
283
  value: _Toml
234
- for key, value in _table(path).items():
284
+ document: dict[str, _Toml] = _document(path)
285
+ for key, value in _table(path, document).items():
235
286
  default: Default | None
236
287
  if key not in readers or (default := readers[key](value)) is None:
237
288
  message: str = f"{path}: [tool.constricter] has an invalid {key} = {value!r}"
238
289
  raise ValueError(message)
239
290
  defaults[key.replace("-", "_")] = default
291
+ required: str | None
292
+ if _MIN_PYTHON not in defaults and (required := _required(document)) is not None:
293
+ defaults[_MIN_PYTHON] = required
240
294
  return defaults