python-constricter 0.4.1__cp312-cp312-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. constricter/__init__.py +55 -0
  2. constricter/__main__.py +26 -0
  3. constricter/_version.py +4 -0
  4. constricter/cli/__init__.py +2 -0
  5. constricter/cli/baseline.py +107 -0
  6. constricter/cli/collecting.py +54 -0
  7. constricter/cli/command.py +547 -0
  8. constricter/cli/config.py +322 -0
  9. constricter/cli/explain.py +104 -0
  10. constricter/cli/lsp/__init__.py +2 -0
  11. constricter/cli/lsp/connection.py +475 -0
  12. constricter/cli/lsp/edits.py +188 -0
  13. constricter/cli/lsp/guard.py +133 -0
  14. constricter/cli/lsp/hints.py +391 -0
  15. constricter/cli/lsp/protocol.py +192 -0
  16. constricter/cli/lsp/remembered.py +81 -0
  17. constricter/cli/lsp/stored.py +200 -0
  18. constricter/cli/options.py +799 -0
  19. constricter/cli/paths.py +151 -0
  20. constricter/cli/reading.py +36 -0
  21. constricter/cli/report.py +397 -0
  22. constricter/cli/rounds.py +328 -0
  23. constricter/cli/runs.py +90 -0
  24. constricter/cli/schedule.py +463 -0
  25. constricter/cli/traced.py +401 -0
  26. constricter/cli/workers.py +286 -0
  27. constricter/empty.py +66 -0
  28. constricter/fix/__init__.cp312-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp312-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp312-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp312-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp312-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp312-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp312-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp312-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp312-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp312-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp312-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp312-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp312-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp312-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp312-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp312-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp312-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp312-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp312-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp312-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp312-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp312-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp312-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp312-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp312-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp312-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp312-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp312-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp312-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp312-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp312-win32.pyd +0 -0
  125. constricter/fix/libraries/walks.py +49 -0
  126. constricter/fix/tables/aliases.json +147 -0
  127. constricter/fix/tables/attributes.json +700 -0
  128. constricter/fix/tables/awaited.json +69 -0
  129. constricter/fix/tables/awaited_overloads.json +14 -0
  130. constricter/fix/tables/bases.json +888 -0
  131. constricter/fix/tables/classes.json +1941 -0
  132. constricter/fix/tables/elements.json +156 -0
  133. constricter/fix/tables/functions.json +58 -0
  134. constricter/fix/tables/generic_attributes.json +254 -0
  135. constricter/fix/tables/method_overloads.json +461 -0
  136. constricter/fix/tables/method_signatures.json +954 -0
  137. constricter/fix/tables/methods.json +954 -0
  138. constricter/fix/tables/overloads.json +540 -0
  139. constricter/fix/tables/platform.json +37 -0
  140. constricter/fix/tables/returns.json +1362 -0
  141. constricter/fix/tables/scalar_members.json +16 -0
  142. constricter/fix/tables/scalars.json +2707 -0
  143. constricter/fix/tables/source.json +28 -0
  144. constricter/fix/tables/subscriptable.json +77 -0
  145. constricter/fix/tables/type_parameters.json +178 -0
  146. constricter/fix/tables/variables.json +2962 -0
  147. constricter/fix/values/__init__.cp312-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp312-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp312-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp312-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp312-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp312-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp312-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp312-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp312-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp312-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp312-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp312-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp312-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp312-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp312-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp312-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp312-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp312-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp312-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp312-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp312-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp312-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp312-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp312-win32.pyd +0 -0
  196. constricter/fix/values/unchecked.py +278 -0
  197. constricter/jsonc.py +50 -0
  198. constricter/noqa.py +47 -0
  199. constricter/notebook.py +115 -0
  200. constricter/offences.py +436 -0
  201. constricter/overrides.py +24 -0
  202. constricter/plugins/__init__.py +2 -0
  203. constricter/plugins/flake8.py +133 -0
  204. constricter/plugins/pylint.py +186 -0
  205. constricter/py.typed +0 -0
  206. constricter/recording.py +660 -0
  207. constricter/rules/__init__.cp312-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp312-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp312-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp312-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp312-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp312-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp312-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp312-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp312-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp312-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp312-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp312-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp312-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp312-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp312-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp312-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp312-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp312-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp312-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp312-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp312-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp312-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp312-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp312-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp312-win32.pyd +0 -0
  257. python_constricter-0.4.1.dist-info/METADATA +400 -0
  258. python_constricter-0.4.1.dist-info/RECORD +261 -0
  259. python_constricter-0.4.1.dist-info/WHEEL +4 -0
  260. python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
  261. python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,660 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """What `python -m constricter.trace` records: the types a run binds its functions' locals to.
3
+
4
+ A `Recorder` is told each line a function of a file under its root arrives at, and notes the type
5
+ of each name the statement it left binds: an assignment's, a loop's or a `with`'s targets, a
6
+ `:=`'s. A statement that ends 20 times with nothing new is no longer looked at, nor a function that
7
+ returns 20 times so; a statement an exception ends says nothing. What it found is a trace: each
8
+ file by its SHA-256, each binding by its line and name, so a file edited since has none, and a copy
9
+ of it has them all.
10
+
11
+ A value is spelled as its annotation: `None`, a builtin scalar, a builtin container or one of
12
+ `collections`' by its first elements' types (`list[int]`, `dict[str, Row | None]`), a tuple by its
13
+ parts' one type (`tuple[int, ...]`) or part by part, a class as `type[C]`, anything else by its
14
+ class, written `module:Class` (`module:Outer.Inner`, for one defined in another). A generic class's
15
+ instance has its arguments where it was made with them (`Box[int]()`), or where the class is a
16
+ builtin container of its own type variables (`class Stack(list[T])`), by its elements. What can't
17
+ be spelled is `?`, which leaves its name untyped: a container of more than three types or nested
18
+ past two levels, a mock, a class defined in a function. An empty container says nothing. None of
19
+ a value's own code runs to spell it: what it holds is read as it's stored, not asked for.
20
+ """
21
+
22
+ import ast
23
+ import builtins
24
+ import collections
25
+ import hashlib
26
+ import inspect
27
+ import itertools
28
+ import json
29
+ import sys
30
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping
31
+ from dataclasses import dataclass, field
32
+ from pathlib import Path
33
+ from types import CodeType, FrameType, ModuleType
34
+ from typing import Final, TypeAlias, TypedDict, TypeVar, cast, get_args, get_origin
35
+
36
+ VERSION: Final = 2 # the file's format
37
+ UNKNOWN: Final = "?" # a value that can't be spelled
38
+ NONE: Final = "None"
39
+ SEPARATOR: Final = ":" # between a class's module and its name
40
+ _DOT: Final = "." # between a class's name and that of one defined in it
41
+ _SEQUENCES: Final = frozenset({list, set, frozenset, collections.deque, collections.Counter})
42
+ _TUPLES: Final = frozenset({tuple})
43
+ _MAPPINGS: Final = frozenset({dict, collections.defaultdict, collections.OrderedDict})
44
+ # The classes a path is on one platform, as the one written for any.
45
+ _PATHS: Final = {
46
+ "PosixPath": "Path",
47
+ "WindowsPath": "Path",
48
+ "PurePosixPath": "PurePath",
49
+ "PureWindowsPath": "PurePath",
50
+ }
51
+ _PATHLIB: Final = "pathlib"
52
+ _BUILTINS: Final = "builtins"
53
+ _MOCK: Final = "unittest.mock"
54
+ _SAMPLE: Final = 20 # how many of a container's elements are looked at
55
+ _DEPTH: Final = 2 # how deep a container's elements are spelled
56
+ _MEMBERS: Final = 3 # how many types a container's elements may have
57
+ _LENGTH: Final = 4 # the longest tuple spelled part by part
58
+ _IDLE: Final = 20 # how many times with nothing new end a statement's recording, or a function's
59
+ _QUIET: Final = 50_000 # how many lines with nothing new end a function's: one that loops, and never returns
60
+ _INSTALLED: Final = "site-packages"
61
+ _MODULE: Final = "<module>" # the name of a module body's code
62
+ # A function that returns to its caller more than once, each a `return` to `sys.settrace`: `inspect`'s
63
+ # `CO_GENERATOR`, `CO_COROUTINE`, `CO_ITERABLE_COROUTINE` and `CO_ASYNC_GENERATOR`.
64
+ _SUSPENDS: Final = 0x20 | 0x80 | 0x100 | 0x200
65
+ _Def: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
66
+ _T = TypeVar("_T")
67
+ SCOPES: Final = ( # what has names of its own
68
+ ast.FunctionDef,
69
+ ast.AsyncFunctionDef,
70
+ ast.ClassDef,
71
+ ast.Lambda,
72
+ ast.ListComp,
73
+ ast.SetComp,
74
+ ast.DictComp,
75
+ ast.GeneratorExp,
76
+ )
77
+ _BLOCKS: Final = (ast.stmt, ast.excepthandler, ast.match_case) # what holds statements
78
+ _Held: TypeAlias = dict[int, dict[str, set[str]]] # each binding's spellings, by its line and name
79
+ Bindings: TypeAlias = dict[str, dict[str, list[str]]] # a file's, as they're written
80
+ _Form: TypeAlias = object # what a generic class is declared with: an alias (`list[T]`), a type variable
81
+ _Member: TypeAlias = object # what a module's name is, or a class's
82
+
83
+
84
+ class Entry(TypedDict):
85
+ """A trace's file: its path under the root, and each binding's spellings by its line, then its name."""
86
+
87
+ path: str
88
+ bindings: Bindings
89
+
90
+
91
+ class Found(TypedDict):
92
+ """A trace, as it's written.
93
+
94
+ The format's version; each file by its SHA-256; and each recorded module's file's SHA-256, by
95
+ the name the run knew it by (`__main__`, for what it ran).
96
+ """
97
+
98
+ version: int
99
+ files: dict[str, Entry]
100
+ modules: dict[str, str]
101
+
102
+
103
+ def spelled(value: object, depth: int = _DEPTH) -> str | None:
104
+ """Spell `value`'s type as an annotation (see the module's docstring).
105
+
106
+ Returns:
107
+ It; `UNKNOWN` for one that can't be spelled; `None` for one that says nothing (an empty
108
+ container, or one of them).
109
+
110
+ """
111
+ if value is None:
112
+ return NONE
113
+ if type(value) in _TUPLES:
114
+ return _tuple(cast("tuple[object, ...]", value), depth)
115
+ if type(value) in _SEQUENCES | _MAPPINGS:
116
+ return _container(named(type(value)), _parts(value, type(value)), depth)
117
+ if issubclass(type(value), type): # not `isinstance`, which asks the value its `__class__`
118
+ return UNKNOWN if named(cast("type", value)) == UNKNOWN else f"type[{named(cast('type', value))}]"
119
+ return _instance(value, type(value), depth)
120
+
121
+
122
+ def _held(holder: object, name: str) -> _Member:
123
+ """Read what a module or a class holds itself under `name`, as it's stored.
124
+
125
+ Not by `getattr`, which a module's `__getattr__` answers with code of the run's own (a lazy
126
+ import), and a class's bases too.
127
+
128
+ Returns:
129
+ It; `None` for a name it doesn't hold, or a `holder` that's neither.
130
+
131
+ """
132
+ if issubclass(type(holder), (ModuleType, type)):
133
+ return cast("Mapping[str, object]", vars(holder)).get(name)
134
+ return None
135
+
136
+
137
+ def _parts(value: object, kind: type) -> tuple[Iterable[object], ...]:
138
+ """List the values each type argument of a container of class `kind` is spelled by.
139
+
140
+ Returns:
141
+ A sequence's elements; a mapping's keys, and its values.
142
+
143
+ """
144
+ if kind in _SEQUENCES:
145
+ return (cast("Iterable[object]", value),)
146
+ return (cast("Iterable[object]", value), cast("Mapping[object, object]", value).values())
147
+
148
+
149
+ def _instance(value: object, cls: type, depth: int) -> str | None:
150
+ """Spell an instance by its class `cls`, a generic one with its arguments where they can be told.
151
+
152
+ Returns:
153
+ It (see `spelled`); a generic class bare where they can't.
154
+
155
+ """
156
+ text: str = named(cls)
157
+ # What `Box[int]()` leaves on what it makes: on an instance of a generic class alone.
158
+ made: _Form = (
159
+ cast("_Form", inspect.getattr_static(value, "__orig_class__", None)) if _generic(cls) else None
160
+ )
161
+ arguments: list[str] = [
162
+ named(argument) if isinstance(argument, type) else UNKNOWN
163
+ for argument in cast("tuple[_Form, ...]", get_args(made))
164
+ ]
165
+ if get_origin(made) is cls and UNKNOWN not in {text, *arguments}:
166
+ return f"{text}[{', '.join(arguments)}]"
167
+ declared: tuple[_Form, ...] = cast("tuple[_Form, ...]", _held(cls, "__parameters__") or ())
168
+ base: _Form
169
+ for base in cast("tuple[_Form, ...]", _held(cls, "__orig_bases__") or ()):
170
+ kind: _Form = cast("_Form", get_origin(base))
171
+ own: tuple[_Form, ...] = get_args(base)
172
+ if (
173
+ text != UNKNOWN
174
+ and isinstance(kind, type)
175
+ and kind in _SEQUENCES | _MAPPINGS
176
+ and all(isinstance(each, TypeVar) for each in own)
177
+ and declared in {(), own}
178
+ ):
179
+ return _container(text, _parts(value, kind), depth)
180
+ return text
181
+
182
+
183
+ def by_class(read: Callable[[type], _T]) -> Callable[[type], _T]:
184
+ """Keep what `read` says of each class: asked of every value a traced statement binds.
185
+
186
+ Not `UNKNOWN`, which a class may be only until its module binds it; nor of a class that can't
187
+ be hashed.
188
+
189
+ Returns:
190
+ `read`, answering from what it kept.
191
+
192
+ """
193
+ kept: dict[Hashable, _T] = {}
194
+
195
+ def remembered(cls: type) -> _T:
196
+ found: _T
197
+ try:
198
+ return kept[cls]
199
+ except KeyError:
200
+ if (found := read(cls)) != UNKNOWN:
201
+ kept[cls] = found
202
+ return found
203
+ except TypeError:
204
+ return read(cls)
205
+
206
+ return remembered
207
+
208
+
209
+ @by_class
210
+ def _generic(cls: type) -> bool:
211
+ """Check whether a class is generic: one of its line of classes takes type arguments.
212
+
213
+ Returns:
214
+ Whether it is.
215
+
216
+ """
217
+ return any(_held(base, "__parameters__") is not None for base in cls.__mro__)
218
+
219
+
220
+ @by_class
221
+ def named(cls: type) -> str:
222
+ """Name a class as an annotation would: a builtin bare, another as `module:Class`.
223
+
224
+ Returns:
225
+ It; `UNKNOWN` for one no annotation can name (a function's, a nested one, a mock).
226
+
227
+ """
228
+ module: str = cls.__module__
229
+ name: str = cls.__qualname__
230
+ if module == _BUILTINS:
231
+ return name if _held(builtins, name) is cls else UNKNOWN
232
+ if module.startswith(_MOCK) or not all(part.isidentifier() for part in name.split(_DOT)):
233
+ return UNKNOWN
234
+ if _DOT in name and _reached(module, name) is not cls: # a class in another, by another name
235
+ return UNKNOWN
236
+ if _held(collections, name) is cls: # wherever it's defined
237
+ return f"{collections.__name__}{SEPARATOR}{name}"
238
+ if module.partition(".")[0] == _PATHLIB:
239
+ return f"{_PATHLIB}{SEPARATOR}{_PATHS.get(name, name)}"
240
+ # A private module's class, as the public one of its name has it (`_io`'s, in `io`).
241
+ public: str = module.lstrip("_")
242
+ return f"{public if _reached(public, name) is cls else module}{SEPARATOR}{name}"
243
+
244
+
245
+ def _reached(module: str, name: str) -> _Member:
246
+ """Find what an imported module's `name` is: `Outer.Inner`, an attribute of its `Outer`.
247
+
248
+ Returns:
249
+ It, or `None`.
250
+
251
+ """
252
+ found: _Member = sys.modules.get(module)
253
+ part: str
254
+ for part in name.split(_DOT):
255
+ found = _held(found, part)
256
+ return found
257
+
258
+
259
+ def _joined(values: Iterable[object], depth: int) -> str | None:
260
+ """Spell the types of the first of `values` as one type, a union of up to `_MEMBERS`.
261
+
262
+ Returns:
263
+ It; `UNKNOWN` for more types than that, or any that is; `None` for none, or any saying nothing.
264
+
265
+ """
266
+ found: set[str | None] = {spelled(value, depth) for value in itertools.islice(values, _SAMPLE)}
267
+ if not found or None in found:
268
+ return None
269
+ types: list[str] = sorted(cast("set[str]", found), key=lambda text: (text == NONE, text))
270
+ return UNKNOWN if UNKNOWN in types or len(types) > _MEMBERS else " | ".join(types)
271
+
272
+
273
+ def _container(kind: str, parts: tuple[Iterable[object], ...], depth: int) -> str | None:
274
+ """Spell a container of the class spelled `kind` by each of its type arguments' values (`parts`).
275
+
276
+ Returns:
277
+ It (see `spelled`).
278
+
279
+ """
280
+ if not depth:
281
+ return UNKNOWN
282
+ arguments: list[str | None] = [_joined(part, depth - 1) for part in parts]
283
+ if None in arguments:
284
+ return None
285
+ if UNKNOWN in arguments:
286
+ return UNKNOWN
287
+ return f"{kind}[{', '.join(cast('list[str]', arguments))}]"
288
+
289
+
290
+ def _tuple(value: tuple[object, ...], depth: int) -> str | None:
291
+ """Spell a tuple: by its parts' one type, of any length, or part by part up to `_LENGTH` long.
292
+
293
+ Returns:
294
+ It (see `spelled`); nothing for an empty one, which any tuple's type takes.
295
+
296
+ """
297
+ if not value or not depth:
298
+ return UNKNOWN if value else None
299
+ parts: list[str | None] = [spelled(part, depth - 1) for part in value[:_SAMPLE]]
300
+ if None in parts:
301
+ return None
302
+ if UNKNOWN in parts or (len(set(parts)) > 1 and len(value) > _LENGTH):
303
+ return UNKNOWN
304
+ spellings: list[str] = cast("list[str]", parts)
305
+ return f"tuple[{spellings[0]}, ...]" if len(set(parts)) == 1 else f"tuple[{', '.join(spellings)}]"
306
+
307
+
308
+ @dataclass(eq=False)
309
+ class _Statement:
310
+ """The names a statement binds, each with its line; and how many times in a row it added nothing."""
311
+
312
+ stores: list[tuple[int, str]] = field(default_factory=list[tuple[int, str]])
313
+ idle: int = 0
314
+
315
+
316
+ @dataclass
317
+ class _File:
318
+ """A file under the root: its path there, its SHA-256, its functions' plans and what was noted."""
319
+
320
+ path: Path
321
+ digest: str
322
+ plans: dict[int, dict[int, _Statement]] # see `_plans`
323
+ held: _Held = field(default_factory=dict[int, dict[str, set[str]]])
324
+
325
+
326
+ @dataclass
327
+ class _Function:
328
+ """A function recorded: the statement each line is of, and how many returns in a row added nothing."""
329
+
330
+ file: _File
331
+ lines: dict[int, _Statement]
332
+ idle: int = 0
333
+ fresh: bool = False # whether anything was added since it last returned
334
+ quiet: int = 0 # how many lines it arrived at since anything was added
335
+
336
+ @property
337
+ def spent(self) -> bool:
338
+ """Whether it's recorded no more: `_IDLE` returns, or `_QUIET` lines, added nothing."""
339
+ return self.idle >= _IDLE or self.quiet >= _QUIET
340
+
341
+
342
+ Function: TypeAlias = _Function # as what tells a recorder's `ended` more names it
343
+
344
+
345
+ def _plans(tree: ast.Module) -> dict[int, dict[int, _Statement]]:
346
+ """Plan each function of a module: the statement each of its lines is of, where it binds a name.
347
+
348
+ Returns:
349
+ Each function's lines' statements, by its first line (its first decorator's).
350
+
351
+ """
352
+ return {
353
+ min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]): _planned(node)
354
+ for node in ast.walk(tree)
355
+ if isinstance(node, _Def)
356
+ }
357
+
358
+
359
+ def _planned(function: _Def) -> dict[int, _Statement]:
360
+ """Plan a function: its own statements that bind names, each under every line it evaluates on.
361
+
362
+ A compound statement is what it evaluates itself (`for x in xs:`, not its body). Statements
363
+ that share a line are one.
364
+
365
+ Returns:
366
+ Each line's statement.
367
+
368
+ """
369
+ lines: dict[int, _Statement] = {}
370
+ statement: ast.stmt
371
+ for statement in _statements(function):
372
+ evaluated: list[ast.AST] = list(_evaluated(statement))
373
+ stores: list[tuple[int, str]] = [
374
+ (node.lineno, node.id)
375
+ for node in evaluated
376
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)
377
+ ]
378
+ if stores:
379
+ last: int = max(
380
+ node.end_lineno or node.lineno for node in evaluated if isinstance(node, ast.expr)
381
+ )
382
+ planned: _Statement = lines.setdefault(statement.lineno, _Statement())
383
+ planned.stores.extend(stores)
384
+ line: int
385
+ for line in range(statement.lineno, last + 1):
386
+ _ = lines.setdefault(line, planned)
387
+ return lines
388
+
389
+
390
+ def _statements(node: ast.AST) -> Iterator[ast.stmt]:
391
+ """Walk the statements of a function's own scope, each before those under it.
392
+
393
+ Yields:
394
+ Each one: not a function's or a class's definition, nor what's in it.
395
+
396
+ """
397
+ child: ast.AST
398
+ for child in ast.iter_child_nodes(node):
399
+ if isinstance(child, ast.stmt) and not isinstance(child, SCOPES):
400
+ yield child
401
+ if isinstance(child, _BLOCKS) and not isinstance(child, SCOPES):
402
+ yield from _statements(child)
403
+
404
+
405
+ def _evaluated(node: ast.AST) -> Iterator[ast.AST]:
406
+ """Walk what a statement evaluates itself: not the statements under it, nor another scope's.
407
+
408
+ Yields:
409
+ Each node under `node`.
410
+
411
+ """
412
+ child: ast.AST
413
+ for child in ast.iter_child_nodes(node):
414
+ if not isinstance(child, (*_BLOCKS, *SCOPES)):
415
+ yield child
416
+ yield from _evaluated(child)
417
+
418
+
419
+ class Recorder:
420
+ """Notes the types of the names each statement binds, told where each recorded function is."""
421
+
422
+ def __init__(self, root: Path) -> None:
423
+ """Record the functions of the files under `root`."""
424
+ self.root: Path = root.resolve()
425
+ # By the code object's id: two alike in two files are equal. None: not one to record.
426
+ self.functions: dict[int, _Function | None] = {}
427
+ self.codes: dict[int, CodeType] = {} # each one seen, kept so its id stays its own
428
+ self.files: dict[str, _File | None] = {} # each file under the root that could be read
429
+ self.modules: dict[str, str] = {} # the SHA-256 of each module a recorded function is in
430
+ # The statement each frame is in, if it binds a name, by the frame's id, with the frame's code.
431
+ self.pending: dict[int, tuple[CodeType, _Statement]] = {}
432
+
433
+ def wanted(self, frame: FrameType) -> bool:
434
+ """Check whether `frame`'s function is one to record, still.
435
+
436
+ Returns:
437
+ Whether it is.
438
+
439
+ """
440
+ return self._function(frame) is not None
441
+
442
+ def line(self, frame: FrameType, at: int = 0) -> bool:
443
+ """Note what the statement `frame` was in bound, as it arrives at another's line.
444
+
445
+ `at`: the line, where the caller was told it (0: it wasn't).
446
+
447
+ Returns:
448
+ Whether `frame`'s function is one to record, still.
449
+
450
+ """
451
+ function: _Function | None
452
+ if (function := self._function(frame)) is None:
453
+ return False
454
+ function.quiet += 1
455
+ statement: _Statement | None = function.lines.get(at or frame.f_lineno)
456
+ if self.pending.get(id(frame), (None, statement))[1] is not statement:
457
+ self.ended(frame, function)
458
+ if statement is not None:
459
+ self.pending[id(frame)] = (frame.f_code, statement)
460
+ return self.functions[id(frame.f_code)] is function and not function.spent
461
+
462
+ def left(self, frame: FrameType, *, finished: bool = False) -> bool:
463
+ """Note what `frame`'s last statement bound, as its function returns: not as it yields.
464
+
465
+ `finished`: the caller was told of a return, not of a `yield` too (`sys.monitoring`): a
466
+ generator's or a coroutine's is counted then, as any function's.
467
+
468
+ Returns:
469
+ Whether `frame`'s function is one to record, still.
470
+
471
+ """
472
+ function: _Function | None
473
+ if (function := self._function(frame)) is None:
474
+ return False
475
+ if not finished and frame.f_code.co_flags & _SUSPENDS:
476
+ return True
477
+ self.ended(frame, function)
478
+ function.idle = 0 if function.fresh else function.idle + 1
479
+ function.fresh = False
480
+ return self.functions[id(frame.f_code)] is function and not function.spent
481
+
482
+ def ended(self, frame: FrameType, function: _Function) -> None:
483
+ """Note what the statement `frame` was in bound: it ended."""
484
+ self._note(frame, function)
485
+
486
+ def raised(self, frame: FrameType) -> None:
487
+ """Forget the statement `frame` was in: an exception ended it."""
488
+ _ = self.pending.pop(id(frame), None)
489
+
490
+ def _note(self, frame: FrameType, function: _Function) -> None:
491
+ """Add the types of what the statement `frame` was in bound, and forget the statement."""
492
+ code: CodeType | None
493
+ statement: _Statement
494
+ code, statement = self.pending.pop(id(frame), (None, _Statement()))
495
+ if code is not frame.f_code or statement.idle >= _IDLE: # another frame's, of this one's id
496
+ return
497
+ try:
498
+ new: bool = _noted(function.file.held, statement.stores, frame.f_locals)
499
+ except (AttributeError, LookupError, RuntimeError, TypeError, ValueError):
500
+ # A container another thread changed, or a class that isn't one: its function isn't recorded.
501
+ self.functions[id(code)] = None
502
+ line: int
503
+ name: str
504
+ for line, name in (store for each in function.lines.values() for store in each.stores):
505
+ _ = function.file.held.get(line, {}).pop(name, None)
506
+ return
507
+ statement.idle = 0 if new else statement.idle + 1
508
+ function.fresh = function.fresh or new
509
+ if new:
510
+ function.quiet = 0
511
+
512
+ def _function(self, frame: FrameType) -> _Function | None:
513
+ """Find `frame`'s function, if it's one to record: in a file under the root, binding a name.
514
+
515
+ Returns:
516
+ It, or `None`: also for one recorded no more (`_Function.spent`).
517
+
518
+ """
519
+ code: CodeType = frame.f_code
520
+ if id(code) not in self.functions:
521
+ self.codes[id(code)] = code
522
+ if code.co_name == _MODULE:
523
+ _ = self._file(code.co_filename) # read as it's imported: its functions' calls needn't
524
+ file: _File | None = None if code.co_name.startswith("<") else self._file(code.co_filename)
525
+ lines: dict[int, _Statement] = {} if file is None else file.plans.get(code.co_firstlineno, {})
526
+ self.functions[id(code)] = _Function(file, lines) if file is not None and lines else None
527
+ if file is not None and lines:
528
+ self.modules[str(frame.f_globals.get("__name__"))] = file.digest
529
+ function: _Function | None = self.functions[id(code)]
530
+ return None if function is None or function.spent else function
531
+
532
+ def _file(self, filename: str) -> _File | None:
533
+ """Read the file `filename`, once.
534
+
535
+ Returns:
536
+ It, or `None` for a file elsewhere than under the root, installed there (`site-packages`),
537
+ or that can't be read or parsed; or that can't be read now, and is asked for again: the
538
+ program may forbid reading a file where this is called (blockbuster, in an event loop),
539
+ and what it raises then mustn't reach it.
540
+
541
+ """
542
+ if filename not in self.files:
543
+ try:
544
+ self.files[filename] = self._under(filename)
545
+ except Exception: # ruff: ignore[blind-except] # pylint: disable=broad-exception-caught
546
+ return None
547
+ return self.files[filename]
548
+
549
+ def _under(self, filename: str) -> _File | None:
550
+ # `_file`, read from disk: whatever the program raises of that.
551
+ path: Path = Path(filename).resolve()
552
+ under: Path | None = path.relative_to(self.root) if path.is_relative_to(self.root) else None
553
+ return None if under is None or _INSTALLED in under.parts else _read(path, under)
554
+
555
+ def found(self) -> Found:
556
+ """Gather what was recorded, as the file holds it.
557
+
558
+ Returns:
559
+ The trace: a file's copies as one file, and only the modules it has something of.
560
+
561
+ """
562
+ files: dict[str, Entry] = merged(
563
+ {
564
+ "version": VERSION,
565
+ "files": {file.digest: {"path": file.path.as_posix(), "bindings": _written(file.held)}},
566
+ "modules": {},
567
+ }
568
+ for file in self.files.values()
569
+ if file is not None and _written(file.held)
570
+ )["files"]
571
+ return {
572
+ "version": VERSION,
573
+ "files": files,
574
+ "modules": {module: digest for module, digest in self.modules.items() if digest in files},
575
+ }
576
+
577
+ def write(self, output: Path) -> None:
578
+ """Write what was recorded to `output`."""
579
+ write(output, self.found())
580
+
581
+
582
+ def _written(held: _Held) -> Bindings:
583
+ """Write a file's bindings as a trace holds them.
584
+
585
+ Returns:
586
+ Each one's spellings, sorted, by its line, then its name: not one with none.
587
+
588
+ """
589
+ return {
590
+ str(line): {name: sorted(types) for name, types in names.items() if types}
591
+ for line, names in held.items()
592
+ if any(names.values())
593
+ }
594
+
595
+
596
+ def merged(traces: Iterable[Found]) -> Found:
597
+ """Join the traces of one run's processes.
598
+
599
+ Returns:
600
+ One trace: each binding with its spellings in them all, each file under the first path it has.
601
+
602
+ """
603
+ files: dict[str, Entry] = {}
604
+ modules: dict[str, str] = {}
605
+ found: Found
606
+ for found in traces:
607
+ modules.update(found["modules"])
608
+ digest: str
609
+ entry: Entry
610
+ for digest, entry in found["files"].items():
611
+ kept: Entry = files.setdefault(digest, {"path": entry["path"], "bindings": {}})
612
+ line: str
613
+ names: dict[str, list[str]]
614
+ for line, names in entry["bindings"].items():
615
+ held: dict[str, list[str]] = kept["bindings"].setdefault(line, {})
616
+ held.update({name: sorted({*held.get(name, ()), *types}) for name, types in names.items()})
617
+ return {"version": VERSION, "files": files, "modules": modules}
618
+
619
+
620
+ def write(output: Path, found: Found) -> None:
621
+ """Write the trace `found` to `output`."""
622
+ _ = output.write_text(json.dumps(found, sort_keys=True) + "\n", encoding="utf-8")
623
+
624
+
625
+ def _noted(held: _Held, stores: Iterable[tuple[int, str]], values: Mapping[str, object]) -> bool:
626
+ """Add to `held` the types of the names of `stores` that a frame holds (`values`).
627
+
628
+ Returns:
629
+ Whether any is new.
630
+
631
+ """
632
+ new: bool = False
633
+ line: int
634
+ name: str
635
+ for line, name in stores:
636
+ if name in values:
637
+ text: str | None = spelled(values[name])
638
+ types: set[str] = held.setdefault(line, {}).setdefault(name, set())
639
+ if text is not None and text not in types:
640
+ types.add(text)
641
+ new = True
642
+ return new
643
+
644
+
645
+ def _read(path: Path, under: Path) -> _File | None:
646
+ """Read the file at `path`, `under` the root: hashed as `--infer-from` will, and planned.
647
+
648
+ Returns:
649
+ It, or `None` for one that can't be read or parsed.
650
+
651
+ """
652
+ try:
653
+ source: bytes = path.read_bytes()
654
+ except OSError:
655
+ return None
656
+ try:
657
+ plans: dict[int, dict[int, _Statement]] = _plans(ast.parse(source))
658
+ except (SyntaxError, ValueError):
659
+ return None
660
+ return _File(under, hashlib.sha256(source).hexdigest(), plans)
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The rules: what each code reports, and the analysis behind it."""