python-constricter 0.4.1__cp311-cp311-win_amd64.whl

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 (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp311-win_amd64.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp311-win_amd64.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp311-win_amd64.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp311-win_amd64.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp311-win_amd64.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp311-win_amd64.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp311-win_amd64.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp311-win_amd64.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp311-win_amd64.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp311-win_amd64.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp311-win_amd64.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp311-win_amd64.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp311-win_amd64.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp311-win_amd64.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp311-win_amd64.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp311-win_amd64.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp311-win_amd64.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp311-win_amd64.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp311-win_amd64.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp311-win_amd64.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp311-win_amd64.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp311-win_amd64.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp311-win_amd64.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp311-win_amd64.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp311-win_amd64.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp311-win_amd64.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp311-win_amd64.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp311-win_amd64.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp311-win_amd64.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp311-win_amd64.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp311-win_amd64.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp311-win_amd64.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp311-win_amd64.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp311-win_amd64.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp311-win_amd64.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp311-win_amd64.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp311-win_amd64.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp311-win_amd64.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp311-win_amd64.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp311-win_amd64.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp311-win_amd64.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp311-win_amd64.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp311-win_amd64.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp311-win_amd64.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp311-win_amd64.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp311-win_amd64.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp311-win_amd64.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp311-win_amd64.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp311-win_amd64.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp311-win_amd64.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp311-win_amd64.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp311-win_amd64.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp311-win_amd64.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp311-win_amd64.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp311-win_amd64.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp311-win_amd64.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp311-win_amd64.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp311-win_amd64.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp311-win_amd64.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp311-win_amd64.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp311-win_amd64.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp311-win_amd64.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp311-win_amd64.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp311-win_amd64.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp311-win_amd64.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp311-win_amd64.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp311-win_amd64.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp311-win_amd64.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp311-win_amd64.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp311-win_amd64.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp311-win_amd64.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp311-win_amd64.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp311-win_amd64.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp311-win_amd64.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp311-win_amd64.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp311-win_amd64.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp311-win_amd64.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp311-win_amd64.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp311-win_amd64.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp311-win_amd64.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp311-win_amd64.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp311-win_amd64.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp311-win_amd64.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp311-win_amd64.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp311-win_amd64.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp311-win_amd64.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp311-win_amd64.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp311-win_amd64.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp311-win_amd64.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp311-win_amd64.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp311-win_amd64.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp311-win_amd64.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp311-win_amd64.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp311-win_amd64.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp311-win_amd64.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp311-win_amd64.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp311-win_amd64.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp311-win_amd64.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp311-win_amd64.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,400 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-constricter
3
+ Version: 0.4.1
4
+ Import-Name: constricter
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
+ Author: Ivy Duggan
7
+ License-Expression: MIT
8
+ License-File: LICENSE.md
9
+ Classifier: Framework :: Flake8
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Provides-Extra: flake8
23
+ Requires-Dist: flake8>=7; extra == 'flake8'
24
+ Provides-Extra: pylint
25
+ Requires-Dist: pylint>=3; extra == 'pylint'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # python-con`strict`er
29
+
30
+ > EXPERIMENTAL UNTIL v1.0.0
31
+
32
+ **I want all of my python code typed.**
33
+
34
+ [![CI](https://github.com/ivylikethevine/python-constricter/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ivylikethevine/python-constricter/actions/workflows/ci.yml)
35
+ [![Security](https://github.com/ivylikethevine/python-constricter/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/ivylikethevine/python-constricter/actions/workflows/security.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/python-constricter)](https://pypi.org/project/python-constricter/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/python-constricter)](https://pypi.org/project/python-constricter/)
38
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ivylikethevine/python-constricter/badge)](https://scorecard.dev/viewer/?uri=github.com/ivylikethevine/python-constricter)
39
+ [![Test coverage: 100%](https://img.shields.io/badge/test_coverage-100%25-brightgreen)](pyproject.toml)
40
+ [![Annotations: 100%](https://img.shields.io/badge/annotations-100%25-brightgreen)](#annotation-coverage)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE.md)
42
+
43
+ Lint rules: every local variable is typed where it's first bound. Ships as a flake8 plugin, a pylint
44
+ plugin and a standalone command (for ruff, which loads no plugins).
45
+
46
+ ```text
47
+ /^\/^\
48
+ _|__| O|
49
+ \/ /~ \_/ \
50
+ \____|__________/ \
51
+ \_______ \
52
+ `\ \ \
53
+ | | \
54
+ / / \
55
+ / / \\
56
+ / / \ \
57
+ / / \ \
58
+ / / _----_ \ \
59
+ / / _-~ ~-_ | |
60
+ ( ( _-~ _--_ ~-_ _/ |
61
+ \ ~-____-~ _-~ ~-_ ~-_-~ /
62
+ ~-_ _-~ ~-_ _-~
63
+ ~--______-~ ~-___-~
64
+ ```
65
+
66
+ ---
67
+
68
+ ```python
69
+ def total(items: list[int]) -> int:
70
+ count = 0 # LVA001
71
+ result: int = 0 # ok
72
+ first, *rest = items # LVA001 twice
73
+ head: int
74
+ tail: list[int]
75
+ head, *tail = items # ok: declared first
76
+ if (n := len(items)) > 3: # LVA001
77
+ result = n # ok: rebinding
78
+ for item in items: # LVA002
79
+ result += item
80
+ for other in items: # type: int # LVA003
81
+ result += other
82
+ value: int
83
+ for value in items: # ok: declared first
84
+ result += value
85
+ return result
86
+ ```
87
+
88
+ ## Rules
89
+
90
+ Checked per function body, including methods and nested functions; with `all-scopes`, module and
91
+ class bodies too. Statements are read in source order, and only a name's first binding counts
92
+ (`LVA008`–`LVA010` aside, which read every one).
93
+
94
+ | Code | Reports | Fix |
95
+ | -------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
96
+ | `LVA001` | `=`, unpacking, `:=` or `with ... as` in a function without annotation | `name: T = ...`, or `name: T` first |
97
+ | `LVA002` | an untyped `for` target or `match` capture | `name: T` first (or a type comment) |
98
+ | `LVA003` | a `for` target typed only by `# type: T` | `name: T` first |
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, past `vague` | name the real type |
101
+ | `LVA006` | an annotation nested `nesting` deep (3 by default) | a `type` alias for a part of it |
102
+ | `LVA007` | a name annotated again with the type it already has, in the same block | drop the second annotation |
103
+ | `LVA008` | with every value the name ever holds known: an annotation that could narrow to them | narrow it (`total: int`) |
104
+ | `LVA009` | a value, anywhere in the name's lifetime, whose type doesn't fit its annotation | fix the value, or widen the annotation |
105
+ | `LVA010` | with every value known: a union member no value is | drop the member |
106
+ | `LVA011` | an annotation listing a fixed-length tuple of more than `max-length` types (4 by default) | name the fields (a `NamedTuple`, a dataclass) |
107
+ | `LVA012` | opt-in: a local bound once, outside any loop, and never rebound | `name: Final = ...` |
108
+
109
+ Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
110
+ `global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
111
+ enum members (a base imported from `enum`, however it's aliased, or else whose name ends in `Enum`
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.
116
+
117
+ A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
118
+ `type-comments`, or automatically in a module written to run on Python 2: one that imports
119
+ `print_function`, `unicode_literals`, `absolute_import`, `division`, `with_statement`, `generators`
120
+ or `nested_scopes` from `__future__`.
121
+
122
+ `LVA007` compares a block on its own: an `if`'s body and its `orelse`, a `try`'s body and its
123
+ `except`s, and the like, are different blocks, since they don't both run in the same pass.
124
+
125
+ `LVA009` checks every binding of an annotated name (or parameter) in the scope, wherever and in
126
+ whatever order they run, but only one whose value's type `--fix` would infer with certainty, and
127
+ only against types whose every subclass is known: builtins, and classes the module defines on such
128
+ bases. An imported class, a protocol or an alias is never compared, builtin containers are compared
129
+ by the container alone (`flags: tuple[str, ...] = ("-q",)` fits), a copy of a union-typed name is
130
+ skipped (an `is None` check may have narrowed it), and so is a name annotated only under
131
+ `if TYPE_CHECKING:`. It has no `--fix`.
132
+
133
+ `LVA008` and `LVA010` also need every binding's value known (one unknown call, loop target or
134
+ unpacking and the name could hold anything), and are only claimed for a function's own names: a
135
+ module or class variable is state other code rebinds out of sight (`mod.X = ...`, `self.x = ...`,
136
+ `monkeypatch`), and a `nonlocal` write from a nested function counts as unknown too. Neither has a
137
+ `--fix`: `--fix` adds annotations, it doesn't rewrite them.
138
+
139
+ ## Levels
140
+
141
+ Each level makes one more code an error. The rest are warnings: the CLI prints them (as `::warning`
142
+ or SARIF `warning` in those formats) but exits 0; flake8 and pylint report errors only.
143
+
144
+ | Level | Errors | Warnings |
145
+ | ----------------- | -------------------------------------- | --------------------------------------------------------- |
146
+ | `relaxed` / `0` | none | `LVA001`–`LVA004`, `LVA007`, `LVA009` |
147
+ | `strict` / `1` | `LVA001`, `LVA004` (the default) | `LVA002`, `LVA003`, `LVA005`–`LVA007`, `LVA009`, `LVA011` |
148
+ | `constrict` / `2` | `LVA001`, `LVA004`, `LVA002`, `LVA009` | `LVA003`, `LVA005`–`LVA008`, `LVA010`, `LVA011` |
149
+ | `suffocate` / `3` | all | none |
150
+
151
+ `LVA005`, `LVA006` and `LVA011` aren't reported at `relaxed`; `LVA008` and `LVA010` only from
152
+ `constrict`. `LVA012` is opt-in, reported only when selected by its full code
153
+ (`--extend-select LVA012`, flake8's `extend-select = LVA012`, pylint's `enable = could-be-final`),
154
+ at every level, and an error only at `suffocate`: on the corpus, 45–70% of every codebase's first
155
+ bindings qualify, so it suits a codebase that wants `Final` everywhere it can go, not a default.
156
+
157
+ Python 3.11+, no runtime dependencies.
158
+
159
+ ## Use
160
+
161
+ | Tool | Setup | Reports | Suppress |
162
+ | ------ | ----------------------------------------------------- | ------------------------------- | ------------------------------------------------ |
163
+ | CLI | `constricter [PATH...] [--level L] [--format F] [-q]` | `LVA001`–`LVA012` | `# noqa: LVA001` |
164
+ | flake8 | install it (on by default) | `LVA001`–`LVA012` | `# noqa: LVA001` |
165
+ | pylint | `load-plugins = ["constricter.plugins.pylint"]` | `C9101`–`C9112` (symbols below) | `# noqa: LVA001` or `# pylint: disable=<symbol>` |
166
+ | ruff | run the CLI after ruff; set `lint.external = ["LVA"]` | `LVA001`–`LVA012` | `# noqa: LVA001` |
167
+
168
+ pylint symbols: `unannotated-local-variable`, `untyped-for-or-match-variable`,
169
+ `comment-typed-for-variable`, `unannotated-module-or-class-variable`, `vague-annotation`,
170
+ `deeply-nested-annotation`, `redundant-annotation`, `narrowable-annotation`,
171
+ `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, `could-be-final` (off until
172
+ enabled).
173
+
174
+ Options:
175
+
176
+ | Option | CLI | `[tool.constricter]` | flake8 (CLI or config) | pylint |
177
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
178
+ | level | `--level` | `level` | `--constricter-level` | `constricter-level` |
179
+ | strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
180
+ | every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
181
+ | type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
182
+ | all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
183
+ | nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
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` | - | - |
187
+ | type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
188
+ | fix | `--fix` (`--unsafe-fixes` for guesses, `--likely` for those measured to hold), `--diff` to preview | - | - | - |
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` | - | - |
191
+ | show fixes | `--show-fixes` (each fix and how it was decided; with `--fix`, those made too) | - | - | - |
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` | - | - |
194
+ | fix widen | `--fix-widen KINDS` (wider types `--fix` may write, each marked: `untyped-parameters`, `empty-containers`, `mixed-containers`, `unions`, `vague`, `untyped-calls`, `unknown-calls`; `all`: every one; [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#wider-types-fix-widen)) | `fix-widen` | - | - |
195
+ | select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
196
+ | 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` |
197
+ | ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
198
+ | exclude | `--exclude GLOB` (repeatable) | `exclude` | flake8's own `exclude` | pylint's own `ignore-paths` |
199
+ | format | `--format`: `text`, `full` (with source), `json`, `github`, `sarif`, `gitlab`, `junit`, `rdjson` | - | - | - |
200
+ | statistics | `--statistics` (counts per code, text format) | - | - | - |
201
+ | jobs | `--jobs N` (`-j`; 0: one per CPU) | `jobs` | flake8's own `--jobs` | pylint's own `--jobs` |
202
+ | baseline | `--baseline FILE`; `--write-baseline` records it | `baseline` | - | - |
203
+ | coverage | `--coverage`, `--fail-under PCT` | - | - | - |
204
+ | per-path levels | - | `per-path-levels` | - | - |
205
+ | per-file ignores | - | `per-file-ignores` | flake8's own `per-file-ignores` | - |
206
+ | stdin | `-` as the path, `--stdin-filename PATH` | - | flake8's own `-` | - |
207
+ | exit status | `--exit-zero` | - | flake8's own `--exit-zero` | pylint's own `--exit-zero` |
208
+ | output file | `--output-file FILE` | - | flake8's own `--output-file` | pylint's own `--output` |
209
+
210
+ `constricter --explain LVA002` prints a code's rationale, its fix, and the levels that report it.
211
+
212
+ The CLI reads `[tool.constricter]` from the nearest `pyproject.toml` above the current directory;
213
+ its flags override it, and `--exclude` adds to it. An unknown key or a bad value exits 2.
214
+
215
+ ```toml
216
+ [tool.constricter]
217
+ level = "constrict" # or 2
218
+ exclude = ["tests/fixtures/*"]
219
+ type-comments = false
220
+ all-scopes = true
221
+ nesting = 3
222
+ max-length = 4
223
+ vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
224
+ min-python = "3.11" # the default: your `requires-python`'s lower bound
225
+ jobs = 0
226
+ baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
227
+
228
+ # The first glob a file matches sets its level; other files get `level`.
229
+ [tool.constricter.per-path-levels]
230
+ "tests/*" = "strict"
231
+
232
+ # Codes (or prefixes) to drop for files matching a glob.
233
+ [tool.constricter.per-file-ignores]
234
+ "tests/fixtures/*" = ["LVA005", "LVA006"]
235
+
236
+ # Your own type hierarchy for LVA008–LVA010: each type, and the types it's narrower than.
237
+ [tool.constricter.narrower]
238
+ UserId = ["str"] # an imported NewType the rules then compare
239
+ int = [] # an `int` no longer fits `float`
240
+ select = ["LVA00"]
241
+ ignore = ["LVA003"]
242
+ ```
243
+
244
+ `--fix` adds the annotation where the value decides it: a literal (`count = 0` becomes
245
+ `count: int = 0`), a container whose elements agree, a constructor or a function that declares its
246
+ return type or whose `return`s agree (in another checked file too), a copy, subscript, attribute or
247
+ method call of a local whose type is known, and values computed from those; a loop's target or an
248
+ unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, `--likely`
249
+ only those measured to hold as often as a certain fix
250
+ ([docs/LIKELY.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/LIKELY.md)),
251
+ and `--show-fixes` lists each fix and how its value decided it, and after `--fix` each one made.
252
+ `--infer-with basedpyright` (or `ty`, or `pyrefly`, or several: `basedpyright,ty`; each installed
253
+ alongside) asks those type checkers for what `--fix` can't type itself, as guesses;
254
+ `--infer-from FILE` takes the types a run bound each name to, recorded by
255
+ `python -m constricter.trace -m pytest`, the same way. The full list is in
256
+ [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
257
+
258
+ ### Installing and running
259
+
260
+ ```bash
261
+ pip install python-constricter # into the project's environment
262
+ uvx --from python-constricter constricter # or run it without installing: uv's tool runner
263
+ pipx run --spec python-constricter constricter # or pipx's
264
+ ```
265
+
266
+ On CPython 3.11 to 3.14 for Linux, macOS and Windows, pip installs a build compiled with mypyc,
267
+ about a fifth faster; anywhere else, the pure Python one, which finds and fixes the same.
268
+
269
+ Tools that run flake8 or pylint (VS Code's extensions, python-lsp-server, prospector, MegaLinter,
270
+ Trunk) pick the plugin up once it's installed alongside them.
271
+
272
+ Without `lint.external`, ruff flags `# noqa: LVA00x` (RUF102) and `--fix` deletes it.
273
+
274
+ The CLI defaults to `.`, checks `*.py` and `*.ipynb`, and skips hidden dirs, `__pycache__`, `venv`,
275
+ `site-packages`, `build`, `dist` and `node_modules` by directory name; `--exclude` adds more
276
+ directory names (or globs) to skip the same way, on top of matching whole paths and file names.
277
+ Under a directory in a git checkout it checks only what git tracks or would, so nothing a
278
+ `.gitignore` names (a file named on the command line is checked regardless); `--no-gitignore`, or
279
+ `gitignore = false`, walks the directory whole. Exit codes: `0` no errors, `1` errors, `2` an
280
+ unreadable or unparsable file, a `--fix` its encoding can't hold, or a bad `pyproject.toml`. A
281
+ module is read in its PEP 263 declaration's encoding.
282
+
283
+ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
284
+
285
+ ```yaml
286
+ - repo: https://github.com/ivylikethevine/python-constricter
287
+ rev: vX.Y.Z # the latest release; `pre-commit autoupdate` sets it
288
+ hooks:
289
+ - id: constricter
290
+ ```
291
+
292
+ pre-commit.ci, tox, nox, Bazel, Pants and the GitHub Action (PR annotations, a summary table and a
293
+ SARIF log) are in
294
+ [docs/INTEGRATIONS.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/INTEGRATIONS.md);
295
+ settings for VS Code, Zed and Neovim are in
296
+ [docs/editors/](https://github.com/ivylikethevine/python-constricter/blob/main/docs/editors/README.md).
297
+
298
+ ### Baselines
299
+
300
+ To adopt constricter on a codebase that already has offences, record them, then report only new
301
+ ones:
302
+
303
+ ```bash
304
+ constricter --write-baseline src # writes constricter-baseline.json next to pyproject.toml
305
+ constricter src # reports only offences the baseline doesn't cover
306
+ ```
307
+
308
+ A baseline counts each file's offences by code and variable name, not line number, so it survives
309
+ code moving around; another offence for a name it covers is still reported. Paths in it are relative
310
+ to it. It's JSON, and like every JSON file constricter reads it may have `//` and `/* */` comments
311
+ and trailing commas. `--baseline FILE` or `baseline` in `[tool.constricter]` names another file; the
312
+ default one is used only if it exists.
313
+
314
+ ### Annotation coverage
315
+
316
+ `constricter --coverage src` prints the share of first bindings that are typed, per file and in
317
+ total; the bindings are the ones the rules cover (with `--all-scopes`, module and class bodies too),
318
+ and `# noqa` doesn't make one typed. `--fail-under PCT` (which implies `--coverage`) exits 1 below
319
+ PCT, so CI can hold a codebase to a share. With `--format=json` it prints
320
+ `{"typed", "total", "widened", "percent", "files"}`, which a badge can read: publish that JSON
321
+ somewhere (a gist, a release asset) and point
322
+ [shields.io's dynamic JSON badge](https://shields.io/badges/dynamic-json-badge) at it with the query
323
+ `$.percent`. This project keeps its own share at 100% in CI, so its badge is static.
324
+
325
+ ### Notebooks
326
+
327
+ `.ipynb` files are checked too (directories include them): their code cells are read as one module,
328
+ IPython-only lines (`%magic`, `!shell`, `obj?`, `%%cell` magics) are skipped, and each offence is
329
+ reported at its cell and line (`analysis.ipynb:cell 3:2:5`). JSON output has a `cell` field; GitHub
330
+ and SARIF output point at the file and put the cell in the message. `--fix` and `--diff` edit the
331
+ cells, keeping the notebook's formatting.
332
+
333
+ ### Adopting it on an existing codebase
334
+
335
+ 1. See the scale: `constricter --statistics src` counts offences per code.
336
+ 2. Record them: `constricter --write-baseline src`, and commit `constricter-baseline.json`.
337
+ 3. Enforce it for new code: add the pre-commit hook or the GitHub Action; the baseline keeps old
338
+ offences quiet, and `--diff` / `--fix` clear the easy ones.
339
+ 4. Burn it down: fix a file or package at a time, then `--write-baseline` again to shrink the file.
340
+ 5. Tighten: raise `level` (or `per-path-levels` for the parts that are clean), then turn on
341
+ `all-scopes`.
342
+
343
+ ### Output formats
344
+
345
+ `--format` is `text` (the default), `json`, `github` (workflow annotations), `sarif` (below),
346
+ `gitlab` (Code Climate JSON, for GitLab's merge-request Code Quality widget: pass the file as a
347
+ `codequality` report artifact), `junit` (a test suite per file, a failed test case per offence, for
348
+ Jenkins, Azure Pipelines, CircleCI or GitLab's test reports) or `rdjson` (for
349
+ [reviewdog](https://github.com/reviewdog/reviewdog), with each certain fix as a suggestion).
350
+ `--output-file FILE` writes the report there, and `--exit-zero` exits 0 even when there are errors
351
+ (not when a file can't be read).
352
+
353
+ ### Editors (standard input)
354
+
355
+ `constricter - --stdin-filename path/to/file.py` checks standard input, reported as that path (which
356
+ also picks its per-path level and baseline entry; a `.ipynb` name reads a notebook). With `--fix` it
357
+ prints the fixed source instead of a report, and `--diff` diffs it. That's what editors that lint
358
+ unsaved buffers through a command need (none-ls, nvim-lint, flycheck, ALE, efm-langserver, Helix).
359
+
360
+ ### SARIF (code scanning)
361
+
362
+ `--format=sarif` writes SARIF 2.1.0, with each result's level (`error` or `warning`) set by
363
+ `--level`. Each rule has its help text and a link to [Rules](#rules), and each certain fix is a
364
+ SARIF fix (columns count characters: `columnKind` is `unicodeCodePoints`). In GitHub Actions, upload
365
+ it to code scanning (the job needs `security-events: write`; the
366
+ [GitHub Action](https://github.com/ivylikethevine/python-constricter/blob/main/docs/INTEGRATIONS.md#github-actions)'s
367
+ `sarif-file` output does the same):
368
+
369
+ ```yaml
370
+ - run: constricter --format=sarif src > constricter.sarif
371
+ - if: ${{ !cancelled() }} # upload the findings even when the step above failed on them
372
+ uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
373
+ with:
374
+ sarif_file: constricter.sarif
375
+ category: constricter
376
+ ```
377
+
378
+ SonarQube and SonarCloud import it with `sonar.sarifReportPaths=constricter.sarif`; any other tool
379
+ that reads SARIF 2.1.0 takes the same file.
380
+
381
+ ## Development
382
+
383
+ Setting up, the checks CI runs, the corpus runs and the rules this project's own linters leave off
384
+ are in
385
+ [docs/CONTRIBUTING.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/CONTRIBUTING.md).
386
+
387
+ ## Roadmap
388
+
389
+ What's done and what's next, by scope:
390
+ [docs/ROADMAP.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/ROADMAP.md).
391
+
392
+ ## AI usage
393
+
394
+ Heavily inspired by
395
+ [Dictionarry/Profilarr's AI Transparency Statement](https://v2.dictionarry.dev/ai-transparency).
396
+
397
+ I have used generative AI to write large parts of this code. All of the code here is my
398
+ _responsibility_ regardless: AI is a tool, not an owner of a project. I have personally understood,
399
+ reviewed, and approved all of the AI-generated code in this repository, and **mainline releases**
400
+ carry the same accountability to me as anything I write and publish myself.