python-constricter 0.4.2__tar.gz → 0.4.4__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 (195) hide show
  1. {python_constricter-0.4.2 → python_constricter-0.4.4}/.gitignore +0 -2
  2. {python_constricter-0.4.2 → python_constricter-0.4.4}/PKG-INFO +39 -11
  3. {python_constricter-0.4.2 → python_constricter-0.4.4}/README.md +38 -10
  4. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/__init__.py +0 -1
  5. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/__main__.py +3 -3
  6. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/_version.py +1 -2
  7. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/__init__.py +0 -1
  8. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/baseline.py +0 -1
  9. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/collecting.py +6 -4
  10. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/command.py +38 -15
  11. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/config.py +7 -6
  12. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/explain.py +21 -1
  13. python_constricter-0.4.4/constricter/cli/lsp/__init__.py +1 -0
  14. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/connection.py +19 -22
  15. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/edits.py +1 -2
  16. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/guard.py +10 -8
  17. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/hints.py +165 -53
  18. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/protocol.py +39 -16
  19. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/remembered.py +9 -10
  20. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/lsp/stored.py +6 -6
  21. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/options.py +24 -7
  22. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/paths.py +0 -1
  23. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/reading.py +5 -1
  24. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/report.py +15 -8
  25. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/rounds.py +72 -27
  26. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/runs.py +34 -2
  27. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/schedule.py +48 -100
  28. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/cli/workers.py +10 -24
  29. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/empty.py +0 -1
  30. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/__init__.py +0 -1
  31. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/__init__.py +0 -1
  32. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/asked.py +0 -1
  33. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/fixes.py +16 -4
  34. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/imports.py +83 -58
  35. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/inherited.py +4 -5
  36. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/known.py +40 -27
  37. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/core/signatures.py +2 -3
  38. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/__init__.py +0 -1
  39. python_constricter-0.4.4/constricter/fix/index/across.py +145 -0
  40. python_constricter-0.4.4/constricter/fix/index/annotated.py +83 -0
  41. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/awaits.py +11 -12
  42. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/beyond.py +5 -6
  43. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/callers.py +37 -8
  44. python_constricter-0.4.4/constricter/fix/index/consistent.py +133 -0
  45. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/crossed.py +84 -35
  46. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/decorated.py +21 -9
  47. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/fixtures.py +21 -12
  48. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/linked.py +29 -30
  49. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/loose.py +9 -10
  50. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/managed.py +9 -10
  51. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/modules.py +90 -41
  52. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/offers.py +10 -17
  53. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/order.py +6 -11
  54. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/origins.py +7 -3
  55. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/own_methods.py +3 -4
  56. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/own_overloads.py +9 -5
  57. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/own_types.py +11 -15
  58. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/params.py +6 -6
  59. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/plain.py +3 -4
  60. python_constricter-0.4.4/constricter/fix/index/project.py +236 -0
  61. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/sides.py +10 -75
  62. python_constricter-0.4.2/constricter/fix/index/project.py → python_constricter-0.4.4/constricter/fix/index/spelling.py +152 -243
  63. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/strings.py +1 -2
  64. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/__init__.py +0 -1
  65. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/atoms.py +36 -25
  66. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/chained.py +2 -3
  67. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/classnames.py +3 -4
  68. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/declared.py +2 -3
  69. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/installed.py +134 -43
  70. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/names.py +8 -11
  71. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/read.py +6 -7
  72. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/signatures.py +11 -14
  73. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/stubs/stubbed.py +10 -12
  74. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/taken.py +10 -7
  75. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/index/tuples.py +7 -11
  76. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/__init__.py +0 -1
  77. python_constricter-0.4.4/constricter/fix/libraries/generics.py +50 -0
  78. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/library.py +60 -22
  79. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/opened.py +0 -1
  80. python_constricter-0.4.4/constricter/fix/libraries/operators.py +185 -0
  81. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/overloads.py +90 -224
  82. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/receivers.py +2 -11
  83. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/stdlib.py +55 -11
  84. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/libraries/walks.py +0 -1
  85. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/source.json +1 -1
  86. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/__init__.py +0 -1
  87. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/aliased.py +8 -19
  88. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/bodies.py +41 -5
  89. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/callables.py +14 -22
  90. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/called.py +20 -24
  91. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/classvars.py +34 -32
  92. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/decided.py +2 -3
  93. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/displays.py +0 -1
  94. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/doubts.py +50 -31
  95. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/entered.py +12 -19
  96. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/fills.py +176 -33
  97. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/guesses.py +34 -34
  98. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/hinted.py +181 -41
  99. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/inference.py +87 -55
  100. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/loops.py +5 -8
  101. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/members.py +142 -20
  102. python_constricter-0.4.4/constricter/fix/values/narrowed.py +483 -0
  103. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/operated.py +0 -1
  104. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/ordered.py +1 -2
  105. python_constricter-0.4.4/constricter/fix/values/reads.py +388 -0
  106. python_constricter-0.4.4/constricter/fix/values/refused.py +87 -0
  107. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/returned.py +46 -48
  108. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/returns.py +2 -2
  109. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/shapes.py +30 -20
  110. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/stored.py +64 -35
  111. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/fix/values}/tables.py +4 -5
  112. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/targets.py +18 -6
  113. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/values/unchecked.py +27 -14
  114. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/jsonc.py +0 -1
  115. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/noqa.py +0 -1
  116. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/notebook.py +2 -3
  117. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/offences.py +102 -41
  118. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/overrides.py +0 -1
  119. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/plugins/__init__.py +0 -1
  120. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/plugins/flake8.py +50 -50
  121. python_constricter-0.4.4/constricter/plugins/options.py +41 -0
  122. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/plugins/pylint.py +36 -65
  123. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/recording.py +63 -23
  124. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/__init__.py +0 -1
  125. python_constricter-0.4.4/constricter/rules/attributes.py +258 -0
  126. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/binding.py +149 -57
  127. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/calls.py +105 -22
  128. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/checker.py +115 -32
  129. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/late.py +46 -312
  130. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/narrowing.py +3 -4
  131. python_constricter-0.4.4/constricter/rules/parameters.py +94 -0
  132. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/rebinding.py +1 -2
  133. python_constricter-0.4.4/constricter/rules/rebound.py +296 -0
  134. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/recorded.py +8 -4
  135. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/redundant.py +2 -3
  136. python_constricter-0.4.4/constricter/rules/returning.py +251 -0
  137. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/scope.py +186 -38
  138. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/scoped.py +11 -6
  139. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/scoping.py +9 -11
  140. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/settings.py +69 -40
  141. python_constricter-0.4.4/constricter/rules/signatures.py +77 -0
  142. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/rules/widened.py +10 -8
  143. python_constricter-0.4.4/constricter/source/__init__.py +1 -0
  144. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/annotations.py +2 -3
  145. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/declarations.py +170 -14
  146. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/decorators.py +11 -30
  147. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/flow.py +1 -2
  148. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/keys.py +1 -2
  149. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/parsed.py +2 -3
  150. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/quoted.py +0 -1
  151. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/syntax.py +99 -46
  152. {python_constricter-0.4.2/constricter/rules → python_constricter-0.4.4/constricter/source}/walked.py +17 -11
  153. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/trace.py +104 -35
  154. {python_constricter-0.4.2/constricter/cli → python_constricter-0.4.4/constricter}/traced.py +54 -9
  155. {python_constricter-0.4.2 → python_constricter-0.4.4}/hatch_build.py +0 -1
  156. {python_constricter-0.4.2 → python_constricter-0.4.4}/mypyc.txt +3 -3
  157. {python_constricter-0.4.2 → python_constricter-0.4.4}/pyproject.toml +32 -20
  158. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/__init__.py +0 -1
  159. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/__main__.py +0 -1
  160. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/accepting.py +0 -1
  161. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/configured.py +0 -1
  162. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/elements.py +0 -1
  163. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/forms.py +1 -2
  164. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/generate.py +8 -9
  165. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/overloads.py +0 -1
  166. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/reading.py +0 -1
  167. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/shed.py +0 -1
  168. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/stubs.py +0 -1
  169. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/templates.py +0 -1
  170. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/written.py +0 -1
  171. python_constricter-0.4.2/constricter/cli/lsp/__init__.py +0 -2
  172. python_constricter-0.4.2/constricter/fix/values/narrowed.py +0 -220
  173. {python_constricter-0.4.2 → python_constricter-0.4.4}/LICENSE.md +0 -0
  174. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/aliases.json +0 -0
  175. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/attributes.json +0 -0
  176. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/awaited.json +0 -0
  177. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/awaited_overloads.json +0 -0
  178. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/bases.json +0 -0
  179. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/classes.json +0 -0
  180. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/elements.json +0 -0
  181. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/functions.json +0 -0
  182. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/generic_attributes.json +0 -0
  183. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/method_overloads.json +0 -0
  184. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/method_signatures.json +0 -0
  185. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/methods.json +0 -0
  186. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/overloads.json +0 -0
  187. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/platform.json +0 -0
  188. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/returns.json +0 -0
  189. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/scalar_members.json +0 -0
  190. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/scalars.json +0 -0
  191. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/subscriptable.json +0 -0
  192. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/type_parameters.json +0 -0
  193. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/fix/tables/variables.json +0 -0
  194. {python_constricter-0.4.2 → python_constricter-0.4.4}/constricter/py.typed +0 -0
  195. {python_constricter-0.4.2 → python_constricter-0.4.4}/stdlib_tables/partial.json +0 -0
@@ -1,5 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
-
3
1
  # Everything generated on this machine (scratch, tool caches, local builds).
4
2
  local/
5
3
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-constricter
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -105,6 +105,9 @@ class bodies too. Statements are read in source order, and only a name's first b
105
105
  | `LVA010` | with every value known: a union member no value is | drop the member |
106
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
107
  | `LVA012` | opt-in: a local bound once, outside any loop, and never rebound | `name: Final = ...` |
108
+ | `LVA013` | opt-in: an instance attribute a plain class's method first stores, declared nowhere in it | `self.x: T = ...` |
109
+ | `LVA014` | opt-in: a plain function that declares no return type | `def f(...) -> T:` |
110
+ | `LVA015` | opt-in: a parameter with no annotation | `def f(x: T):` |
108
111
 
109
112
  Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
110
113
  `global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
@@ -117,7 +120,8 @@ type checker: `T = TypeVar("T")`, a `ParamSpec`, a `TypeVarTuple`, a `NewType`,
117
120
  A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
118
121
  `type-comments`, or automatically in a module written to run on Python 2: one that imports
119
122
  `print_function`, `unicode_literals`, `absolute_import`, `division`, `with_statement`, `generators`
120
- or `nested_scopes` from `__future__`.
123
+ or `nested_scopes` from `__future__`. Otherwise it's `LVA001`, and `--fix` writes the comment's type
124
+ as the annotation and drops the comment.
121
125
 
122
126
  `LVA007` compares a block on its own: an `if`'s body and its `orelse`, a `try`'s body and its
123
127
  `except`s, and the like, are different blocks, since they don't both run in the same pass.
@@ -154,29 +158,51 @@ or SARIF `warning` in those formats) but exits 0; flake8 and pylint report error
154
158
  at every level, and an error only at `suffocate`: on the corpus, 45–70% of every codebase's first
155
159
  bindings qualify, so it suits a codebase that wants `Final` everywhere it can go, not a default.
156
160
 
161
+ `LVA013` is opt-in the same way (pylint's `enable = unannotated-instance-attribute`), and an error
162
+ from `strict`, as `LVA001` is. It reports an instance attribute a plain class's method stores
163
+ (`self.count = 0`) that no annotation in the class declares, at its first plain `self.x = value`:
164
+ a class the module defines once, with no keyword (a metaclass), on no base but `object` or another
165
+ such class, since another file's base may declare the attribute itself. Dunders, stores in a
166
+ nested function, and names the class's body or a base of the module's binds are left out.
167
+
168
+ `LVA014` is opt-in too (pylint's `enable = missing-return-type`), an error from `strict`. It
169
+ reports a plain function or method (not decorated) with no return annotation, as ruff's `ANN201`
170
+ does; its fix is what the function's own `return`s give (`-> int`, a method's a guess, as is a
171
+ function's whose `return`s' values are guessed), or `-> None` for one that returns nothing, or what
172
+ a traced run saw it return (`--infer-from`, a guess), where the type names only what the module
173
+ already binds. `LVA015` is opt-in the same way (pylint's
174
+ `enable = missing-parameter-type`): a parameter with no annotation, as ruff's `ANN001` reports it,
175
+ but a method's receiver; its fix, a guess, is what typed the parameter for the function's own
176
+ check: what every call passes it, the one class with every member the function reads of it, its
177
+ pytest fixture's value, or the one type the checked files annotate its name with. A signature is
178
+ written whole or not at all: a return on bare parameters, or one parameter's type among bare ones,
179
+ is an error to a checker held to complete signatures where none was. A signature `--fix` writes
180
+ reaches the other files in the same run.
181
+
157
182
  Python 3.11+, no runtime dependencies.
158
183
 
159
184
  ## Use
160
185
 
161
186
  | Tool | Setup | Reports | Suppress |
162
187
  | ------ | ----------------------------------------------------- | ------------------------------- | ------------------------------------------------ |
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` |
188
+ | CLI | `constricter [PATH...] [--level L] [--format F] [-q]` | `LVA001`–`LVA015` | `# noqa: LVA001` |
189
+ | flake8 | install it (on by default) | `LVA001`–`LVA015` | `# noqa: LVA001` |
190
+ | pylint | `load-plugins = ["constricter.plugins.pylint"]` | `C9101`–`C9115` (symbols below) | `# noqa: LVA001` or `# pylint: disable=<symbol>` |
191
+ | ruff | run the CLI after ruff; set `lint.external = ["LVA"]` | `LVA001`–`LVA015` | `# noqa: LVA001` |
167
192
 
168
193
  pylint symbols: `unannotated-local-variable`, `untyped-for-or-match-variable`,
169
194
  `comment-typed-for-variable`, `unannotated-module-or-class-variable`, `vague-annotation`,
170
195
  `deeply-nested-annotation`, `redundant-annotation`, `narrowable-annotation`,
171
- `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, `could-be-final` (off until
172
- enabled).
196
+ `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, and, off until enabled,
197
+ `could-be-final`, `unannotated-instance-attribute`, `missing-return-type` and
198
+ `missing-parameter-type`.
173
199
 
174
200
  Options:
175
201
 
176
202
  | Option | CLI | `[tool.constricter]` | flake8 (CLI or config) | pylint |
177
203
  | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
178
204
  | level | `--level` | `level` | `--constricter-level` | `constricter-level` |
179
- | strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
205
+ | strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` and `LVA013` | — | — | — |
180
206
  | every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
181
207
  | type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
182
208
  | all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
@@ -369,7 +395,7 @@ it to code scanning (the job needs `security-events: write`; the
369
395
  ```yaml
370
396
  - run: constricter --format=sarif src > constricter.sarif
371
397
  - if: ${{ !cancelled() }} # upload the findings even when the step above failed on them
372
- uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
398
+ uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
373
399
  with:
374
400
  sarif_file: constricter.sarif
375
401
  category: constricter
@@ -386,8 +412,10 @@ are in
386
412
 
387
413
  ## Roadmap
388
414
 
389
- What's done and what's next, by scope:
415
+ What's next, by size:
390
416
  [docs/ROADMAP.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/ROADMAP.md).
417
+ What's done, by scope:
418
+ [docs/COMPLETED.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/COMPLETED.md).
391
419
 
392
420
  ## AI usage
393
421
 
@@ -78,6 +78,9 @@ class bodies too. Statements are read in source order, and only a name's first b
78
78
  | `LVA010` | with every value known: a union member no value is | drop the member |
79
79
  | `LVA011` | an annotation listing a fixed-length tuple of more than `max-length` types (4 by default) | name the fields (a `NamedTuple`, a dataclass) |
80
80
  | `LVA012` | opt-in: a local bound once, outside any loop, and never rebound | `name: Final = ...` |
81
+ | `LVA013` | opt-in: an instance attribute a plain class's method first stores, declared nowhere in it | `self.x: T = ...` |
82
+ | `LVA014` | opt-in: a plain function that declares no return type | `def f(...) -> T:` |
83
+ | `LVA015` | opt-in: a parameter with no annotation | `def f(x: T):` |
81
84
 
82
85
  Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
83
86
  `global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
@@ -90,7 +93,8 @@ type checker: `T = TypeVar("T")`, a `ParamSpec`, a `TypeVarTuple`, a `NewType`,
90
93
  A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
91
94
  `type-comments`, or automatically in a module written to run on Python 2: one that imports
92
95
  `print_function`, `unicode_literals`, `absolute_import`, `division`, `with_statement`, `generators`
93
- or `nested_scopes` from `__future__`.
96
+ or `nested_scopes` from `__future__`. Otherwise it's `LVA001`, and `--fix` writes the comment's type
97
+ as the annotation and drops the comment.
94
98
 
95
99
  `LVA007` compares a block on its own: an `if`'s body and its `orelse`, a `try`'s body and its
96
100
  `except`s, and the like, are different blocks, since they don't both run in the same pass.
@@ -127,29 +131,51 @@ or SARIF `warning` in those formats) but exits 0; flake8 and pylint report error
127
131
  at every level, and an error only at `suffocate`: on the corpus, 45–70% of every codebase's first
128
132
  bindings qualify, so it suits a codebase that wants `Final` everywhere it can go, not a default.
129
133
 
134
+ `LVA013` is opt-in the same way (pylint's `enable = unannotated-instance-attribute`), and an error
135
+ from `strict`, as `LVA001` is. It reports an instance attribute a plain class's method stores
136
+ (`self.count = 0`) that no annotation in the class declares, at its first plain `self.x = value`:
137
+ a class the module defines once, with no keyword (a metaclass), on no base but `object` or another
138
+ such class, since another file's base may declare the attribute itself. Dunders, stores in a
139
+ nested function, and names the class's body or a base of the module's binds are left out.
140
+
141
+ `LVA014` is opt-in too (pylint's `enable = missing-return-type`), an error from `strict`. It
142
+ reports a plain function or method (not decorated) with no return annotation, as ruff's `ANN201`
143
+ does; its fix is what the function's own `return`s give (`-> int`, a method's a guess, as is a
144
+ function's whose `return`s' values are guessed), or `-> None` for one that returns nothing, or what
145
+ a traced run saw it return (`--infer-from`, a guess), where the type names only what the module
146
+ already binds. `LVA015` is opt-in the same way (pylint's
147
+ `enable = missing-parameter-type`): a parameter with no annotation, as ruff's `ANN001` reports it,
148
+ but a method's receiver; its fix, a guess, is what typed the parameter for the function's own
149
+ check: what every call passes it, the one class with every member the function reads of it, its
150
+ pytest fixture's value, or the one type the checked files annotate its name with. A signature is
151
+ written whole or not at all: a return on bare parameters, or one parameter's type among bare ones,
152
+ is an error to a checker held to complete signatures where none was. A signature `--fix` writes
153
+ reaches the other files in the same run.
154
+
130
155
  Python 3.11+, no runtime dependencies.
131
156
 
132
157
  ## Use
133
158
 
134
159
  | Tool | Setup | Reports | Suppress |
135
160
  | ------ | ----------------------------------------------------- | ------------------------------- | ------------------------------------------------ |
136
- | CLI | `constricter [PATH...] [--level L] [--format F] [-q]` | `LVA001`–`LVA012` | `# noqa: LVA001` |
137
- | flake8 | install it (on by default) | `LVA001`–`LVA012` | `# noqa: LVA001` |
138
- | pylint | `load-plugins = ["constricter.plugins.pylint"]` | `C9101`–`C9112` (symbols below) | `# noqa: LVA001` or `# pylint: disable=<symbol>` |
139
- | ruff | run the CLI after ruff; set `lint.external = ["LVA"]` | `LVA001`–`LVA012` | `# noqa: LVA001` |
161
+ | CLI | `constricter [PATH...] [--level L] [--format F] [-q]` | `LVA001`–`LVA015` | `# noqa: LVA001` |
162
+ | flake8 | install it (on by default) | `LVA001`–`LVA015` | `# noqa: LVA001` |
163
+ | pylint | `load-plugins = ["constricter.plugins.pylint"]` | `C9101`–`C9115` (symbols below) | `# noqa: LVA001` or `# pylint: disable=<symbol>` |
164
+ | ruff | run the CLI after ruff; set `lint.external = ["LVA"]` | `LVA001`–`LVA015` | `# noqa: LVA001` |
140
165
 
141
166
  pylint symbols: `unannotated-local-variable`, `untyped-for-or-match-variable`,
142
167
  `comment-typed-for-variable`, `unannotated-module-or-class-variable`, `vague-annotation`,
143
168
  `deeply-nested-annotation`, `redundant-annotation`, `narrowable-annotation`,
144
- `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, `could-be-final` (off until
145
- enabled).
169
+ `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, and, off until enabled,
170
+ `could-be-final`, `unannotated-instance-attribute`, `missing-return-type` and
171
+ `missing-parameter-type`.
146
172
 
147
173
  Options:
148
174
 
149
175
  | Option | CLI | `[tool.constricter]` | flake8 (CLI or config) | pylint |
150
176
  | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
151
177
  | level | `--level` | `level` | `--constricter-level` | `constricter-level` |
152
- | strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
178
+ | strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` and `LVA013` | — | — | — |
153
179
  | every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
154
180
  | type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
155
181
  | all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
@@ -342,7 +368,7 @@ it to code scanning (the job needs `security-events: write`; the
342
368
  ```yaml
343
369
  - run: constricter --format=sarif src > constricter.sarif
344
370
  - if: ${{ !cancelled() }} # upload the findings even when the step above failed on them
345
- uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
371
+ uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
346
372
  with:
347
373
  sarif_file: constricter.sarif
348
374
  category: constricter
@@ -359,8 +385,10 @@ are in
359
385
 
360
386
  ## Roadmap
361
387
 
362
- What's done and what's next, by scope:
388
+ What's next, by size:
363
389
  [docs/ROADMAP.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/ROADMAP.md).
390
+ What's done, by scope:
391
+ [docs/COMPLETED.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/COMPLETED.md).
364
392
 
365
393
  ## AI usage
366
394
 
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """Every local variable typed where it's first bound."""
3
2
 
4
3
  from constricter._version import __version__ # written by the build, from the git tag
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """`python -m constricter`, and the `constricter` script."""
3
2
 
4
3
  import contextlib
@@ -13,9 +12,10 @@ def run() -> None:
13
12
 
14
13
  Not by the interpreter's own exit, which frees every parsed file and the index an object at a
15
14
  time: a fifth of a check of a thousand files. What the command wrote is flushed first; nothing
16
- else waits on the exit (its workers have ended, its output file is closed).
15
+ else waits on the exit (its workers have ended, its output file is closed). Nor is the collector
16
+ turned on again for it (`leaving`).
17
17
  """
18
- status: int = main()
18
+ status: int = main(leaving=True)
19
19
  with contextlib.suppress(OSError, ValueError): # a reader that left, a stream closed already
20
20
  _ = sys.stdout.flush()
21
21
  _ = sys.stderr.flush()
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """The version this was built as, from its git tag (written by the build, not tracked)."""
3
2
 
4
- __version__ = "0.4.2"
3
+ __version__ = "0.4.4"
@@ -1,2 +1 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """The `constricter` command (`command.main`): its options, the files it checks, and how it reports."""
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """Baseline files: the offences a codebase already has, so only new ones are reported.
3
2
 
4
3
  An entry is a file, a code and a variable name, with how many times it occurs; line numbers aren't
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """The garbage collector, as the command runs it: by hand, once per file checked.
3
2
 
4
3
  A check keeps every file's tree (see `parsed`), and allocates a great deal for each file, little of it
@@ -24,9 +23,12 @@ _YOUNGER: Final = not sysconfig.get_config_var("Py_GIL_DISABLED")
24
23
 
25
24
 
26
25
  @contextlib.contextmanager
27
- def by_hand() -> Generator[None]:
26
+ def by_hand(*, leaving: bool = False) -> Generator[None]:
28
27
  """Turn automatic collection off for the command's run, and back as it was after.
29
28
 
29
+ Not for a process that leaves once the run's done (`leaving`): turned on again, the collector
30
+ looked through all the run left alive, at once (a check of pandas: 3.4s, for nothing).
31
+
30
32
  Yields:
31
33
  Nothing: the run.
32
34
 
@@ -36,9 +38,9 @@ def by_hand() -> Generator[None]:
36
38
  try:
37
39
  yield
38
40
  finally:
39
- if _FREEZES:
41
+ if _FREEZES and not leaving:
40
42
  gc.unfreeze()
41
- if was:
43
+ if was and not leaving:
42
44
  gc.enable()
43
45
 
44
46
 
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """The `constricter` command (see README)."""
3
2
 
4
3
  import codecs
@@ -18,7 +17,7 @@ from constricter.cli import baseline, collecting, schedule
18
17
  from constricter.cli.lsp.protocol import HintError
19
18
  from constricter.cli.options import Mode, Options, Output
20
19
  from constricter.cli.paths import STDIN, python_files
21
- from constricter.cli.reading import encoding_of, read_text
20
+ from constricter.cli.reading import READ_ERRORS, encoding_of, read_text
22
21
  from constricter.cli.report import Format, Result, fix_reasons, render, statistics
23
22
  from constricter.cli.rounds import checked_all, settled
24
23
  from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
@@ -32,16 +31,17 @@ from constricter.offences import (
32
31
  Edit,
33
32
  Offence,
34
33
  )
35
- from constricter.rules import parsed
36
34
  from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
35
+ from constricter.source import parsed
37
36
 
38
37
  if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
38
+ from types import ModuleType
39
+
39
40
  from constricter.cli.lsp import hints
40
41
 
41
42
  EXIT_CLEAN: Final = 0
42
43
  EXIT_FOUND: Final = 1
43
44
  EXIT_ERROR: Final = 2
44
- _ALL: Final = 100 # percent
45
45
 
46
46
 
47
47
  def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
@@ -198,11 +198,21 @@ def _read_checked(
198
198
  checked: Checked
199
199
  try:
200
200
  raw, checked = _checked(path, name, checks, outside)
201
- except (OSError, ValueError, SyntaxError) as error: # UnicodeDecodeError is a ValueError
202
- return "", Checked([], Returns()), f"{name}: error: {error}"
201
+ except READ_ERRORS as error:
202
+ return "", Checked([], Returns()), _error(name, error)
203
203
  return raw, checked, ""
204
204
 
205
205
 
206
+ def _error(name: Path, error: Exception) -> str:
207
+ """Say why a file couldn't be read or parsed.
208
+
209
+ Returns:
210
+ The message.
211
+
212
+ """
213
+ return f"{name}: error: {error}"
214
+
215
+
206
216
  def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
207
217
  """Turn the offences in `raw`, the text of `name`, into the results to report.
208
218
 
@@ -325,8 +335,8 @@ def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
325
335
  name: Path = options.input.name(path)
326
336
  try:
327
337
  return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
328
- except (OSError, ValueError, SyntaxError) as error:
329
- return CoverageRun(error=f"{name}: error: {error}")
338
+ except READ_ERRORS as error:
339
+ return CoverageRun(error=_error(name, error))
330
340
 
331
341
 
332
342
  def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
@@ -370,12 +380,17 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
370
380
  return names, settled(paths, check, options, None)
371
381
  session: hints.Session
372
382
  # Imported only for `--infer-with`: it's slow to import, for every run's start.
373
- with cast("type[hints.Session]", importlib.import_module("constricter.cli.lsp.hints").Session)(
383
+ hinting: ModuleType = importlib.import_module("constricter.cli.lsp.hints")
384
+ asking: hints.Asking = cast("type[hints.Asking]", hinting.Asking)(
385
+ kept=not options.infer_fresh,
386
+ in_turn=options.infer_in_turn,
387
+ )
388
+ with cast("type[hints.Session]", hinting.Session)(
374
389
  options.infer_with,
375
390
  Path.cwd(),
376
391
  options.jobs,
377
392
  options.infer_memory,
378
- kept=not options.infer_fresh,
393
+ asking,
379
394
  ) as session:
380
395
  runs: list[FileRun] = settled(paths, check, options, session)
381
396
  checker: str
@@ -383,6 +398,11 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
383
398
  for checker, path in session.abandoned:
384
399
  name: Path = options.input.name(path)
385
400
  _ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
401
+ read: int
402
+ rounds: int
403
+ for checker, read, rounds in session.reused:
404
+ told: str = f"{checker}'s hints for {read} of {rounds} rounds were read from the store"
405
+ _ = sys.stderr.write(f"constricter: note: {told} (`--infer-fresh` asks again)\n")
386
406
  return names, runs
387
407
 
388
408
 
@@ -485,8 +505,8 @@ def _typed(found: Coverage) -> str:
485
505
  return f"{found.typed}/{found.total} typed ({found.percent:.1f}%){wider}"
486
506
 
487
507
 
488
- def _run(options: Options) -> int:
489
- """Check, fix, diff, count or baseline, printing what that finds.
508
+ def _run(options: Options, *, leaving: bool) -> int:
509
+ """Check, fix, diff, count or baseline, printing what that finds (`leaving`: see `main`).
490
510
 
491
511
  Returns:
492
512
  The exit status.
@@ -494,7 +514,7 @@ def _run(options: Options) -> int:
494
514
  """
495
515
  names: list[Path]
496
516
  runs: list[FileRun]
497
- with collecting.by_hand():
517
+ with collecting.by_hand(leaving=leaving):
498
518
  try:
499
519
  names, runs = _check_all(options)
500
520
  except HintError as error:
@@ -527,9 +547,12 @@ def _run(options: Options) -> int:
527
547
  return EXIT_ERROR if failed else status
528
548
 
529
549
 
530
- def main(argv: Sequence[str] | None = None) -> int:
550
+ def main(argv: Sequence[str] | None = None, *, leaving: bool = False) -> int:
531
551
  """Run the command.
532
552
 
553
+ `leaving`: the process leaves once it's done (`constricter.__main__.run`), its collector as the
554
+ run left it (see `collecting.by_hand`).
555
+
533
556
  Returns:
534
557
  Its exit status.
535
558
 
@@ -543,5 +566,5 @@ def main(argv: Sequence[str] | None = None) -> int:
543
566
  else contextlib.nullcontext(sys.stdout) as stream,
544
567
  contextlib.redirect_stdout(stream),
545
568
  ):
546
- status: int = _run(options)
569
+ status: int = _run(options, leaving=leaving)
547
570
  return EXIT_CLEAN if options.output.exit_zero and status == EXIT_FOUND else status
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """The command's defaults, from the nearest `pyproject.toml`'s `[tool.constricter]` table."""
3
2
 
4
3
  import re
@@ -221,11 +220,13 @@ def _checkers(value: _Toml) -> list[str] | None:
221
220
  """Read `infer-with`: a checker, or a list of them, each one it knows.
222
221
 
223
222
  Returns:
224
- Them, or `None` for anything else.
223
+ Them, each once in the order they're preferred, as `--infer-with` gives them; `None` for
224
+ anything else.
225
225
 
226
226
  """
227
227
  checkers: list[str] | None = [value] if isinstance(value, str) else _strings(value)
228
- return checkers if checkers and all(checker in SERVERS for checker in checkers) else None
228
+ known: bool = bool(checkers) and all(checker in SERVERS for checker in checkers or ())
229
+ return list(dict.fromkeys(checkers)) if checkers and known else None
229
230
 
230
231
 
231
232
  def _levels(value: _Toml) -> dict[str, str] | None:
@@ -248,7 +249,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
248
249
  return None if None in lists.values() else {key: list(item or []) for key, item in lists.items()}
249
250
 
250
251
 
251
- def _baseline(value: _Toml, root: Path) -> str | None:
252
+ def _path(value: _Toml, root: Path) -> str | None:
252
253
  """Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
253
254
 
254
255
  Returns:
@@ -303,8 +304,8 @@ def config_defaults(start: Path) -> dict[str, Default]:
303
304
  return {}
304
305
  readers: dict[str, Callable[[_Toml], Default | None]] = {
305
306
  **_READERS,
306
- "baseline": partial(_baseline, root=path.parent),
307
- "infer-from": partial(_baseline, root=path.parent),
307
+ "baseline": partial(_path, root=path.parent),
308
+ "infer-from": partial(_path, root=path.parent),
308
309
  }
309
310
  defaults: dict[str, Default] = {}
310
311
  key: str
@@ -1,4 +1,3 @@
1
- # SPDX-License-Identifier: MIT
2
1
  """Each code's rationale and fix, for `constricter --explain`."""
3
2
 
4
3
  from typing import Final
@@ -14,7 +13,10 @@ from constricter.offences import (
14
13
  OPT_IN,
15
14
  REDUNDANT_TYPE,
16
15
  UNANNOTATED,
16
+ UNANNOTATED_ATTRIBUTE,
17
17
  UNANNOTATED_MEMBER,
18
+ UNANNOTATED_PARAMETER,
19
+ UNANNOTATED_RETURN,
18
20
  UNTYPED_TARGET,
19
21
  UNUSED_UNION_MEMBER,
20
22
  VAGUE_TYPE,
@@ -83,6 +85,24 @@ _WHY: Final = {
83
85
  "(not from a nested function either) could say so: `limit: Final = 10`. Off unless selected\n"
84
86
  "by its full code (`--extend-select LVA012`): most locals are bound once."
85
87
  ),
88
+ UNANNOTATED_ATTRIBUTE: (
89
+ "An instance attribute a plain class's method first stores (`self.count = 0`), declared\n"
90
+ "nowhere in the class, is typed by whatever a type checker infers from that store. Annotate\n"
91
+ "it there: `self.count: int = 0`. Off unless selected by its full code (`--extend-select LVA013`)."
92
+ ),
93
+ UNANNOTATED_RETURN: (
94
+ "A plain function with no return annotation leaves its callers to whatever a type checker\n"
95
+ "infers. `--fix` writes what its own `return`s give (`-> int`), or `-> None` for one that\n"
96
+ "returns nothing. Off unless selected by its full code (`--extend-select LVA014`): ruff's ANN201\n"
97
+ "reports it too; this is for the fix."
98
+ ),
99
+ UNANNOTATED_PARAMETER: (
100
+ "A parameter with no annotation leaves the function's body to whatever a type checker\n"
101
+ "infers of it. `--fix` writes what typed it from outside, a guess: what every call passes it,\n"
102
+ "the one class with every member the function reads of it, its pytest fixture's value, or\n"
103
+ "the one type the checked files annotate its name with. Off unless selected by its full code\n"
104
+ "(`--extend-select LVA015`): ruff's ANN001 reports it too; this is for the fix."
105
+ ),
86
106
  }
87
107
 
88
108
 
@@ -0,0 +1 @@
1
+ """constricter.cli.lsp."""