python-constricter 0.4.1__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- constricter/__init__.py +55 -0
- constricter/__main__.py +26 -0
- constricter/_version.py +4 -0
- constricter/cli/__init__.py +2 -0
- constricter/cli/baseline.py +107 -0
- constricter/cli/collecting.py +54 -0
- constricter/cli/command.py +547 -0
- constricter/cli/config.py +322 -0
- constricter/cli/explain.py +104 -0
- constricter/cli/lsp/__init__.py +2 -0
- constricter/cli/lsp/connection.py +475 -0
- constricter/cli/lsp/edits.py +188 -0
- constricter/cli/lsp/guard.py +133 -0
- constricter/cli/lsp/hints.py +391 -0
- constricter/cli/lsp/protocol.py +192 -0
- constricter/cli/lsp/remembered.py +81 -0
- constricter/cli/lsp/stored.py +200 -0
- constricter/cli/options.py +799 -0
- constricter/cli/paths.py +151 -0
- constricter/cli/reading.py +36 -0
- constricter/cli/report.py +397 -0
- constricter/cli/rounds.py +328 -0
- constricter/cli/runs.py +90 -0
- constricter/cli/schedule.py +463 -0
- constricter/cli/traced.py +401 -0
- constricter/cli/workers.py +286 -0
- constricter/empty.py +66 -0
- constricter/fix/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp314-win_amd64.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp314-win_amd64.pyd +0 -0
- constricter/fix/libraries/walks.py +49 -0
- constricter/fix/tables/aliases.json +147 -0
- constricter/fix/tables/attributes.json +700 -0
- constricter/fix/tables/awaited.json +69 -0
- constricter/fix/tables/awaited_overloads.json +14 -0
- constricter/fix/tables/bases.json +888 -0
- constricter/fix/tables/classes.json +1941 -0
- constricter/fix/tables/elements.json +156 -0
- constricter/fix/tables/functions.json +58 -0
- constricter/fix/tables/generic_attributes.json +254 -0
- constricter/fix/tables/method_overloads.json +461 -0
- constricter/fix/tables/method_signatures.json +954 -0
- constricter/fix/tables/methods.json +954 -0
- constricter/fix/tables/overloads.json +540 -0
- constricter/fix/tables/platform.json +37 -0
- constricter/fix/tables/returns.json +1362 -0
- constricter/fix/tables/scalar_members.json +16 -0
- constricter/fix/tables/scalars.json +2707 -0
- constricter/fix/tables/source.json +28 -0
- constricter/fix/tables/subscriptable.json +77 -0
- constricter/fix/tables/type_parameters.json +178 -0
- constricter/fix/tables/variables.json +2962 -0
- constricter/fix/values/__init__.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp314-win_amd64.pyd +0 -0
- constricter/fix/values/unchecked.py +278 -0
- constricter/jsonc.py +50 -0
- constricter/noqa.py +47 -0
- constricter/notebook.py +115 -0
- constricter/offences.py +436 -0
- constricter/overrides.py +24 -0
- constricter/plugins/__init__.py +2 -0
- constricter/plugins/flake8.py +133 -0
- constricter/plugins/pylint.py +186 -0
- constricter/py.typed +0 -0
- constricter/recording.py +660 -0
- constricter/rules/__init__.cp314-win_amd64.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp314-win_amd64.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp314-win_amd64.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp314-win_amd64.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp314-win_amd64.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp314-win_amd64.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp314-win_amd64.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp314-win_amd64.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp314-win_amd64.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp314-win_amd64.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp314-win_amd64.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp314-win_amd64.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp314-win_amd64.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp314-win_amd64.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp314-win_amd64.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp314-win_amd64.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp314-win_amd64.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp314-win_amd64.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp314-win_amd64.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp314-win_amd64.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp314-win_amd64.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp314-win_amd64.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp314-win_amd64.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp314-win_amd64.pyd +0 -0
- python_constricter-0.4.1.dist-info/METADATA +400 -0
- python_constricter-0.4.1.dist-info/RECORD +261 -0
- python_constricter-0.4.1.dist-info/WHEEL +4 -0
- python_constricter-0.4.1.dist-info/entry_points.txt +5 -0
- python_constricter-0.4.1.dist-info/licenses/LICENSE.md +21 -0
constricter/recording.py
ADDED
|
@@ -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)
|
|
Binary file
|
|
Binary file
|