python-constricter 0.4.3__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 (193) hide show
  1. {python_constricter-0.4.3 → python_constricter-0.4.4}/PKG-INFO +19 -8
  2. {python_constricter-0.4.3 → python_constricter-0.4.4}/README.md +18 -7
  3. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/_version.py +1 -1
  4. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/command.py +16 -7
  5. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/config.py +7 -5
  6. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/explain.py +8 -0
  7. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/connection.py +5 -10
  8. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/edits.py +1 -1
  9. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/guard.py +10 -7
  10. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/hints.py +23 -18
  11. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/protocol.py +25 -14
  12. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/remembered.py +9 -9
  13. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/stored.py +6 -5
  14. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/options.py +10 -5
  15. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/reading.py +5 -0
  16. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/report.py +14 -7
  17. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/rounds.py +59 -19
  18. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/runs.py +2 -1
  19. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/schedule.py +28 -103
  20. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/workers.py +10 -23
  21. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/fixes.py +5 -1
  22. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/imports.py +62 -53
  23. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/inherited.py +4 -4
  24. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/known.py +21 -26
  25. python_constricter-0.4.4/constricter/fix/index/across.py +145 -0
  26. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/annotated.py +1 -1
  27. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/awaits.py +11 -11
  28. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/beyond.py +5 -5
  29. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/callers.py +37 -7
  30. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/consistent.py +11 -20
  31. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/crossed.py +62 -33
  32. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/decorated.py +21 -8
  33. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/fixtures.py +7 -5
  34. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/linked.py +29 -29
  35. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/loose.py +9 -9
  36. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/managed.py +9 -9
  37. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/modules.py +69 -43
  38. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/offers.py +10 -16
  39. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/order.py +6 -10
  40. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/origins.py +5 -2
  41. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/own_methods.py +3 -3
  42. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/own_overloads.py +9 -4
  43. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/own_types.py +11 -14
  44. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/params.py +6 -5
  45. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/plain.py +3 -3
  46. python_constricter-0.4.4/constricter/fix/index/project.py +236 -0
  47. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/sides.py +10 -74
  48. python_constricter-0.4.3/constricter/fix/index/project.py → python_constricter-0.4.4/constricter/fix/index/spelling.py +83 -242
  49. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/strings.py +1 -1
  50. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/atoms.py +36 -24
  51. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/chained.py +2 -2
  52. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/classnames.py +3 -3
  53. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/declared.py +2 -2
  54. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/installed.py +89 -24
  55. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/names.py +8 -10
  56. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/read.py +6 -6
  57. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/signatures.py +11 -13
  58. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/stubbed.py +6 -9
  59. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/taken.py +10 -6
  60. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/tuples.py +7 -10
  61. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/library.py +10 -10
  62. python_constricter-0.4.4/constricter/fix/libraries/operators.py +185 -0
  63. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/overloads.py +11 -175
  64. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/receivers.py +2 -10
  65. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/stdlib.py +34 -18
  66. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/source.json +1 -1
  67. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/aliased.py +8 -18
  68. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/bodies.py +41 -4
  69. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/callables.py +14 -21
  70. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/called.py +20 -23
  71. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/classvars.py +24 -31
  72. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/decided.py +2 -2
  73. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/doubts.py +25 -27
  74. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/entered.py +12 -18
  75. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/fills.py +20 -22
  76. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/guesses.py +21 -27
  77. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/hinted.py +7 -7
  78. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/inference.py +67 -53
  79. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/loops.py +3 -5
  80. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/members.py +12 -27
  81. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/narrowed.py +14 -12
  82. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/ordered.py +1 -1
  83. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/reads.py +57 -25
  84. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/refused.py +1 -1
  85. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/returned.py +46 -47
  86. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/returns.py +2 -1
  87. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/shapes.py +13 -14
  88. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/stored.py +25 -23
  89. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/fix/values}/tables.py +4 -4
  90. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/targets.py +18 -5
  91. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/unchecked.py +2 -2
  92. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/notebook.py +2 -2
  93. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/offences.py +90 -44
  94. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/plugins/flake8.py +31 -47
  95. python_constricter-0.4.4/constricter/plugins/options.py +41 -0
  96. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/plugins/pylint.py +29 -63
  97. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/recording.py +63 -22
  98. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/attributes.py +2 -3
  99. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/binding.py +32 -24
  100. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/calls.py +44 -16
  101. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/checker.py +83 -29
  102. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/late.py +20 -305
  103. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/narrowing.py +3 -3
  104. python_constricter-0.4.4/constricter/rules/parameters.py +94 -0
  105. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/rebinding.py +1 -1
  106. python_constricter-0.4.4/constricter/rules/rebound.py +296 -0
  107. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/recorded.py +2 -2
  108. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/redundant.py +2 -2
  109. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/returning.py +49 -29
  110. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/scope.py +6 -6
  111. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/scoped.py +4 -4
  112. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/scoping.py +7 -7
  113. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/settings.py +41 -43
  114. python_constricter-0.4.4/constricter/rules/signatures.py +77 -0
  115. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/widened.py +10 -7
  116. python_constricter-0.4.4/constricter/source/__init__.py +1 -0
  117. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/annotations.py +2 -2
  118. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/declarations.py +15 -17
  119. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/decorators.py +11 -29
  120. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/flow.py +1 -1
  121. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/keys.py +1 -1
  122. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/parsed.py +1 -1
  123. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/syntax.py +36 -28
  124. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/trace.py +10 -7
  125. {python_constricter-0.4.3/constricter/cli → python_constricter-0.4.4/constricter}/traced.py +54 -8
  126. {python_constricter-0.4.3 → python_constricter-0.4.4}/pyproject.toml +20 -8
  127. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/forms.py +1 -1
  128. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/generate.py +8 -8
  129. {python_constricter-0.4.3 → python_constricter-0.4.4}/.gitignore +0 -0
  130. {python_constricter-0.4.3 → python_constricter-0.4.4}/LICENSE.md +0 -0
  131. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/__init__.py +0 -0
  132. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/__main__.py +0 -0
  133. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/__init__.py +0 -0
  134. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/baseline.py +0 -0
  135. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/collecting.py +0 -0
  136. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/lsp/__init__.py +0 -0
  137. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/cli/paths.py +0 -0
  138. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/empty.py +0 -0
  139. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/__init__.py +0 -0
  140. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/__init__.py +0 -0
  141. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/asked.py +0 -0
  142. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/core/signatures.py +2 -2
  143. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/__init__.py +0 -0
  144. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/index/stubs/__init__.py +0 -0
  145. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/__init__.py +0 -0
  146. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/generics.py +0 -0
  147. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/opened.py +0 -0
  148. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/libraries/walks.py +0 -0
  149. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/aliases.json +0 -0
  150. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/attributes.json +0 -0
  151. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/awaited.json +0 -0
  152. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/awaited_overloads.json +0 -0
  153. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/bases.json +0 -0
  154. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/classes.json +0 -0
  155. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/elements.json +0 -0
  156. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/functions.json +0 -0
  157. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/generic_attributes.json +0 -0
  158. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/method_overloads.json +0 -0
  159. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/method_signatures.json +0 -0
  160. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/methods.json +0 -0
  161. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/overloads.json +0 -0
  162. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/platform.json +0 -0
  163. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/returns.json +0 -0
  164. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/scalar_members.json +0 -0
  165. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/scalars.json +0 -0
  166. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/subscriptable.json +0 -0
  167. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/type_parameters.json +0 -0
  168. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/tables/variables.json +0 -0
  169. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/__init__.py +0 -0
  170. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/displays.py +0 -0
  171. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/fix/values/operated.py +0 -0
  172. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/jsonc.py +0 -0
  173. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/noqa.py +0 -0
  174. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/overrides.py +0 -0
  175. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/plugins/__init__.py +0 -0
  176. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/py.typed +0 -0
  177. {python_constricter-0.4.3 → python_constricter-0.4.4}/constricter/rules/__init__.py +0 -0
  178. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/quoted.py +0 -0
  179. {python_constricter-0.4.3/constricter/rules → python_constricter-0.4.4/constricter/source}/walked.py +0 -0
  180. {python_constricter-0.4.3 → python_constricter-0.4.4}/hatch_build.py +0 -0
  181. {python_constricter-0.4.3 → python_constricter-0.4.4}/mypyc.txt +0 -0
  182. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/__init__.py +0 -0
  183. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/__main__.py +0 -0
  184. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/accepting.py +0 -0
  185. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/configured.py +0 -0
  186. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/elements.py +0 -0
  187. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/overloads.py +0 -0
  188. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/partial.json +0 -0
  189. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/reading.py +0 -0
  190. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/shed.py +0 -0
  191. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/stubs.py +0 -0
  192. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/templates.py +0 -0
  193. {python_constricter-0.4.3 → python_constricter-0.4.4}/stdlib_tables/written.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-constricter
3
- Version: 0.4.3
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
@@ -107,6 +107,7 @@ class bodies too. Statements are read in source order, and only a name's first b
107
107
  | `LVA012` | opt-in: a local bound once, outside any loop, and never rebound | `name: Final = ...` |
108
108
  | `LVA013` | opt-in: an instance attribute a plain class's method first stores, declared nowhere in it | `self.x: T = ...` |
109
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):` |
110
111
 
111
112
  Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
112
113
  `global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
@@ -166,8 +167,17 @@ nested function, and names the class's body or a base of the module's binds are
166
167
 
167
168
  `LVA014` is opt-in too (pylint's `enable = missing-return-type`), an error from `strict`. It
168
169
  reports a plain function or method (not decorated) with no return annotation, as ruff's `ANN201`
169
- does; its fix is what the function's own `return`s give (`-> int`, a method's a guess), or `-> None`
170
- for one that returns nothing, where the type names only what the module already binds.
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.
171
181
 
172
182
  Python 3.11+, no runtime dependencies.
173
183
 
@@ -175,16 +185,17 @@ Python 3.11+, no runtime dependencies.
175
185
 
176
186
  | Tool | Setup | Reports | Suppress |
177
187
  | ------ | ----------------------------------------------------- | ------------------------------- | ------------------------------------------------ |
178
- | CLI | `constricter [PATH...] [--level L] [--format F] [-q]` | `LVA001`–`LVA014` | `# noqa: LVA001` |
179
- | flake8 | install it (on by default) | `LVA001`–`LVA014` | `# noqa: LVA001` |
180
- | pylint | `load-plugins = ["constricter.plugins.pylint"]` | `C9101`–`C9114` (symbols below) | `# noqa: LVA001` or `# pylint: disable=<symbol>` |
181
- | ruff | run the CLI after ruff; set `lint.external = ["LVA"]` | `LVA001`–`LVA014` | `# 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` |
182
192
 
183
193
  pylint symbols: `unannotated-local-variable`, `untyped-for-or-match-variable`,
184
194
  `comment-typed-for-variable`, `unannotated-module-or-class-variable`, `vague-annotation`,
185
195
  `deeply-nested-annotation`, `redundant-annotation`, `narrowable-annotation`,
186
196
  `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, and, off until enabled,
187
- `could-be-final`, `unannotated-instance-attribute` and `missing-return-type`.
197
+ `could-be-final`, `unannotated-instance-attribute`, `missing-return-type` and
198
+ `missing-parameter-type`.
188
199
 
189
200
  Options:
190
201
 
@@ -80,6 +80,7 @@ class bodies too. Statements are read in source order, and only a name's first b
80
80
  | `LVA012` | opt-in: a local bound once, outside any loop, and never rebound | `name: Final = ...` |
81
81
  | `LVA013` | opt-in: an instance attribute a plain class's method first stores, declared nowhere in it | `self.x: T = ...` |
82
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):` |
83
84
 
84
85
  Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
85
86
  `global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
@@ -139,8 +140,17 @@ nested function, and names the class's body or a base of the module's binds are
139
140
 
140
141
  `LVA014` is opt-in too (pylint's `enable = missing-return-type`), an error from `strict`. It
141
142
  reports a plain function or method (not decorated) with no return annotation, as ruff's `ANN201`
142
- does; its fix is what the function's own `return`s give (`-> int`, a method's a guess), or `-> None`
143
- for one that returns nothing, where the type names only what the module already binds.
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.
144
154
 
145
155
  Python 3.11+, no runtime dependencies.
146
156
 
@@ -148,16 +158,17 @@ Python 3.11+, no runtime dependencies.
148
158
 
149
159
  | Tool | Setup | Reports | Suppress |
150
160
  | ------ | ----------------------------------------------------- | ------------------------------- | ------------------------------------------------ |
151
- | CLI | `constricter [PATH...] [--level L] [--format F] [-q]` | `LVA001`–`LVA014` | `# noqa: LVA001` |
152
- | flake8 | install it (on by default) | `LVA001`–`LVA014` | `# noqa: LVA001` |
153
- | pylint | `load-plugins = ["constricter.plugins.pylint"]` | `C9101`–`C9114` (symbols below) | `# noqa: LVA001` or `# pylint: disable=<symbol>` |
154
- | ruff | run the CLI after ruff; set `lint.external = ["LVA"]` | `LVA001`–`LVA014` | `# 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` |
155
165
 
156
166
  pylint symbols: `unannotated-local-variable`, `untyped-for-or-match-variable`,
157
167
  `comment-typed-for-variable`, `unannotated-module-or-class-variable`, `vague-annotation`,
158
168
  `deeply-nested-annotation`, `redundant-annotation`, `narrowable-annotation`,
159
169
  `mismatched-value-type`, `unused-union-member`, `long-tuple-annotation`, and, off until enabled,
160
- `could-be-final`, `unannotated-instance-attribute` and `missing-return-type`.
170
+ `could-be-final`, `unannotated-instance-attribute`, `missing-return-type` and
171
+ `missing-parameter-type`.
161
172
 
162
173
  Options:
163
174
 
@@ -1,3 +1,3 @@
1
1
  """The version this was built as, from its git tag (written by the build, not tracked)."""
2
2
 
3
- __version__ = "0.4.3"
3
+ __version__ = "0.4.4"
@@ -17,7 +17,7 @@ from constricter.cli import baseline, collecting, schedule
17
17
  from constricter.cli.lsp.protocol import HintError
18
18
  from constricter.cli.options import Mode, Options, Output
19
19
  from constricter.cli.paths import STDIN, python_files
20
- from constricter.cli.reading import encoding_of, read_text
20
+ from constricter.cli.reading import READ_ERRORS, encoding_of, read_text
21
21
  from constricter.cli.report import Format, Result, fix_reasons, render, statistics
22
22
  from constricter.cli.rounds import checked_all, settled
23
23
  from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
@@ -31,8 +31,8 @@ from constricter.offences import (
31
31
  Edit,
32
32
  Offence,
33
33
  )
34
- from constricter.rules import parsed
35
34
  from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
35
+ from constricter.source import parsed
36
36
 
37
37
  if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
38
38
  from types import ModuleType
@@ -42,7 +42,6 @@ if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
42
42
  EXIT_CLEAN: Final = 0
43
43
  EXIT_FOUND: Final = 1
44
44
  EXIT_ERROR: Final = 2
45
- _ALL: Final = 100 # percent
46
45
 
47
46
 
48
47
  def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
@@ -199,11 +198,21 @@ def _read_checked(
199
198
  checked: Checked
200
199
  try:
201
200
  raw, checked = _checked(path, name, checks, outside)
202
- except (OSError, ValueError, SyntaxError) as error: # UnicodeDecodeError is a ValueError
203
- return "", Checked([], Returns()), f"{name}: error: {error}"
201
+ except READ_ERRORS as error:
202
+ return "", Checked([], Returns()), _error(name, error)
204
203
  return raw, checked, ""
205
204
 
206
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
+
207
216
  def _results(raw: str, name: Path, offences: Sequence[Offence], options: Options) -> list[Result]:
208
217
  """Turn the offences in `raw`, the text of `name`, into the results to report.
209
218
 
@@ -326,8 +335,8 @@ def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
326
335
  name: Path = options.input.name(path)
327
336
  try:
328
337
  return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
329
- except (OSError, ValueError, SyntaxError) as error:
330
- return CoverageRun(error=f"{name}: error: {error}")
338
+ except READ_ERRORS as error:
339
+ return CoverageRun(error=_error(name, error))
331
340
 
332
341
 
333
342
  def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
@@ -220,11 +220,13 @@ def _checkers(value: _Toml) -> list[str] | None:
220
220
  """Read `infer-with`: a checker, or a list of them, each one it knows.
221
221
 
222
222
  Returns:
223
- 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.
224
225
 
225
226
  """
226
227
  checkers: list[str] | None = [value] if isinstance(value, str) else _strings(value)
227
- 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
228
230
 
229
231
 
230
232
  def _levels(value: _Toml) -> dict[str, str] | None:
@@ -247,7 +249,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
247
249
  return None if None in lists.values() else {key: list(item or []) for key, item in lists.items()}
248
250
 
249
251
 
250
- def _baseline(value: _Toml, root: Path) -> str | None:
252
+ def _path(value: _Toml, root: Path) -> str | None:
251
253
  """Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
252
254
 
253
255
  Returns:
@@ -302,8 +304,8 @@ def config_defaults(start: Path) -> dict[str, Default]:
302
304
  return {}
303
305
  readers: dict[str, Callable[[_Toml], Default | None]] = {
304
306
  **_READERS,
305
- "baseline": partial(_baseline, root=path.parent),
306
- "infer-from": partial(_baseline, root=path.parent),
307
+ "baseline": partial(_path, root=path.parent),
308
+ "infer-from": partial(_path, root=path.parent),
307
309
  }
308
310
  defaults: dict[str, Default] = {}
309
311
  key: str
@@ -15,6 +15,7 @@ from constricter.offences import (
15
15
  UNANNOTATED,
16
16
  UNANNOTATED_ATTRIBUTE,
17
17
  UNANNOTATED_MEMBER,
18
+ UNANNOTATED_PARAMETER,
18
19
  UNANNOTATED_RETURN,
19
20
  UNTYPED_TARGET,
20
21
  UNUSED_UNION_MEMBER,
@@ -95,6 +96,13 @@ _WHY: Final = {
95
96
  "returns nothing. Off unless selected by its full code (`--extend-select LVA014`): ruff's ANN201\n"
96
97
  "reports it too; this is for the fix."
97
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
+ ),
98
106
  }
99
107
 
100
108
 
@@ -5,7 +5,6 @@ The files it has open, the hint requests sent and not yet answered, and what it
5
5
 
6
6
  import contextlib
7
7
  import itertools
8
- import json
9
8
  import os
10
9
  import queue
11
10
  import re
@@ -329,7 +328,7 @@ class Connection:
329
328
 
330
329
  """
331
330
  self.last += 1
332
- self._send({"jsonrpc": "2.0", _ID: self.last, _METHOD: method, "params": params})
331
+ self._send({_ID: self.last, _METHOD: method, "params": params})
333
332
  return self.last
334
333
 
335
334
  def _collect(self, numbers: set[int], method: str) -> dict[int, _Json]:
@@ -444,7 +443,7 @@ class Connection:
444
443
  def _answer(self, message: _Object) -> None:
445
444
  """Answer one of the server's own requests: its settings, and nothing else it asks for."""
446
445
  method: str = str(message[_METHOD])
447
- reply: _Object = {"jsonrpc": "2.0", _ID: message[_ID]}
446
+ reply: _Object = {_ID: message[_ID]}
448
447
  if method == _CONFIGURATION:
449
448
  items: list[_Json] = cast("list[_Json]", cast("_Object", message["params"])["items"])
450
449
  reply["result"] = [protocol.settings(cast("_Object", item).get("section")) for item in items]
@@ -456,22 +455,18 @@ class Connection:
456
455
 
457
456
  def notify(self, method: str, params: _Json) -> None:
458
457
  """Send a notification: no answer comes."""
459
- self._send({"jsonrpc": "2.0", "method": method, "params": params})
458
+ self._send({"method": method, "params": params})
460
459
 
461
460
  def _send(self, message: _Object) -> None:
462
- """Frame and send one message.
461
+ """Frame and send one message (see `protocol.write`).
463
462
 
464
463
  Raises:
465
464
  ExitedError: The server has exited.
466
465
 
467
466
  """
468
- body: bytes = json.dumps(message).encode()
469
467
  with self.writing: # one message at a time: the reader answers the server's own meanwhile
470
468
  try:
471
- protocol.write(
472
- cast("IO[bytes]", self.process.stdin),
473
- f"Content-Length: {len(body)}\r\n\r\n".encode() + body,
474
- )
469
+ protocol.write(cast("IO[bytes]", self.process.stdin), message)
475
470
  except (OSError, ValueError) as error: # it has gone, or its input was closed to stop it
476
471
  failure: str = f"{self.name} exited: {error}"
477
472
  raise ExitedError(failure) from error
@@ -19,7 +19,7 @@ from urllib.request import url2pathname
19
19
  from constricter.cli.lsp.protocol import Json, Object
20
20
  from constricter.fix.core.known import Offered
21
21
  from constricter.fix.index.modules import SUFFIX, module_name
22
- from constricter.rules import parsed
22
+ from constricter.source import parsed
23
23
 
24
24
  _Where: TypeAlias = tuple[int, int] # a line (from 1) and a UTF-8 byte column, as `ast` counts
25
25
  _Locate: TypeAlias = Callable[[Object], _Where | None] # a position's, or `None` past the file's end
@@ -1,6 +1,6 @@
1
1
  """Run a type checker's language server for `--infer-with`, so it can't outlive constricter.
2
2
 
3
- python .../constricter/cli/guard.py GRACE EXECUTABLE [ARGUMENT ...]
3
+ python .../constricter/cli/lsp/guard.py GRACE EXECUTABLE [ARGUMENT ...]
4
4
 
5
5
  It imports only the standard library, so it runs by its path whatever the Python can import.
6
6
 
@@ -65,20 +65,23 @@ def main(argv: Sequence[str], stdin: IO[bytes] | None = None, stdout: IO[bytes]
65
65
 
66
66
  def _pass_on(source: IO[bytes], target: IO[bytes]) -> None:
67
67
  """Pass the server's output on until it ends (or constricter's end of it has gone)."""
68
- chunk: bytes
69
68
  with contextlib.suppress(OSError):
70
- for chunk in iter(lambda: _read(source), b""):
71
- _write(target, chunk)
69
+ _copied(source, target)
70
+
71
+
72
+ def _copied(source: IO[bytes], target: IO[bytes]) -> None:
73
+ """Copy `source` to `target` until it ends, each chunk as it comes: the reader waits on it."""
74
+ chunk: bytes
75
+ for chunk in iter(lambda: _read(source), b""):
76
+ _write(target, chunk)
72
77
 
73
78
 
74
79
  def relay_to(source: IO[bytes], server: "subprocess.Popen[bytes]", grace: float) -> None:
75
80
  """Pass `source` on to the server's input; when it ends, end the server's, and give it `grace` seconds."""
76
81
  target: IO[bytes] = cast("IO[bytes]", server.stdin)
77
- chunk: bytes
78
82
  # The server has gone (or this is exiting, its input closed): nothing more to pass on.
79
83
  with contextlib.suppress(OSError, ValueError):
80
- for chunk in iter(lambda: _read(source), b""):
81
- _write(target, chunk)
84
+ _copied(source, target)
82
85
  with contextlib.suppress(OSError): # what's left for a server that has gone is dropped
83
86
  target.close()
84
87
  try:
@@ -94,11 +94,7 @@ class Session:
94
94
 
95
95
  def __exit__(self, *_details: object) -> None:
96
96
  """Shut every server down, at once."""
97
- running: list[Connection]
98
- if running := [server for checker in self.checkers for server in checker.servers]:
99
- pool: ThreadPoolExecutor
100
- with ThreadPoolExecutor(len(running)) as pool:
101
- _ = list(pool.map(Connection.close, running))
97
+ _closed([server for checker in self.checkers for server in checker.servers])
102
98
 
103
99
  def hints(self, files: Mapping[Path, str]) -> dict[Path, tuple[Hints, ...]]:
104
100
  """Ask every checker for the variable-type hints in `files` (each path's current text).
@@ -306,7 +302,7 @@ class Checker: # pylint: disable=too-many-instance-attributes
306
302
  batches: queue.SimpleQueue[dict[Path, str]] = queue.SimpleQueue()
307
303
  start: int
308
304
  for start in range(0, len(fresh), _BATCH):
309
- batch: list[Path] = fresh[start:][:_BATCH]
305
+ batch: list[Path] = fresh[start : start + _BATCH]
310
306
  batches.put({path: files[path] for path in batch})
311
307
  return [partial(self._work, index, own.get(index, {}), batches) for index in range(len(self.servers))]
312
308
 
@@ -337,11 +333,7 @@ class Checker: # pylint: disable=too-many-instance-attributes
337
333
  except HintError as failure: # it has gone since its last answers: past its cap, say
338
334
  if not self._restartable(failure):
339
335
  raise
340
- unanswered: dict[Path, str] = {
341
- path: text for sent, _ in pending for path, text in sent.items()
342
- }
343
- pending.clear()
344
- found.update(self._alone(index, unanswered | batch))
336
+ found.update(self._requeued(index, pending, batch))
345
337
  continue
346
338
  if len(pending) > 1:
347
339
  found.update(self._answers(index, pending))
@@ -367,12 +359,21 @@ class Checker: # pylint: disable=too-many-instance-attributes
367
359
  except HintError as failure:
368
360
  if not self._restartable(failure):
369
361
  raise
370
- unanswered: dict[Path, str] = {path: text for batch, _ in pending for path, text in batch.items()}
371
- pending.clear()
372
- return self._alone(index, unanswered)
362
+ return self._requeued(index, pending, {})
373
363
  del pending[0]
374
364
  return found
375
365
 
366
+ def _requeued(self, index: int, pending: list[Sent], more: Mapping[Path, str]) -> Found:
367
+ """Ask server `index`, which failed, about each pending file and each of `more` alone (see `_alone`).
368
+
369
+ Returns:
370
+ Their hints.
371
+
372
+ """
373
+ unanswered: dict[Path, str] = {path: text for batch, _ in pending for path, text in batch.items()}
374
+ pending.clear()
375
+ return self._alone(index, unanswered | dict(more))
376
+
376
377
  def _alone(self, index: int, files: Mapping[Path, str]) -> Found:
377
378
  """Restart server `index`, which hung or exited past its cap, and ask it about each of `files` alone.
378
379
 
@@ -410,10 +411,7 @@ class Checker: # pylint: disable=too-many-instance-attributes
410
411
 
411
412
  def stop(self) -> None:
412
413
  """Shut its servers down, at once: the next round starts others, which open each file anew."""
413
- pool: ThreadPoolExecutor
414
- # None where its turn had no file to ask about (a round with none, or every one abandoned).
415
- with ThreadPoolExecutor(max(1, len(self.servers))) as pool:
416
- _ = list(pool.map(Connection.close, self.servers))
414
+ _closed(self.servers)
417
415
  self.servers.clear()
418
416
  self.assigned.clear()
419
417
 
@@ -496,3 +494,10 @@ def command(checker: str) -> list[str]:
496
494
  )
497
495
  raise HintError(message)
498
496
  return [found, *server.args]
497
+
498
+
499
+ def _closed(servers: Sequence[Connection]) -> None:
500
+ """Shut `servers` down, at once: none where a turn had no file to ask about."""
501
+ pool: ThreadPoolExecutor
502
+ with ThreadPoolExecutor(max(1, len(servers))) as pool:
503
+ _ = list(pool.map(Connection.close, servers))
@@ -15,6 +15,7 @@ Json: TypeAlias = dict[str, "Json"] | list["Json"] | str | int | float | bool |
15
15
  Object: TypeAlias = dict[str, Json]
16
16
  _HEADER_END: Final = b"\r\n"
17
17
  _LENGTH: Final = "content-length"
18
+ _JSONRPC: Final = "2.0" # the version every message names, first
18
19
  _TYPE_HINT: Final = 1 # `InlayHintKind.Type`
19
20
  # Only variable types: argument names and return types are hints too, but nothing here.
20
21
  _BASEDPYRIGHT_SECTION: Final = "basedpyright.analysis"
@@ -91,14 +92,7 @@ def executable(checker: str) -> str | None:
91
92
  Its path, or `None` if it isn't installed.
92
93
 
93
94
  """
94
- name: str = SERVERS[checker].executable
95
- found: list[str] = list(
96
- dict.fromkeys(
97
- path
98
- for path in (shutil.which(name), shutil.which(name, path=str(Path(sys.executable).parent)))
99
- if path is not None
100
- ),
101
- )
95
+ found: list[str] = _found(checker)
102
96
  return next((path for path in found if _runs(path)), found[0] if found else None)
103
97
 
104
98
 
@@ -106,11 +100,27 @@ def runs(checker: str) -> bool:
106
100
  """Check that `checker` is installed, and runs.
107
101
 
108
102
  Returns:
109
- Whether it does.
103
+ Whether it does: any of its servers found runs.
110
104
 
111
105
  """
112
- found: str | None = executable(checker)
113
- return found is not None and _runs(found)
106
+ return any(_runs(path) for path in _found(checker))
107
+
108
+
109
+ def _found(checker: str) -> list[str]:
110
+ """Find `checker`'s language servers: on `PATH`, then beside this Python.
111
+
112
+ Returns:
113
+ Their paths, each once.
114
+
115
+ """
116
+ name: str = SERVERS[checker].executable
117
+ return list(
118
+ dict.fromkeys(
119
+ path
120
+ for path in (shutil.which(name), shutil.which(name, path=str(Path(sys.executable).parent)))
121
+ if path is not None
122
+ ),
123
+ )
114
124
 
115
125
 
116
126
  def _runs(path: str) -> bool:
@@ -144,9 +154,10 @@ class ExitedError(HintError):
144
154
  """The type checker's server exited while it was being asked: past its heap's cap, say."""
145
155
 
146
156
 
147
- def write(stream: IO[bytes], data: bytes) -> None:
148
- """Write `data` to `stream`, and flush it: the server reads it now."""
149
- _ = stream.write(data)
157
+ def write(stream: IO[bytes], sent: Object) -> None:
158
+ """Frame `sent` (see `message`), write it to `stream`, and flush it: the server reads it now."""
159
+ body: bytes = json.dumps({"jsonrpc": _JSONRPC, **sent}).encode()
160
+ _ = stream.write(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)
150
161
  stream.flush()
151
162
 
152
163
 
@@ -29,9 +29,7 @@ def _marker(checker: str, command: Sequence[str], named: str) -> Path:
29
29
  Its path in the cache.
30
30
 
31
31
  """
32
- made: str = ""
33
- with contextlib.suppress(OSError):
34
- made = build(checker, Path(command[0]))
32
+ made: str = build(checker, Path(command[0]))
35
33
  key: str = "\0".join((checker, command[0], made, named))
36
34
  return cache_directory().with_name("hung") / hashlib.sha256(key.encode("utf-8", "replace")).hexdigest()
37
35
 
@@ -43,15 +41,17 @@ def build(checker: str, executable: Path) -> str:
43
41
 
44
42
  Returns:
45
43
  The distribution's folder's name, which holds its version; else the executable's size and when
46
- it changed.
44
+ it changed; empty where neither can be read.
47
45
 
48
46
  """
49
47
  pattern: str
50
- for pattern in _DISTRIBUTIONS:
51
- found: list[Path]
52
- if found := sorted(executable.parent.parent.glob(f"{pattern}/{checker}-*.dist-info")):
53
- return found[0].name
54
- return f"{executable.stat().st_size}:{executable.stat().st_mtime_ns}"
48
+ with contextlib.suppress(OSError):
49
+ for pattern in _DISTRIBUTIONS:
50
+ found: list[Path]
51
+ if found := sorted(executable.parent.parent.glob(f"{pattern}/{checker}-*.dist-info")):
52
+ return found[0].name
53
+ return f"{executable.stat().st_size}:{executable.stat().st_mtime_ns}"
54
+ return ""
55
55
 
56
56
 
57
57
  def hung(checker: str, command: Sequence[str], text: str, path: Path | None = None) -> bool:
@@ -24,7 +24,7 @@ from collections.abc import Iterable, Mapping, Sequence
24
24
  from pathlib import Path
25
25
  from typing import Final, TypeAlias, TypedDict, cast
26
26
 
27
- from constricter.cli.lsp import connection, protocol, remembered
27
+ from constricter.cli.lsp import connection, edits, protocol, remembered
28
28
  from constricter.cli.lsp.connection import FileHints, Found
29
29
  from constricter.fix.core.known import Offered
30
30
  from constricter.fix.index.stubs.installed import cache_directory, search_path
@@ -56,11 +56,12 @@ def key(checker: str, command: Sequence[str], root: Path, files: Mapping[Path, s
56
56
  A digest of everything they rest on (see the module docstring).
57
57
 
58
58
  """
59
- build: str = ""
60
- with contextlib.suppress(OSError):
61
- build = remembered.build(checker, Path(command[0]))
59
+ build: str = remembered.build(checker, Path(command[0]))
62
60
  texts: list[str] = [checker, *command, build, *(os.environ.get(name, "") for name in _VARIABLES)]
63
- parts: list[bytes] = [Path(cast("str", code.__file__)).read_bytes() for code in (connection, protocol)]
61
+ # The code that asks and reads them, `edits` the one that writes what the store keeps.
62
+ parts: list[bytes] = [
63
+ Path(cast("str", code.__file__)).read_bytes() for code in (connection, protocol, edits)
64
+ ]
64
65
  asked: dict[str, str] = {_relative(path, root): text for path, text in files.items()}
65
66
  name: str
66
67
  for name in sorted(asked):
@@ -7,10 +7,10 @@ from collections.abc import Callable, Sequence
7
7
  from dataclasses import dataclass, field
8
8
  from enum import Enum
9
9
  from pathlib import Path
10
- from typing import Final, cast
10
+ from typing import Final, Self, cast
11
11
 
12
- from constricter import __version__
13
- from constricter.cli import baseline, traced
12
+ from constricter import __version__, traced
13
+ from constricter.cli import baseline
14
14
  from constricter.cli.config import (
15
15
  DEFAULT_BASELINE,
16
16
  config_defaults,
@@ -33,6 +33,7 @@ from constricter.offences import (
33
33
  OPT_IN,
34
34
  PLAIN_BASES,
35
35
  UNANNOTATED_ATTRIBUTE,
36
+ UNANNOTATED_PARAMETER,
36
37
  UNANNOTATED_RETURN,
37
38
  VAGUE,
38
39
  Checks,
@@ -637,7 +638,7 @@ class Options: # pylint: disable=too-many-instance-attributes
637
638
  infer_in_turn: bool = False # ask one checker at a time, not all at once (`--infer-in-turn`)
638
639
 
639
640
  @classmethod
640
- def parse(cls, argv: Sequence[str] | None) -> "Options":
641
+ def parse(cls, argv: Sequence[str] | None) -> Self:
641
642
  """Parse `argv` over the defaults `pyproject.toml` sets; `--explain` prints and exits.
642
643
 
643
644
  Returns:
@@ -679,6 +680,7 @@ class Options: # pylint: disable=too-many-instance-attributes
679
680
  final=CAN_BE_FINAL in {*_select(args), *cast("list[str]", args.extend_select)},
680
681
  attributes=UNANNOTATED_ATTRIBUTE in {*_select(args), *cast("list[str]", args.extend_select)},
681
682
  returns=UNANNOTATED_RETURN in {*_select(args), *cast("list[str]", args.extend_select)},
683
+ parameters=UNANNOTATED_PARAMETER in {*_select(args), *cast("list[str]", args.extend_select)},
682
684
  fixes=FixPolicy(
683
685
  frozenset(cast("list[str]", args.fix_select)),
684
686
  frozenset(cast("list[str]", args.fix_ignore)),
@@ -744,7 +746,10 @@ def _maximal(args: argparse.Namespace) -> argparse.Namespace:
744
746
  args.per_path_levels = {}
745
747
  args.all_scopes = True
746
748
  # LVA014 only selected by itself: it's ruff's ANN201 again, for its fix.
747
- args.extend_select = [*cast("list[str]", args.extend_select), *sorted(OPT_IN - {UNANNOTATED_RETURN})]
749
+ args.extend_select = [
750
+ *cast("list[str]", args.extend_select),
751
+ *sorted(OPT_IN - {UNANNOTATED_RETURN, UNANNOTATED_PARAMETER}),
752
+ ]
748
753
  return args
749
754
 
750
755
 
@@ -4,10 +4,15 @@ import io
4
4
  import sys
5
5
  import tokenize
6
6
  from pathlib import Path
7
+ from typing import Final
7
8
 
8
9
  from constricter import notebook
9
10
  from constricter.cli.paths import STDIN
10
11
 
12
+ # What reading or parsing a file raises: UnicodeDecodeError is a ValueError; a bad encoding
13
+ # declaration, a SyntaxError.
14
+ READ_ERRORS: Final = (OSError, ValueError, SyntaxError)
15
+
11
16
 
12
17
  def encoding_of(path: Path, data: bytes) -> str:
13
18
  """Find the encoding of `path`, whose bytes are `data`: a notebook's is UTF-8 (it's JSON).