python-constricter 0.4.1__cp313-cp313-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__.cp313-win32.pyd +0 -0
  29. constricter/fix/__init__.py +2 -0
  30. constricter/fix/core/__init__.cp313-win32.pyd +0 -0
  31. constricter/fix/core/__init__.py +2 -0
  32. constricter/fix/core/asked.cp313-win32.pyd +0 -0
  33. constricter/fix/core/asked.py +56 -0
  34. constricter/fix/core/fixes.cp313-win32.pyd +0 -0
  35. constricter/fix/core/fixes.py +317 -0
  36. constricter/fix/core/imports.cp313-win32.pyd +0 -0
  37. constricter/fix/core/imports.py +580 -0
  38. constricter/fix/core/inherited.cp313-win32.pyd +0 -0
  39. constricter/fix/core/inherited.py +311 -0
  40. constricter/fix/core/known.cp313-win32.pyd +0 -0
  41. constricter/fix/core/known.py +730 -0
  42. constricter/fix/core/signatures.cp313-win32.pyd +0 -0
  43. constricter/fix/core/signatures.py +132 -0
  44. constricter/fix/index/__init__.cp313-win32.pyd +0 -0
  45. constricter/fix/index/__init__.py +2 -0
  46. constricter/fix/index/awaits.cp313-win32.pyd +0 -0
  47. constricter/fix/index/awaits.py +90 -0
  48. constricter/fix/index/beyond.cp313-win32.pyd +0 -0
  49. constricter/fix/index/beyond.py +149 -0
  50. constricter/fix/index/callers.cp313-win32.pyd +0 -0
  51. constricter/fix/index/callers.py +155 -0
  52. constricter/fix/index/crossed.cp313-win32.pyd +0 -0
  53. constricter/fix/index/crossed.py +327 -0
  54. constricter/fix/index/decorated.cp313-win32.pyd +0 -0
  55. constricter/fix/index/decorated.py +140 -0
  56. constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
  57. constricter/fix/index/fixtures.py +176 -0
  58. constricter/fix/index/linked.cp313-win32.pyd +0 -0
  59. constricter/fix/index/linked.py +142 -0
  60. constricter/fix/index/loose.cp313-win32.pyd +0 -0
  61. constricter/fix/index/loose.py +103 -0
  62. constricter/fix/index/managed.cp313-win32.pyd +0 -0
  63. constricter/fix/index/managed.py +63 -0
  64. constricter/fix/index/modules.cp313-win32.pyd +0 -0
  65. constricter/fix/index/modules.py +741 -0
  66. constricter/fix/index/offers.cp313-win32.pyd +0 -0
  67. constricter/fix/index/offers.py +252 -0
  68. constricter/fix/index/order.cp313-win32.pyd +0 -0
  69. constricter/fix/index/order.py +155 -0
  70. constricter/fix/index/origins.cp313-win32.pyd +0 -0
  71. constricter/fix/index/origins.py +238 -0
  72. constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
  73. constricter/fix/index/own_methods.py +91 -0
  74. constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
  75. constricter/fix/index/own_overloads.py +70 -0
  76. constricter/fix/index/own_types.cp313-win32.pyd +0 -0
  77. constricter/fix/index/own_types.py +330 -0
  78. constricter/fix/index/params.cp313-win32.pyd +0 -0
  79. constricter/fix/index/params.py +81 -0
  80. constricter/fix/index/plain.cp313-win32.pyd +0 -0
  81. constricter/fix/index/plain.py +221 -0
  82. constricter/fix/index/project.cp313-win32.pyd +0 -0
  83. constricter/fix/index/project.py +797 -0
  84. constricter/fix/index/sides.cp313-win32.pyd +0 -0
  85. constricter/fix/index/sides.py +160 -0
  86. constricter/fix/index/strings.cp313-win32.pyd +0 -0
  87. constricter/fix/index/strings.py +46 -0
  88. constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
  89. constricter/fix/index/stubs/__init__.py +2 -0
  90. constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
  91. constricter/fix/index/stubs/atoms.py +425 -0
  92. constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
  93. constricter/fix/index/stubs/chained.py +69 -0
  94. constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
  95. constricter/fix/index/stubs/classnames.py +124 -0
  96. constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
  97. constricter/fix/index/stubs/declared.py +508 -0
  98. constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
  99. constricter/fix/index/stubs/installed.py +425 -0
  100. constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
  101. constricter/fix/index/stubs/names.py +200 -0
  102. constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
  103. constricter/fix/index/stubs/read.py +70 -0
  104. constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
  105. constricter/fix/index/stubs/signatures.py +656 -0
  106. constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
  107. constricter/fix/index/stubs/stubbed.py +208 -0
  108. constricter/fix/index/taken.cp313-win32.pyd +0 -0
  109. constricter/fix/index/taken.py +45 -0
  110. constricter/fix/index/tuples.cp313-win32.pyd +0 -0
  111. constricter/fix/index/tuples.py +112 -0
  112. constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
  113. constricter/fix/libraries/__init__.py +2 -0
  114. constricter/fix/libraries/library.cp313-win32.pyd +0 -0
  115. constricter/fix/libraries/library.py +420 -0
  116. constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
  117. constricter/fix/libraries/opened.py +147 -0
  118. constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
  119. constricter/fix/libraries/overloads.py +960 -0
  120. constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
  121. constricter/fix/libraries/receivers.py +274 -0
  122. constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
  123. constricter/fix/libraries/stdlib.py +752 -0
  124. constricter/fix/libraries/walks.cp313-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__.cp313-win32.pyd +0 -0
  148. constricter/fix/values/__init__.py +2 -0
  149. constricter/fix/values/aliased.cp313-win32.pyd +0 -0
  150. constricter/fix/values/aliased.py +195 -0
  151. constricter/fix/values/bodies.cp313-win32.pyd +0 -0
  152. constricter/fix/values/bodies.py +76 -0
  153. constricter/fix/values/callables.cp313-win32.pyd +0 -0
  154. constricter/fix/values/callables.py +323 -0
  155. constricter/fix/values/called.cp313-win32.pyd +0 -0
  156. constricter/fix/values/called.py +194 -0
  157. constricter/fix/values/classvars.cp313-win32.pyd +0 -0
  158. constricter/fix/values/classvars.py +629 -0
  159. constricter/fix/values/decided.cp313-win32.pyd +0 -0
  160. constricter/fix/values/decided.py +429 -0
  161. constricter/fix/values/displays.cp313-win32.pyd +0 -0
  162. constricter/fix/values/displays.py +101 -0
  163. constricter/fix/values/doubts.cp313-win32.pyd +0 -0
  164. constricter/fix/values/doubts.py +677 -0
  165. constricter/fix/values/entered.cp313-win32.pyd +0 -0
  166. constricter/fix/values/entered.py +332 -0
  167. constricter/fix/values/fills.cp313-win32.pyd +0 -0
  168. constricter/fix/values/fills.py +446 -0
  169. constricter/fix/values/guesses.cp313-win32.pyd +0 -0
  170. constricter/fix/values/guesses.py +376 -0
  171. constricter/fix/values/hinted.cp313-win32.pyd +0 -0
  172. constricter/fix/values/hinted.py +811 -0
  173. constricter/fix/values/inference.cp313-win32.pyd +0 -0
  174. constricter/fix/values/inference.py +698 -0
  175. constricter/fix/values/loops.cp313-win32.pyd +0 -0
  176. constricter/fix/values/loops.py +335 -0
  177. constricter/fix/values/members.cp313-win32.pyd +0 -0
  178. constricter/fix/values/members.py +578 -0
  179. constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
  180. constricter/fix/values/narrowed.py +220 -0
  181. constricter/fix/values/operated.cp313-win32.pyd +0 -0
  182. constricter/fix/values/operated.py +202 -0
  183. constricter/fix/values/ordered.cp313-win32.pyd +0 -0
  184. constricter/fix/values/ordered.py +74 -0
  185. constricter/fix/values/returned.cp313-win32.pyd +0 -0
  186. constricter/fix/values/returned.py +479 -0
  187. constricter/fix/values/returns.cp313-win32.pyd +0 -0
  188. constricter/fix/values/returns.py +237 -0
  189. constricter/fix/values/shapes.cp313-win32.pyd +0 -0
  190. constricter/fix/values/shapes.py +654 -0
  191. constricter/fix/values/stored.cp313-win32.pyd +0 -0
  192. constricter/fix/values/stored.py +564 -0
  193. constricter/fix/values/targets.cp313-win32.pyd +0 -0
  194. constricter/fix/values/targets.py +419 -0
  195. constricter/fix/values/unchecked.cp313-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__.cp313-win32.pyd +0 -0
  208. constricter/rules/__init__.py +2 -0
  209. constricter/rules/annotations.cp313-win32.pyd +0 -0
  210. constricter/rules/annotations.py +296 -0
  211. constricter/rules/binding.cp313-win32.pyd +0 -0
  212. constricter/rules/binding.py +652 -0
  213. constricter/rules/calls.cp313-win32.pyd +0 -0
  214. constricter/rules/calls.py +460 -0
  215. constricter/rules/checker.cp313-win32.pyd +0 -0
  216. constricter/rules/checker.py +536 -0
  217. constricter/rules/declarations.cp313-win32.pyd +0 -0
  218. constricter/rules/declarations.py +730 -0
  219. constricter/rules/decorators.cp313-win32.pyd +0 -0
  220. constricter/rules/decorators.py +351 -0
  221. constricter/rules/flow.cp313-win32.pyd +0 -0
  222. constricter/rules/flow.py +502 -0
  223. constricter/rules/keys.cp313-win32.pyd +0 -0
  224. constricter/rules/keys.py +79 -0
  225. constricter/rules/late.cp313-win32.pyd +0 -0
  226. constricter/rules/late.py +969 -0
  227. constricter/rules/narrowing.cp313-win32.pyd +0 -0
  228. constricter/rules/narrowing.py +98 -0
  229. constricter/rules/parsed.cp313-win32.pyd +0 -0
  230. constricter/rules/parsed.py +136 -0
  231. constricter/rules/quoted.cp313-win32.pyd +0 -0
  232. constricter/rules/quoted.py +141 -0
  233. constricter/rules/rebinding.cp313-win32.pyd +0 -0
  234. constricter/rules/rebinding.py +169 -0
  235. constricter/rules/recorded.cp313-win32.pyd +0 -0
  236. constricter/rules/recorded.py +168 -0
  237. constricter/rules/redundant.cp313-win32.pyd +0 -0
  238. constricter/rules/redundant.py +160 -0
  239. constricter/rules/scope.cp313-win32.pyd +0 -0
  240. constricter/rules/scope.py +802 -0
  241. constricter/rules/scoped.cp313-win32.pyd +0 -0
  242. constricter/rules/scoped.py +232 -0
  243. constricter/rules/scoping.cp313-win32.pyd +0 -0
  244. constricter/rules/scoping.py +346 -0
  245. constricter/rules/settings.cp313-win32.pyd +0 -0
  246. constricter/rules/settings.py +189 -0
  247. constricter/rules/syntax.cp313-win32.pyd +0 -0
  248. constricter/rules/syntax.py +472 -0
  249. constricter/rules/tables.cp313-win32.pyd +0 -0
  250. constricter/rules/tables.py +169 -0
  251. constricter/rules/walked.cp313-win32.pyd +0 -0
  252. constricter/rules/walked.py +127 -0
  253. constricter/rules/widened.cp313-win32.pyd +0 -0
  254. constricter/rules/widened.py +322 -0
  255. constricter/trace.py +374 -0
  256. e45ce06b2fdd5c43f782__mypyc.cp313-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,629 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """`--fix` for a plain class's variables: `limit = 3` in a class body, typed by its literal value.
3
+
4
+ An annotation in a class body can be more than a type: a dataclass's, a `NamedTuple`'s or a model's
5
+ makes the variable a field. So only a plain class's body is fixed: one defined once in its module,
6
+ with no decorator or metaclass, every base of which is `object`, a `unittest` test case, a builtin
7
+ exception or value class (`ValueError`, `str`: see `BUILTIN_BASES`), `Generic[T]` or `abc.ABC`
8
+ (`PASSED_OVER`), a listed one (`listed`: a framework's that reads no annotations, as
9
+ `fix-plain-bases` says), or another plain class, subscripted or not (`Box[int]`); and no class that
10
+ isn't plain may inherit from it (a mixin of a model's). What the module alone sees is
11
+ `plain`; with the CLI, the index of checked files says which bases other files define are plain too
12
+ (see `constricter.fix.index.project`).
13
+
14
+ A variable counts when it's bound once, by a plain `name = value` directly in the body, to a value
15
+ whose type its own text decides (a literal, or a display of them) or the standard library does, by
16
+ the module's imports (`re.compile("x")`, `os.sep`, `len(NAMES)`), and nothing in the module stores
17
+ the attribute as anything else (`self.limit = 5` and `self.limit += 1` keep an `int`;
18
+ `self.limit = size` may not), nor is it one a builtin base has itself (`errno` under `OSError`) or a
19
+ class above it annotates as another type (`limit: int | None`): a type checker holds a variable to
20
+ what its base declares. Its type is that value's, whatever the module
21
+ declares: the same read from the file alone, so the files reading the attribute (`self.limit`,
22
+ `cls.limit`) are typed in the same run as the class is fixed. But one a test case declares itself
23
+ (`maxDiff`, an `int | None`) is that type, under any base no checked file defines but a builtin
24
+ one, where its value is of it (see `HELD`). A guess (`member`): a subclass, or code elsewhere, may
25
+ bind it to another type.
26
+ """
27
+
28
+ import ast
29
+ import builtins
30
+ import re
31
+ from collections import Counter
32
+ from collections.abc import Callable, Mapping, Sequence
33
+ from typing import Final, TypeAlias, cast
34
+ from weakref import WeakKeyDictionary
35
+
36
+ from constricter.fix.core.imports import plan, taken_names
37
+ from constricter.fix.core.known import Inference, Known, LibraryNames, Outside
38
+ from constricter.fix.libraries import stdlib
39
+ from constricter.fix.values.inference import inference
40
+ from constricter.fix.values.operated import operated
41
+ from constricter.offences import MAX_LENGTH
42
+ from constricter.rules.annotations import dotted
43
+ from constricter.rules.declarations import classes as annotated
44
+ from constricter.rules.flow import members as union
45
+ from constricter.rules.syntax import import_bindings
46
+ from constricter.rules.walked import classes, of_type
47
+
48
+ OBJECT: Final = "object"
49
+ # The standard library's test cases: their subclasses' variables are only ever their own.
50
+ TEST_CASES: Final = frozenset(
51
+ {
52
+ "unittest.TestCase",
53
+ "unittest.IsolatedAsyncioTestCase",
54
+ "unittest.case.TestCase",
55
+ "unittest.async_case.IsolatedAsyncioTestCase",
56
+ },
57
+ )
58
+ # The builtin classes of values whose subclasses' variables are their own, as an exception's are.
59
+ _VALUES: Final = frozenset(
60
+ {"bytearray", "bytes", "complex", "dict", "float", "frozenset", "int", "list", "set", "str", "tuple"},
61
+ )
62
+ # The standard library's bases that read no annotation in a class body, and bind nothing there.
63
+ PASSED_OVER: Final = frozenset({"abc.ABC", "typing.Generic", "typing_extensions.Generic"})
64
+ # The functions a call to which, as a base, makes a class of the call's fields: its body is plain.
65
+ # `bases` spells such a base with `CALLED` after the function (`collections.namedtuple()`): the class
66
+ # under `NamedTuple` itself has fields in its body, and isn't plain.
67
+ FUNCTIONAL: Final = frozenset({"namedtuple", "NamedTuple", "TypedDict"})
68
+ CALLED: Final = "()"
69
+ FUNCTIONAL_BASES: Final = frozenset(
70
+ {
71
+ f"collections.namedtuple{CALLED}",
72
+ *(
73
+ f"{module}.{name}{CALLED}"
74
+ for module in ("typing", "typing_extensions")
75
+ for name in ("NamedTuple", "TypedDict")
76
+ ),
77
+ },
78
+ )
79
+ Bases: TypeAlias = Mapping[str, tuple[str, ...]] # each class's bases as written (see `bases`)
80
+ SPECIAL: Final = "" # a base no class has: what makes a class one that can't be plain
81
+ # Each module's classes' bases and variables, for as long as its tree lives: read for the index,
82
+ # then for the check.
83
+ _Bases: TypeAlias = dict[str, tuple[str, ...]]
84
+ _Members: TypeAlias = dict[str, dict[str, str]]
85
+ _BASES: Final[WeakKeyDictionary[ast.Module, _Bases]] = WeakKeyDictionary()
86
+ _MEMBERS: Final[WeakKeyDictionary[ast.Module, _Members]] = WeakKeyDictionary()
87
+ # What decides a value's type from its own text, with what the module imports of the standard
88
+ # library: nothing the module declares.
89
+ _OWN_KINDS: Final = frozenset(
90
+ {
91
+ "literal",
92
+ "container",
93
+ "arithmetic",
94
+ "compare",
95
+ "stdlib",
96
+ "builtin",
97
+ "builder",
98
+ "loop",
99
+ "method",
100
+ "conditional",
101
+ "boolean",
102
+ "comprehension",
103
+ "open",
104
+ },
105
+ )
106
+ _NONE: Final = "None"
107
+ _NOT: Final = "!" # an entry that leaves a base out (see `listed`)
108
+ _DEFINITIONS: Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
109
+ # One store of an attribute: its value, and (augmented) its operator.
110
+ _Store: TypeAlias = tuple[ast.operator | None, ast.expr]
111
+
112
+
113
+ def _builtin_bases() -> dict[str, frozenset[str]]:
114
+ """Find the builtin classes a plain class may inherit from: the exceptions, and `_VALUES`.
115
+
116
+ Returns:
117
+ Each one's name, and the names of what it has itself.
118
+
119
+ """
120
+ return {
121
+ name: frozenset(dir(cast("object", getattr(builtins, name))))
122
+ for name in dir(builtins)
123
+ if _is_base(name, cast("object", getattr(builtins, name)))
124
+ }
125
+
126
+
127
+ def _is_base(name: str, value: object) -> bool:
128
+ """Check whether a builtin is a class a plain class may inherit from.
129
+
130
+ Returns:
131
+ Whether it is.
132
+
133
+ """
134
+ return isinstance(value, type) and (name in _VALUES or issubclass(value, BaseException))
135
+
136
+
137
+ BUILTIN_BASES: Final = _builtin_bases()
138
+ _WORD: Final = re.compile(r"[A-Za-z_][\w.]*")
139
+ # What a test case declares of itself, where builtins alone spell it: a subclass's variable of the
140
+ # name has that type, to a type checker, whatever its value's is.
141
+ HELD: Final = {
142
+ name: typed
143
+ for name, typed in stdlib.attributes("unittest.TestCase").items()
144
+ if all(word == _NONE or hasattr(builtins, word) for word in cast("list[str]", _WORD.findall(typed)))
145
+ }
146
+
147
+
148
+ def member_type(value: ast.expr, known: Known) -> str | None:
149
+ """Type a class variable's value by its own text: a literal, or a display of them.
150
+
151
+ Or, `known` naming the standard library as the module does (see `library`), by what it gives:
152
+ a function's or a class's with a fixed type, an attribute, a builtin's.
153
+
154
+ Returns:
155
+ The annotation, or `None` for a value that names anything else (a class, a copy), or `None`
156
+ itself.
157
+
158
+ """
159
+ found: Inference | None = inference(value, known, {})
160
+ if found is None or found.annotation == _NONE or not found.kinds <= _OWN_KINDS:
161
+ return None
162
+ return found.annotation
163
+
164
+
165
+ def library(tree: ast.Module) -> Known:
166
+ """Know what a module names of the standard library, and nothing it declares itself.
167
+
168
+ Returns:
169
+ What `member_type` types a value by, the same for every file that reads the module.
170
+
171
+ """
172
+ return Known({}, frozenset(), {}, {}, names=LibraryNames(frozenset(), stdlib.origins(tree), plan(tree)))
173
+
174
+
175
+ def bases(tree: ast.Module) -> dict[str, tuple[str, ...]]:
176
+ """Map each class the module defines once to its bases, as written.
177
+
178
+ Returns:
179
+ Each one's bases (`object` left out, a subscripted one by its name, a call to a functional
180
+ `namedtuple`, `NamedTuple` or `TypedDict` by the function's: a class with those fields); with
181
+ `SPECIAL` among them for a class that can't be plain: decorated, given a metaclass or another
182
+ keyword, or with a base that isn't a name (`make()`) or is a builtin's the module binds itself.
183
+
184
+ """
185
+ found: dict[str, tuple[str, ...]] | None
186
+ if (found := _BASES.get(tree)) is None:
187
+ found = _BASES[tree] = _read_bases(tree)
188
+ return found
189
+
190
+
191
+ def _read_bases(tree: ast.Module) -> dict[str, tuple[str, ...]]:
192
+ taken: frozenset[str] = taken_names(tree)[0]
193
+ counts: dict[str, int] = {}
194
+ node: ast.ClassDef
195
+ for node in classes(tree):
196
+ counts[node.name] = counts.get(node.name, 0) + 1
197
+ found: dict[str, tuple[str, ...]] = {}
198
+ for node in classes(tree):
199
+ if counts[node.name] == 1:
200
+ names: list[str | None] = [_base_name(base) for base in node.bases]
201
+ shadowed: bool = any(name in BUILTIN_BASES and name in taken for name in names)
202
+ special: bool = bool(node.keywords or node.decorator_list) or None in names or shadowed
203
+ found[node.name] = (
204
+ *([SPECIAL] if special else []),
205
+ *(name for name in names if name and name != OBJECT),
206
+ )
207
+ return found
208
+
209
+
210
+ def _base_name(base: ast.expr) -> str | None:
211
+ """Spell a base: a name, a subscripted one's, or a functional `namedtuple`-like call's function.
212
+
213
+ Returns:
214
+ It, or `None` for a base that's anything else (`make()`).
215
+
216
+ """
217
+ name: str | None
218
+ value: ast.expr
219
+ func: ast.expr
220
+ match base:
221
+ case ast.Subscript(value=value):
222
+ return dotted(value)
223
+ case ast.Call(func=func) if (name := dotted(func)) is not None and name.rpartition(".")[
224
+ 2
225
+ ] in FUNCTIONAL:
226
+ return f"{name}{CALLED}"
227
+ case _:
228
+ return dotted(base)
229
+
230
+
231
+ def imported(tree: ast.Module) -> dict[str, str]:
232
+ """Map each top-level name the module's absolute imports bind to what it is (`models.Model`'s `models`).
233
+
234
+ Returns:
235
+ Each bound name's dotted origin, to resolve a base by (see `plain`).
236
+
237
+ """
238
+ return {name: origin for name, origin, _ in import_bindings(tree.body)}
239
+
240
+
241
+ def plain(tree: ast.Module, origins: Mapping[str, str], entries: Sequence[str] = ()) -> frozenset[str]:
242
+ """Find the module's plain classes, as far as the module alone sees (see the module docstring).
243
+
244
+ `origins`: what each name its imports bind refers to (see `imported`), to know a test case;
245
+ `entries`: the listed bases (see `listed`).
246
+
247
+ Returns:
248
+ Their names.
249
+
250
+ """
251
+ return settled(bases(tree), lambda base: allowed(_resolved(base, origins), entries))
252
+
253
+
254
+ def allowed(base: str, entries: Sequence[str] = ()) -> bool:
255
+ """Check whether a base no checked file defines is one a plain class may have.
256
+
257
+ Returns:
258
+ Whether it's a test case's (`TEST_CASES`), a builtin's (`BUILTIN_BASES`), one passed over
259
+ (`PASSED_OVER`) or a listed one (`listed`), by its origin.
260
+
261
+ """
262
+ return (
263
+ base in TEST_CASES
264
+ or base in BUILTIN_BASES
265
+ or base in PASSED_OVER
266
+ or base in FUNCTIONAL_BASES
267
+ or listed(base, entries)
268
+ )
269
+
270
+
271
+ def listed(base: str, entries: Sequence[str]) -> bool:
272
+ """Check whether `entries` list a base, by its dotted origin (`django.db.models.Model`).
273
+
274
+ An entry names a class, or a package for every class in it (`django`); one starting `!` leaves
275
+ out what it names, whatever else lists it.
276
+
277
+ Returns:
278
+ Whether one lists it, and none leaves it out.
279
+
280
+ """
281
+ left_out: list[str] = [entry[1:] for entry in entries if entry.startswith(_NOT)]
282
+ kept: list[str] = [entry for entry in entries if not entry.startswith(_NOT)]
283
+ return _under(base, kept) and not _under(base, left_out)
284
+
285
+
286
+ def _under(base: str, paths: Sequence[str]) -> bool:
287
+ return any(base == path or base.startswith(f"{path}.") for path in paths)
288
+
289
+
290
+ def ancestors(name: str, found: Bases) -> list[str]:
291
+ """Name every class of `found` that `name` inherits from, however far.
292
+
293
+ Returns:
294
+ Them, sorted.
295
+
296
+ """
297
+ return sorted(_ancestors(name, found, frozenset({name})))
298
+
299
+
300
+ def reserved(name: str, found: Bases) -> frozenset[str]:
301
+ """Name what the builtin classes `name` inherits from have themselves, as far as `found` sees.
302
+
303
+ A variable hiding one would be declared another type than its base declares it.
304
+
305
+ Returns:
306
+ Those names.
307
+
308
+ """
309
+ owners: list[str] = [name, *ancestors(name, found)]
310
+ return frozenset[str]().union(
311
+ *(BUILTIN_BASES.get(base, frozenset[str]()) for owner in owners for base in found[owner]),
312
+ )
313
+
314
+
315
+ def settled(found: Bases, outside: Callable[[str], bool]) -> frozenset[str]:
316
+ """Settle which classes are plain, given each one's bases (see `bases`).
317
+
318
+ `outside`: whether a base that isn't one of `found` is one a plain class may have.
319
+
320
+ Returns:
321
+ The names of those whose every base is plain, and that no class that isn't inherits from.
322
+
323
+ """
324
+ above: dict[str, set[str]] = {name: _ancestors(name, found, frozenset({name})) for name in found}
325
+ rooted: set[str] = {name for name in found if all(base in found or outside(base) for base in found[name])}
326
+ kept: set[str] = {name for name in rooted if above[name] <= rooted}
327
+ # A class that isn't plain makes what it inherits from not plain either: a model's mixin.
328
+ mixed: set[str] = {base for name in found.keys() - kept for base in above[name]}
329
+ return frozenset(kept - mixed)
330
+
331
+
332
+ def _ancestors(name: str, found: Bases, seen: frozenset[str]) -> set[str]:
333
+ """Name every class of `found` that `name` inherits from, however far.
334
+
335
+ Returns:
336
+ Them.
337
+
338
+ """
339
+ above: set[str] = {base for base in found[name] if base in found and base not in seen}
340
+ base: str
341
+ for base in sorted(above):
342
+ above |= _ancestors(base, found, seen | above)
343
+ return above
344
+
345
+
346
+ def _resolved(base: str, origins: Mapping[str, str]) -> str:
347
+ """Resolve a base as written through the module's imports (`TestCase`, `unittest.TestCase`).
348
+
349
+ Returns:
350
+ Its dotted origin; itself, if its first name isn't one the module imports.
351
+
352
+ """
353
+ first: str
354
+ rest: str
355
+ called: str = CALLED if base.endswith(CALLED) else ""
356
+ first, _, rest = base.removesuffix(CALLED).partition(".")
357
+ origin: str = origins.get(first, first)
358
+ return f"{origin}.{rest}{called}" if rest else f"{origin}{called}"
359
+
360
+
361
+ def variables(
362
+ tree: ast.Module,
363
+ origins: Mapping[str, str],
364
+ outside: Outside | None,
365
+ entries: Sequence[str] = (),
366
+ ) -> dict[str, Mapping[str, str]]:
367
+ """Type the plain classes' variables a module reads: its own, and those it imports.
368
+
369
+ Which of its own are plain is the index's to say (`Outside.plain`), or else the module's alone,
370
+ with `entries` the listed bases (see `listed`).
371
+
372
+ Returns:
373
+ Each class's variables' types, as the module spells the class.
374
+
375
+ """
376
+ own: frozenset[str] = own_plain(tree, origins, outside, entries)
377
+ typed: dict[str, dict[str, str]] = members(tree) if own else {}
378
+ # The index's of its own: what the file alone types, and what it types by other files' classes.
379
+ indexed: Mapping[str, Mapping[str, str]] = {} if outside is None else outside.members
380
+ return {
381
+ **indexed,
382
+ **{
383
+ name: {**typed.get(name, {}), **indexed.get(name, {})}
384
+ for name in own
385
+ if name in typed or name in indexed
386
+ },
387
+ }
388
+
389
+
390
+ def own_plain(
391
+ tree: ast.Module,
392
+ origins: Mapping[str, str],
393
+ outside: Outside | None,
394
+ entries: Sequence[str] = (),
395
+ ) -> frozenset[str]:
396
+ """Name the module's own plain classes: the index's to say (`Outside.plain`), or else the module's alone.
397
+
398
+ Returns:
399
+ Them.
400
+
401
+ """
402
+ indexed: frozenset[str] | None = None if outside is None else outside.plain
403
+ return plain(tree, origins, entries) if indexed is None else indexed
404
+
405
+
406
+ def members(tree: ast.Module) -> dict[str, dict[str, str]]:
407
+ """Type the variables of each class the module defines once (see the module docstring).
408
+
409
+ Of every such class, plain or not: which are plain is the caller's to say.
410
+
411
+ Returns:
412
+ Each class's variables' types, by name; a class with none is left out.
413
+
414
+ """
415
+ found: dict[str, dict[str, str]] | None
416
+ if (found := _MEMBERS.get(tree)) is None:
417
+ found = _MEMBERS[tree] = _read_members(tree)
418
+ return found
419
+
420
+
421
+ def _read_members(tree: ast.Module) -> dict[str, dict[str, str]]:
422
+ single: dict[str, tuple[str, ...]]
423
+ if not (single := bases(tree)):
424
+ return {}
425
+ stored: dict[str, list[_Store | None]] = _stores(tree)
426
+ origins: dict[str, str] = imported(tree)
427
+ known: Known = library(tree)
428
+ typed: dict[str, dict[str, str]] = {
429
+ node.name: _typed(
430
+ node.body,
431
+ reserved(node.name, single),
432
+ stored,
433
+ (HELD if _may_test(node.name, single, origins) else {}, known),
434
+ )
435
+ for node in classes(tree)
436
+ if node.name in single
437
+ }
438
+ if not any(typed.values()):
439
+ return {}
440
+ declared: Mapping[str, Mapping[str, str]] = annotated(tree)
441
+ kept: dict[str, dict[str, str]] = {
442
+ name: agreeing(each, [declared[above] for above in ancestors(name, single)])
443
+ for name, each in typed.items()
444
+ }
445
+ return {name: each for name, each in kept.items() if each}
446
+
447
+
448
+ def named(tree: ast.Module) -> dict[str, dict[str, str]]:
449
+ """Find the variables of each class the module defines once that are bound to a bare name.
450
+
451
+ `form_class = PasswordChangeForm`: a `type[PasswordChangeForm]`, where the index finds a checked
452
+ file's class by that name and no class above or under this one binds the variable
453
+ (`plain.settled`). Bound once, directly in the body, and not one the module stores
454
+ (`self.form_class = ...`), a builtin base has, or its value's own text types already.
455
+
456
+ Returns:
457
+ Each class's such variables, with the name each is bound to; a class with none left out.
458
+
459
+ """
460
+ single: dict[str, tuple[str, ...]]
461
+ if not (single := bases(tree)):
462
+ return {}
463
+ stored: dict[str, list[_Store | None]] = _stores(tree)
464
+ typed: dict[str, dict[str, str]] = members(tree)
465
+ found: dict[str, dict[str, str]] = {}
466
+ node: ast.ClassDef
467
+ for node in (each for each in classes(tree) if each.name in single):
468
+ counts: Counter[str] = Counter(name for stmt in node.body for name in _bound(stmt))
469
+ kept: frozenset[str] = reserved(node.name, single).union(typed.get(node.name, {}), stored)
470
+ bound: dict[str, str] = {}
471
+ stmt: ast.stmt
472
+ for stmt in node.body:
473
+ variable: str
474
+ value: str
475
+ match stmt:
476
+ case ast.Assign(targets=[ast.Name(id=variable)], value=ast.Name(id=value)) if (
477
+ counts[variable] == 1 and variable not in kept
478
+ ):
479
+ bound[variable] = value
480
+ case _:
481
+ pass
482
+ if bound:
483
+ found[node.name] = bound
484
+ return found
485
+
486
+
487
+ def _may_test(name: str, found: Bases, origins: Mapping[str, str]) -> bool:
488
+ """Check whether a class may be a test case: it has a base no class of the module, nor a builtin, is.
489
+
490
+ A test case's own, or a framework's (which this file can't see the bases of).
491
+
492
+ Returns:
493
+ Whether it may.
494
+
495
+ """
496
+ return any(
497
+ base not in found and _resolved(base, origins) not in BUILTIN_BASES.keys() | PASSED_OVER
498
+ for owner in (name, *ancestors(name, found))
499
+ for base in found[owner]
500
+ )
501
+
502
+
503
+ def agreeing(typed: Mapping[str, str], declared: Sequence[Mapping[str, str]]) -> dict[str, str]:
504
+ """Keep a class's variables that no class above it annotates as another type.
505
+
506
+ `declared`: each such class's annotated attributes.
507
+
508
+ Returns:
509
+ Those variables' types, by name.
510
+
511
+ """
512
+ return {
513
+ name: annotation
514
+ for name, annotation in typed.items()
515
+ if all(above.get(name, annotation) == annotation for above in declared)
516
+ }
517
+
518
+
519
+ def _stores(tree: ast.Module) -> dict[str, list[_Store | None]]:
520
+ """Find how the module stores each attribute, whatever it's an attribute of.
521
+
522
+ Returns:
523
+ Each stored attribute's name, with its every store: an assignment's value (`self.x = 1`), with
524
+ an augmented one's operator (`self.x += 1`), or `None` for any other (an unpacking, a loop's
525
+ target, a `del`, an annotated one).
526
+
527
+ """
528
+ assigned: dict[ast.expr, _Store] = {
529
+ target: (None, stmt.value)
530
+ for stmt in cast("list[ast.Assign]", of_type(tree, ast.Assign))
531
+ for target in stmt.targets
532
+ }
533
+ stmt: ast.AugAssign
534
+ for stmt in cast("list[ast.AugAssign]", of_type(tree, ast.AugAssign)):
535
+ assigned[stmt.target] = (stmt.op, stmt.value)
536
+ found: dict[str, list[_Store | None]] = {}
537
+ node: ast.Attribute
538
+ for node in cast("list[ast.Attribute]", of_type(tree, ast.Attribute)):
539
+ if not isinstance(node.ctx, ast.Load):
540
+ found.setdefault(node.attr, []).append(assigned.get(node))
541
+ return found
542
+
543
+
544
+ def _keeps(store: _Store | None, annotation: str, known: Known) -> bool:
545
+ """Check whether a store of an attribute keeps it the type `annotation`.
546
+
547
+ Returns:
548
+ Whether its value's own text types it so (see `member_type`), or (augmented) the operator
549
+ gives that type back.
550
+
551
+ """
552
+ if store is None:
553
+ return False
554
+ own: str | None = _of(store[1], annotation, known)
555
+ if store[0] is None:
556
+ return own == annotation
557
+ given: ast.BinOp = ast.BinOp(store[1], store[0], store[1])
558
+ return own is not None and operated(given, annotation, own, MAX_LENGTH) == annotation
559
+
560
+
561
+ def _typed(
562
+ body: Sequence[ast.stmt],
563
+ kept: frozenset[str],
564
+ stored: Mapping[str, Sequence[_Store | None]],
565
+ typing: tuple[Mapping[str, str], Known],
566
+ ) -> dict[str, str]:
567
+ """Type a class body's variables: each bound once there, to a value its text types.
568
+
569
+ `kept`: the names left alone (a builtin base's own); `stored`: how the module stores each
570
+ attribute (see `_stores`), which leaves one alone unless every store keeps its type; `typing`:
571
+ the types a base declares some names (see `HELD`), each its variable's where the value is of
572
+ it, and what the module names of the standard library (see `library`).
573
+
574
+ Returns:
575
+ Their types, by name.
576
+
577
+ """
578
+ bound: dict[str, int] = {}
579
+ stmt: ast.stmt
580
+ name: str
581
+ for stmt in body:
582
+ for name in _bound(stmt):
583
+ bound[name] = bound.get(name, 0) + 1
584
+ found: dict[str, str] = {}
585
+ value: ast.expr
586
+ annotation: str | None
587
+ for stmt in body:
588
+ match stmt:
589
+ case ast.Assign(targets=[ast.Name(id=name)], value=value) if (
590
+ bound[name] == 1
591
+ and name not in kept
592
+ and (annotation := _of(value, typing[0].get(name), typing[1])) is not None
593
+ and all(_keeps(store, annotation, typing[1]) for store in stored.get(name, ()))
594
+ ):
595
+ found[name] = annotation
596
+ case _:
597
+ pass
598
+ return found
599
+
600
+
601
+ def _of(value: ast.expr, held: str | None, known: Known) -> str | None:
602
+ """Type a variable by its `value`, under a base that declares it `held` (`None`: none does).
603
+
604
+ Returns:
605
+ `held`, where its value is of it, `None` too (`maxDiff = None`); its value's own type (see
606
+ `member_type`) where no base declares it; else `None`.
607
+
608
+ """
609
+ none: bool = isinstance(value, ast.Constant) and value.value is None
610
+ own: str | None = _NONE if none and held is not None else member_type(value, known)
611
+ if own is None or held is None:
612
+ return own
613
+ return held if own in (union(held) or ()) else None
614
+
615
+
616
+ def _bound(stmt: ast.stmt) -> list[str]:
617
+ """Name what one statement of a class's body binds: a definition's name, or every name it stores.
618
+
619
+ Returns:
620
+ Them, each as many times as it's bound.
621
+
622
+ """
623
+ if isinstance(stmt, _DEFINITIONS):
624
+ return [stmt.name]
625
+ return [
626
+ node.id
627
+ for node in ast.walk(stmt)
628
+ if isinstance(node, ast.Name) and not isinstance(node.ctx, ast.Load)
629
+ ]