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.
- 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__.cp312-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp312-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp312-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp312-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp312-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp312-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp312-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp312-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp312-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp312-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp312-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp312-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp312-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp312-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp312-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp312-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp312-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp312-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp312-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp312-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp312-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp312-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp312-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp312-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp312-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp312-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp312-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp312-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp312-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp312-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp312-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp312-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp312-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp312-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp312-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp312-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp312-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp312-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp312-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp312-win32.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__.cp312-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp312-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp312-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp312-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp312-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp312-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp312-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp312-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp312-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp312-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp312-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp312-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp312-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp312-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp312-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp312-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp312-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp312-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp312-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp312-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp312-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp312-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp312-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp312-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp312-win32.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__.cp312-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp312-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp312-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp312-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp312-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp312-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp312-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp312-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp312-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp312-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp312-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp312-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp312-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp312-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp312-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp312-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp312-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp312-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp312-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp312-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp312-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp312-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp312-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp312-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp312-win32.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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--infer-from`: the types a traced run saw (`python -m constricter.trace`), as a type checker's hints are.
|
|
3
|
+
|
|
4
|
+
A trace holds, for each file by its SHA-256, the types each binding of its functions' locals held
|
|
5
|
+
(see `constricter.recording`). `hints` turns a file's into `Hints`, which `--fix` judges as it does a
|
|
6
|
+
checker's (`constricter.fix.values.hinted`): each a guess, of kind `traced`. Only for a local no
|
|
7
|
+
annotation types: one an assignment or a `for` loop binds to a value rooted at a parameter no
|
|
8
|
+
checker types (`row = rows[0].load()`, `for row in rows:`; see `_unannotated`), or at an attribute
|
|
9
|
+
of `self` its class stores nothing else in (see `_undeclared`). Anything else a type
|
|
10
|
+
checker may type wider than the run saw (an `X | None` that was never `None`, a base class, a
|
|
11
|
+
`TypedDict`), which the narrower annotation would make an error. A name bound more than once has
|
|
12
|
+
what all its bindings held, where each is such a binding the run reached. Up to three types seen
|
|
13
|
+
are their union, `None` last.
|
|
14
|
+
|
|
15
|
+
A class is shown bare (one defined in another as `Outer.Inner`), with the import that names it for
|
|
16
|
+
type checking alone (`Offered`), unless the file defines it: `constricter.fix.index.offers` then
|
|
17
|
+
takes only a class the index knows. One of a private module of the standard library has no hint.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
26
|
+
from functools import cache
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
29
|
+
|
|
30
|
+
from constricter import recording
|
|
31
|
+
from constricter.fix.core.known import Hints, Offered
|
|
32
|
+
from constricter.fix.values.hinted import TRACED
|
|
33
|
+
|
|
34
|
+
_LABEL: Final = "a traced run"
|
|
35
|
+
_SUFFIX: Final = ".py"
|
|
36
|
+
_MEMBERS: Final = 3 # how many types seen make a union
|
|
37
|
+
_CLASS: Final = re.compile(rf"([\w.]+){recording.SEPARATOR}([\w.]+)")
|
|
38
|
+
_STDLIB: Final = sys.stdlib_module_names
|
|
39
|
+
_SELF: Final = "self"
|
|
40
|
+
_TYPED: Final = frozenset({_SELF, "cls"}) # the parameters a class types, unannotated
|
|
41
|
+
_Function: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
|
|
42
|
+
# Each binding's spellings, by its name, by its line.
|
|
43
|
+
_Bindings: TypeAlias = Mapping[int, Mapping[str, tuple[str, ...]]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Trace(NamedTuple):
|
|
47
|
+
"""A trace file: each file's bindings by its SHA-256, and the file each module named in it is."""
|
|
48
|
+
|
|
49
|
+
files: Mapping[str, _Bindings]
|
|
50
|
+
modules: Mapping[str, str]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@cache
|
|
54
|
+
def load(path: Path) -> Trace:
|
|
55
|
+
"""Read the trace file at `path`, once.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
It.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: It can't be read, or isn't a trace this version reads.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
return _read(_table(cast("object", json.loads(path.read_text(encoding="utf-8")))))
|
|
66
|
+
except (OSError, ValueError) as error:
|
|
67
|
+
message: str = f"{path}: {error}"
|
|
68
|
+
raise ValueError(message) from error
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _read(found: Mapping[str, object]) -> Trace:
|
|
72
|
+
"""Read a trace file's contents.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The trace, without what isn't as the recorder writes it.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: It isn't a trace this version reads.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
if found.get("version") != recording.VERSION:
|
|
82
|
+
message: str = f"not a trace `python -m constricter.trace` version {recording.VERSION} wrote"
|
|
83
|
+
raise ValueError(message)
|
|
84
|
+
return Trace(
|
|
85
|
+
{
|
|
86
|
+
digest: {
|
|
87
|
+
int(line): {
|
|
88
|
+
name: tuple(cast("list[str]", types))
|
|
89
|
+
for name, types in _table(held).items()
|
|
90
|
+
if isinstance(types, list)
|
|
91
|
+
}
|
|
92
|
+
for line, held in _table(_table(entry).get("bindings")).items()
|
|
93
|
+
if line.isdecimal()
|
|
94
|
+
}
|
|
95
|
+
for digest, entry in _table(found.get("files")).items()
|
|
96
|
+
},
|
|
97
|
+
{name: digest for name, digest in _table(found.get("modules")).items() if isinstance(digest, str)},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _table(value: object) -> dict[str, object]:
|
|
102
|
+
return cast("dict[str, object]", value) if isinstance(value, dict) else {}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def merged(
|
|
106
|
+
hinted: Mapping[Path, tuple[Hints, ...]],
|
|
107
|
+
found: Trace,
|
|
108
|
+
paths: Iterable[Path],
|
|
109
|
+
) -> dict[Path, tuple[Hints, ...]]:
|
|
110
|
+
"""Add the trace's hints for each of `paths` after the type checkers' (`hinted`).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Each file's hints.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
every: dict[Path, tuple[Hints, ...]] = dict(hinted)
|
|
117
|
+
path: Path
|
|
118
|
+
for path in paths:
|
|
119
|
+
try:
|
|
120
|
+
traced: Hints | None = hints(found, path.read_bytes()) if path.suffix == _SUFFIX else None
|
|
121
|
+
except OSError: # checking it reports that
|
|
122
|
+
continue
|
|
123
|
+
if traced is not None:
|
|
124
|
+
every[path] = (*every.get(path, ()), traced)
|
|
125
|
+
return every
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def hints(found: Trace, source: bytes) -> Hints | None:
|
|
129
|
+
"""Make the hints a trace has for a file's `source`.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Them; `None` for a file the trace doesn't have as it is, or has nothing for.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
digest: str = hashlib.sha256(source).hexdigest()
|
|
136
|
+
bindings: _Bindings = found.files.get(digest, {})
|
|
137
|
+
try:
|
|
138
|
+
tree: ast.Module | None = ast.parse(source) if bindings else None
|
|
139
|
+
except (SyntaxError, ValueError):
|
|
140
|
+
tree = None
|
|
141
|
+
types: dict[tuple[int, int], str] = {}
|
|
142
|
+
offered: dict[tuple[int, int], Offered] = {}
|
|
143
|
+
stored: dict[int, frozenset[str]] = {} if tree is None else _undeclared(tree)
|
|
144
|
+
node: ast.AST
|
|
145
|
+
for node in ast.walk(tree) if tree is not None else ():
|
|
146
|
+
targets: list[ast.Name]
|
|
147
|
+
for targets in (
|
|
148
|
+
_untyped(node, stored.get(id(node), frozenset())).values() if isinstance(node, _Function) else ()
|
|
149
|
+
):
|
|
150
|
+
# A binding the run never reached may hold anything.
|
|
151
|
+
seen: list[tuple[str, ...]] = [
|
|
152
|
+
bindings.get(target.lineno, {}).get(target.id, ()) for target in targets
|
|
153
|
+
]
|
|
154
|
+
typed: Offered | None = (
|
|
155
|
+
_typed(sorted({spelling for each in seen for spelling in each}), found.modules, digest)
|
|
156
|
+
if all(seen)
|
|
157
|
+
else None
|
|
158
|
+
)
|
|
159
|
+
if typed is not None:
|
|
160
|
+
types.update({(target.lineno, target.end_col_offset or 0): typed.text for target in targets})
|
|
161
|
+
if typed is not None and typed.imports:
|
|
162
|
+
offered.update({(target.lineno, target.end_col_offset or 0): typed for target in targets})
|
|
163
|
+
return Hints(_LABEL, types, offered, TRACED) if types else None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _typed(spellings: Sequence[str], modules: Mapping[str, str], digest: str) -> Offered | None:
|
|
167
|
+
"""Join a local's `spellings` into the annotation they make, with the imports its classes need.
|
|
168
|
+
|
|
169
|
+
`modules`, `digest`: see `Trace.modules`, and the file's own SHA-256, whose classes need none.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
It, or `None`: nothing seen, something that couldn't be spelled, more than `_MEMBERS` types,
|
|
173
|
+
two classes of one name, or one of a private module of the standard library.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
if not spellings or recording.UNKNOWN in spellings or len(spellings) > _MEMBERS:
|
|
177
|
+
return None
|
|
178
|
+
origins: dict[str, str] = {}
|
|
179
|
+
spelling: str
|
|
180
|
+
for spelling in spellings:
|
|
181
|
+
module: str
|
|
182
|
+
name: str
|
|
183
|
+
for module, name in cast("list[tuple[str, str]]", _CLASS.findall(spelling)):
|
|
184
|
+
private: bool = module.partition(".")[0] in _STDLIB and any(
|
|
185
|
+
part.startswith("_") for part in module.split(".")
|
|
186
|
+
)
|
|
187
|
+
# A class defined in another is imported by the outer one's name.
|
|
188
|
+
if private or origins.setdefault(name.partition(".")[0], module) != module:
|
|
189
|
+
return None
|
|
190
|
+
return Offered(
|
|
191
|
+
" | ".join(
|
|
192
|
+
_CLASS.sub(r"\2", spelling)
|
|
193
|
+
for spelling in sorted(spellings, key=lambda text: (text == recording.NONE, text))
|
|
194
|
+
),
|
|
195
|
+
tuple(
|
|
196
|
+
f"from {module} import {name}"
|
|
197
|
+
for name, module in sorted(origins.items())
|
|
198
|
+
if modules.get(module) != digest
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _undeclared(tree: ast.Module) -> dict[int, frozenset[str]]:
|
|
204
|
+
"""Find the attributes of `self` no type checker types, in each method of a class with no base.
|
|
205
|
+
|
|
206
|
+
Those the class's methods store only by a plain `self.x = value`, each value rooted at a
|
|
207
|
+
parameter its method leaves unannotated, and the class's body doesn't bind: a checker types
|
|
208
|
+
one by what's stored in it, so has no type for it.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Them, by each such method's identity.
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
found: dict[int, frozenset[str]] = {}
|
|
215
|
+
node: ast.AST
|
|
216
|
+
for node in ast.walk(tree):
|
|
217
|
+
if not isinstance(node, ast.ClassDef) or node.bases or node.keywords:
|
|
218
|
+
continue
|
|
219
|
+
methods: list[_Function] = [
|
|
220
|
+
method
|
|
221
|
+
for method in node.body
|
|
222
|
+
if isinstance(method, _Function) and [arg.arg for arg in _params(method)][:1] == [_SELF]
|
|
223
|
+
]
|
|
224
|
+
bound: set[str] = {
|
|
225
|
+
each.name if isinstance(each, ast.ClassDef | _Function) else each.id
|
|
226
|
+
for statement in node.body
|
|
227
|
+
for each in (statement, *_own(statement))
|
|
228
|
+
if isinstance(each, ast.ClassDef | _Function)
|
|
229
|
+
or (isinstance(each, ast.Name) and isinstance(each.ctx, ast.Store))
|
|
230
|
+
}
|
|
231
|
+
plain: dict[str, bool] = {} # whether each attribute stored is only ever stored so
|
|
232
|
+
method: _Function
|
|
233
|
+
for method in methods:
|
|
234
|
+
rooted: set[str | None] = _unannotated(method)
|
|
235
|
+
taken: set[int] = {
|
|
236
|
+
id(statement.targets[0])
|
|
237
|
+
for statement in ast.walk(method)
|
|
238
|
+
if isinstance(statement, ast.Assign)
|
|
239
|
+
and len(statement.targets) == 1
|
|
240
|
+
and _root(statement.value)[0] in rooted
|
|
241
|
+
}
|
|
242
|
+
plain.update(
|
|
243
|
+
{
|
|
244
|
+
store.attr: plain.get(store.attr, True) and id(store) in taken
|
|
245
|
+
for store in ast.walk(method)
|
|
246
|
+
if isinstance(store, ast.Attribute)
|
|
247
|
+
and isinstance(store.ctx, ast.Store | ast.Del)
|
|
248
|
+
and isinstance(store.value, ast.Name)
|
|
249
|
+
and store.value.id == _SELF
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
attributes: frozenset[str] = frozenset(attr for attr, only in plain.items() if only) - bound
|
|
253
|
+
found.update({id(method): attributes for method in methods})
|
|
254
|
+
return found
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _params(function: _Function) -> list[ast.arg]:
|
|
258
|
+
"""List a function's parameters, in order.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Them.
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
args: ast.arguments = function.args
|
|
265
|
+
return [
|
|
266
|
+
arg
|
|
267
|
+
for arg in (*args.posonlyargs, *args.args, args.vararg, *args.kwonlyargs, args.kwarg)
|
|
268
|
+
if arg is not None
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _unannotated(function: _Function) -> set[str | None]:
|
|
273
|
+
"""Name the parameters of a function no type checker has a type for.
|
|
274
|
+
|
|
275
|
+
The unannotated ones (not `self` or `cls`, which its class types) it gives no default but
|
|
276
|
+
`None` (a checker types one by its default), doesn't bind again, and doesn't test by a call
|
|
277
|
+
(`isinstance(rows, list)`, a `TypeGuard`'s) or match, which narrows one.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Them.
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
args: ast.arguments = function.args
|
|
284
|
+
last: list[ast.arg] = [*args.posonlyargs, *args.args][::-1][: len(args.defaults)][::-1]
|
|
285
|
+
defaults: list[tuple[ast.arg, ast.expr | None]] = [
|
|
286
|
+
*zip(last, args.defaults, strict=True),
|
|
287
|
+
*zip(args.kwonlyargs, args.kw_defaults, strict=True),
|
|
288
|
+
]
|
|
289
|
+
typed: set[str] = {
|
|
290
|
+
arg.arg
|
|
291
|
+
for arg, default in defaults
|
|
292
|
+
if default is not None and not (isinstance(default, ast.Constant) and default.value is None)
|
|
293
|
+
}
|
|
294
|
+
node: ast.AST
|
|
295
|
+
for node in _own(function):
|
|
296
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
297
|
+
typed.add(node.id)
|
|
298
|
+
elif isinstance(node, ast.Match) and isinstance(node.subject, ast.Name):
|
|
299
|
+
typed.add(node.subject.id)
|
|
300
|
+
elif isinstance(node, ast.If | ast.While | ast.IfExp | ast.Assert):
|
|
301
|
+
typed.update(
|
|
302
|
+
passed.id
|
|
303
|
+
for call in ast.walk(node.test)
|
|
304
|
+
if isinstance(call, ast.Call)
|
|
305
|
+
for passed in (*call.args, *(keyword.value for keyword in call.keywords))
|
|
306
|
+
if isinstance(passed, ast.Name)
|
|
307
|
+
)
|
|
308
|
+
return {arg.arg for arg in _params(function) if arg.annotation is None and arg.arg not in _TYPED | typed}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _untyped(function: _Function, stored: frozenset[str]) -> dict[str, list[ast.Name]]:
|
|
312
|
+
"""Find the locals `function` binds only to values no annotation types.
|
|
313
|
+
|
|
314
|
+
Each by a plain assignment or as a `for` loop's target, to a value rooted at a parameter it
|
|
315
|
+
leaves unannotated (`rows[0].load()`, `make(n)`; not `self` or `cls`, which its class types),
|
|
316
|
+
or at one of the attributes of `self` its class has `stored` so (`self.rows[0]`): a type checker
|
|
317
|
+
has no type for it either, so none to set against what the run saw. Not one rooted at such a
|
|
318
|
+
local, which a checker types by its new annotation.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Each such name's bindings.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
others: set[str] = {arg.arg for arg in _params(function)}
|
|
325
|
+
rooted: set[str | None] = _unannotated(function)
|
|
326
|
+
stores: list[ast.Name] = []
|
|
327
|
+
node: ast.AST
|
|
328
|
+
for node in _own(function):
|
|
329
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
330
|
+
stores.append(node)
|
|
331
|
+
elif isinstance(node, ast.Global | ast.Nonlocal):
|
|
332
|
+
others.update(node.names)
|
|
333
|
+
elif isinstance(node, ast.alias):
|
|
334
|
+
others.add((node.asname or node.name).partition(".")[0])
|
|
335
|
+
elif isinstance(node, ast.ExceptHandler | ast.MatchAs | ast.MatchStar) and node.name is not None:
|
|
336
|
+
others.add(node.name)
|
|
337
|
+
elif isinstance(node, ast.MatchMapping) and node.rest is not None:
|
|
338
|
+
others.add(node.rest)
|
|
339
|
+
targets: list[ast.Name] = [
|
|
340
|
+
target
|
|
341
|
+
for target, value in _assigned(function)
|
|
342
|
+
if _root(value)[0] in rooted or (_root(value)[0] == _SELF and _root(value)[1] in stored)
|
|
343
|
+
]
|
|
344
|
+
others.update(store.id for store in stores if all(store is not target for target in targets))
|
|
345
|
+
found: dict[str, list[ast.Name]] = {}
|
|
346
|
+
target: ast.Name
|
|
347
|
+
for target in targets:
|
|
348
|
+
if target.id not in others:
|
|
349
|
+
found.setdefault(target.id, []).append(target)
|
|
350
|
+
return found
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _assigned(function: _Function) -> Iterator[tuple[ast.Name, ast.expr]]:
|
|
354
|
+
"""Find what binds one name to one value in a function's own scope, in order.
|
|
355
|
+
|
|
356
|
+
Yields:
|
|
357
|
+
Each plain assignment's name and value, and each `for` loop's target and what it loops over.
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
node: ast.AST
|
|
361
|
+
for node in _own(function):
|
|
362
|
+
target: ast.expr | None = None
|
|
363
|
+
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
364
|
+
target = node.targets[0]
|
|
365
|
+
elif isinstance(node, ast.For | ast.AsyncFor):
|
|
366
|
+
target = node.target
|
|
367
|
+
if isinstance(node, ast.Assign | ast.For | ast.AsyncFor) and isinstance(target, ast.Name):
|
|
368
|
+
yield target, node.value if isinstance(node, ast.Assign) else node.iter
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _root(value: ast.expr, attr: str | None = None) -> tuple[str | None, str | None]:
|
|
372
|
+
"""Find the name a value is taken from: `rows` in `rows[0].load()`, `make` in `make(n)`.
|
|
373
|
+
|
|
374
|
+
`attr`: the attribute `value` is read for.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
It (`None` for a value that's computed some other way), and the attribute read of the name
|
|
378
|
+
itself, if one is: `rows` in `self.rows[0].load()`.
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
if isinstance(value, ast.Attribute):
|
|
382
|
+
return _root(value.value, value.attr)
|
|
383
|
+
if isinstance(value, ast.Subscript | ast.Await):
|
|
384
|
+
return _root(value.value)
|
|
385
|
+
if isinstance(value, ast.Call):
|
|
386
|
+
return _root(value.func)
|
|
387
|
+
return (value.id if isinstance(value, ast.Name) else None, attr)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _own(node: ast.AST) -> Iterator[ast.AST]:
|
|
391
|
+
"""Walk what a function's own scope holds: not what a function, class or comprehension inside it does.
|
|
392
|
+
|
|
393
|
+
Yields:
|
|
394
|
+
Each node under `node`, a nested scope's own node included.
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
child: ast.AST
|
|
398
|
+
for child in ast.iter_child_nodes(node):
|
|
399
|
+
yield child
|
|
400
|
+
if not isinstance(child, recording.SCOPES):
|
|
401
|
+
yield from _own(child)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Worker processes for `--jobs`: each indexes its own share of the files, then checks it."""
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
import copy
|
|
6
|
+
import enum
|
|
7
|
+
import gc
|
|
8
|
+
import importlib
|
|
9
|
+
import mmap
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Callable, Collection, Generator, Mapping, Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, BinaryIO, Final, Self, TypeAlias, cast
|
|
15
|
+
|
|
16
|
+
from constricter.cli import collecting
|
|
17
|
+
from constricter.cli.paths import shares
|
|
18
|
+
from constricter.cli.runs import FileRun
|
|
19
|
+
from constricter.fix.core.known import Hints, Outside, Returns
|
|
20
|
+
from constricter.fix.index import project
|
|
21
|
+
from constricter.rules import parsed
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING: # slow to import, and only needed for many files
|
|
24
|
+
from concurrent.futures import Future, ProcessPoolExecutor
|
|
25
|
+
from multiprocessing.context import DefaultContext
|
|
26
|
+
|
|
27
|
+
# A worker's answers, one per file of its share: what it read (to index), and what it found.
|
|
28
|
+
_Reading: TypeAlias = "Future[list[project.Module | None]]"
|
|
29
|
+
_FIRST_COMPLETED: Final = "FIRST_COMPLETED" # `concurrent.futures.FIRST_COMPLETED`
|
|
30
|
+
_Check: TypeAlias = Callable[[Path, Outside], FileRun]
|
|
31
|
+
File: TypeAlias = tuple[Path, tuple[Hints, ...]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Again(enum.Enum):
|
|
35
|
+
"""Which of a worker's files asked about again it checks: those whose last check knew otherwise."""
|
|
36
|
+
|
|
37
|
+
NO = "no" # each, whatever it was last checked knowing
|
|
38
|
+
RETURNED = "returned" # each whose imports return something else now
|
|
39
|
+
OUTSIDE = "outside" # each that knows anything else from outside now (its text as it was)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# A worker's files to check: each with its hints; what checked modules' functions return since it
|
|
43
|
+
# was last told; and which of them it checks.
|
|
44
|
+
Asked: TypeAlias = tuple[Sequence[File], Mapping[str, Returns], Again]
|
|
45
|
+
Answering: TypeAlias = "Future[list[FileRun | None]]" # a worker's answer for the files it was asked
|
|
46
|
+
_Wait: TypeAlias = Callable[..., tuple[set[object], set[object]]] # `concurrent.futures.wait`: done, and not
|
|
47
|
+
_FORKSERVER: Final = "forkserver" # Python 3.14's start method on Linux
|
|
48
|
+
_PRELOADED: Final = ["constricter.cli.command"] # what a fork server imports before forking a worker
|
|
49
|
+
_TOLD: Final = "told.pickle" # in the workers' directory: what they're all told, once (see `Workers.tell`)
|
|
50
|
+
Told: TypeAlias = bytes | mmap.mmap # what `Workers.tell` gives each worker: a file's contents, mapped
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Workers:
|
|
54
|
+
"""Worker processes, each with its own share of the files, which it indexes and then checks.
|
|
55
|
+
|
|
56
|
+
The trees a worker parsed to index its files are still there to check (`parsed.keep`), within its
|
|
57
|
+
part of the budget: sharing the files out anew for the check (a pool's way) would parse them again.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, paths: Sequence[Path], jobs: int) -> None:
|
|
61
|
+
"""Share `paths` out among up to `jobs` workers (see `paths.shares`)."""
|
|
62
|
+
self.paths: Sequence[Path] = paths
|
|
63
|
+
self.shares: list[list[int]] = shares(paths, jobs)
|
|
64
|
+
self.stack: contextlib.ExitStack = contextlib.ExitStack()
|
|
65
|
+
self.pools: list[ProcessPoolExecutor] = [] # started by `__enter__`
|
|
66
|
+
self.told: Path = Path() # in a directory of their own, made by `__enter__`
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> Self:
|
|
69
|
+
"""Start the workers, one process each, each with its part of the budget.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Them.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
_preload()
|
|
76
|
+
# Before the pools: they stop first, as the stack unwinds, and the directory goes after.
|
|
77
|
+
self.told = Path(self.stack.enter_context(tempfile.TemporaryDirectory(prefix="constricter-"))) / _TOLD
|
|
78
|
+
self.pools = [
|
|
79
|
+
self.stack.enter_context(
|
|
80
|
+
cast(
|
|
81
|
+
"type[ProcessPoolExecutor]",
|
|
82
|
+
importlib.import_module("concurrent.futures").ProcessPoolExecutor,
|
|
83
|
+
)(
|
|
84
|
+
max_workers=1,
|
|
85
|
+
initializer=_started,
|
|
86
|
+
initargs=(len(self.shares),),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
for _ in self.shares
|
|
90
|
+
]
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __exit__(self, *_details: object) -> None:
|
|
94
|
+
"""Stop the workers."""
|
|
95
|
+
self.stack.close()
|
|
96
|
+
|
|
97
|
+
def over(self, paths: Sequence[Path]) -> "Workers":
|
|
98
|
+
"""Ask these workers about `paths` alone, some of their files: each has what it had of them.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Them, each with its share of `paths`.
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
place: dict[Path, int] = {path: at for at, path in enumerate(paths)}
|
|
105
|
+
some: Workers = copy.copy(self)
|
|
106
|
+
some.paths = paths
|
|
107
|
+
some.shares = [
|
|
108
|
+
[place[self.paths[at]] for at in share if self.paths[at] in place] for share in self.shares
|
|
109
|
+
]
|
|
110
|
+
return some
|
|
111
|
+
|
|
112
|
+
def index(self) -> project.Index:
|
|
113
|
+
"""Index every file, each worker its share.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The index, as one process would build it.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
return project.indexed(self.read())
|
|
120
|
+
|
|
121
|
+
def read(self) -> list[project.Module | None]:
|
|
122
|
+
"""Read every file, each worker its share, keeping its tree to check (see `project.read`).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Each file's module, in their order, as one process reads them.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
read: list[_Reading] = [
|
|
129
|
+
pool.submit(_read_share, [self.paths[at] for at in share])
|
|
130
|
+
for pool, share in zip(self.pools, self.shares, strict=True)
|
|
131
|
+
]
|
|
132
|
+
found: dict[int, project.Module | None] = {}
|
|
133
|
+
share: list[int]
|
|
134
|
+
future: _Reading
|
|
135
|
+
for share, future in zip(self.shares, read, strict=True):
|
|
136
|
+
found.update(zip(share, future.result(), strict=True))
|
|
137
|
+
return [found[at] for at in range(len(self.paths))]
|
|
138
|
+
|
|
139
|
+
def tell(self, function: Callable[[Told], None], told: bytes) -> None:
|
|
140
|
+
"""Run `function(told)` in every worker, and wait for them all: what each is to know from then on.
|
|
141
|
+
|
|
142
|
+
`told` is written once, to a file each worker maps (`_mapped`): a pool would pickle it again
|
|
143
|
+
on its way down each worker's pipe, every copy held at once.
|
|
144
|
+
"""
|
|
145
|
+
_ = self.told.write_bytes(told)
|
|
146
|
+
telling: list[Future[None]] = [pool.submit(_mapped, function, self.told) for pool in self.pools]
|
|
147
|
+
each: Future[None]
|
|
148
|
+
for each in telling:
|
|
149
|
+
each.result()
|
|
150
|
+
|
|
151
|
+
def ask(
|
|
152
|
+
self,
|
|
153
|
+
worker: int,
|
|
154
|
+
function: Callable[[_Check, Asked], list[FileRun | None]],
|
|
155
|
+
check: _Check,
|
|
156
|
+
asked: Asked,
|
|
157
|
+
) -> Answering:
|
|
158
|
+
"""Start `function(check, asked)` in one worker: its files to check, and what it's to know for them.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The answer it will give.
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
return self.pools[worker].submit(function, check, asked)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class _Kept:
|
|
169
|
+
"""A run's workers, started for its first round and asked again in each later one."""
|
|
170
|
+
|
|
171
|
+
stack: contextlib.ExitStack | None = None
|
|
172
|
+
workers: Workers | None = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
_KEPT: Final = _Kept()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@contextlib.contextmanager
|
|
179
|
+
def kept() -> Generator[None]:
|
|
180
|
+
"""Keep the workers `started` starts until the block ends: a run's rounds share them.
|
|
181
|
+
|
|
182
|
+
A file's second check is then by the process that has its tree and how its first check ended,
|
|
183
|
+
where new processes would parse it and check it whole.
|
|
184
|
+
"""
|
|
185
|
+
stack: contextlib.ExitStack
|
|
186
|
+
with contextlib.ExitStack() as stack:
|
|
187
|
+
_KEPT.stack = stack
|
|
188
|
+
try:
|
|
189
|
+
yield
|
|
190
|
+
finally:
|
|
191
|
+
_KEPT.stack = _KEPT.workers = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def running() -> bool:
|
|
195
|
+
"""Check whether the run's workers (see `kept`) have started: its later rounds are theirs to check.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Whether they have.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
return _KEPT.workers is not None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def started(paths: Sequence[Path], jobs: int) -> Workers:
|
|
205
|
+
"""Start workers for `paths`, or ask the run's (see `kept`) about these of their files.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The workers.
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
if _KEPT.workers is not None:
|
|
212
|
+
return _KEPT.workers.over(paths)
|
|
213
|
+
_KEPT.workers = (_KEPT.stack or contextlib.ExitStack()).enter_context(Workers(paths, jobs))
|
|
214
|
+
return _KEPT.workers
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def first_done(pending: Collection[Answering]) -> set[Answering]:
|
|
218
|
+
"""Wait for any of `pending` to finish.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Those that have.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
wait: _Wait = cast("_Wait", importlib.import_module("concurrent.futures").wait)
|
|
225
|
+
return cast("set[Answering]", wait(pending, return_when=_FIRST_COMPLETED)[0])
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _preload() -> None:
|
|
229
|
+
"""Have a fork server import constricter, its tables read, before it forks a worker.
|
|
230
|
+
|
|
231
|
+
Its own default imports a script's `__main__` alone: under `python -m constricter` each worker
|
|
232
|
+
would import constricter itself as it starts. Once the server runs, this changes nothing; a
|
|
233
|
+
forked worker has the parent's imports, and a spawned one imports its own whatever this says.
|
|
234
|
+
"""
|
|
235
|
+
get_context: Callable[[], DefaultContext] = cast(
|
|
236
|
+
"Callable[[], DefaultContext]",
|
|
237
|
+
importlib.import_module("multiprocessing").get_context,
|
|
238
|
+
)
|
|
239
|
+
context: DefaultContext = get_context()
|
|
240
|
+
if context.get_start_method() == _FORKSERVER:
|
|
241
|
+
context.set_forkserver_preload(_PRELOADED)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _mapped(function: Callable[[Told], None], told: Path) -> None:
|
|
245
|
+
"""Run `function` on the contents of the file `told`, mapped, in a worker (see `Workers.tell`)."""
|
|
246
|
+
file: BinaryIO
|
|
247
|
+
mapped: mmap.mmap
|
|
248
|
+
with told.open("rb") as file, mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as mapped:
|
|
249
|
+
function(mapped)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _started(processes: int) -> None:
|
|
253
|
+
"""Start a worker: its share of the kept trees' budget, and the collector off (see `collecting`)."""
|
|
254
|
+
parsed.budget(processes)
|
|
255
|
+
gc.disable()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _read_share(paths: Sequence[Path]) -> list[project.Module | None]:
|
|
259
|
+
"""Index a worker's share of the files (see `project.read`).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Each file's module, or `None`.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
found: list[project.Module | None] = [project.read(path) for path in paths]
|
|
266
|
+
collecting.indexed()
|
|
267
|
+
return found
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def check_share(
|
|
271
|
+
check: Callable[[Path, Outside], FileRun],
|
|
272
|
+
files: Sequence[tuple[Path, Outside]],
|
|
273
|
+
) -> list[FileRun]:
|
|
274
|
+
"""Check a share of the files (a worker's, or all of them in one process).
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
What each found.
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
found: list[FileRun] = []
|
|
281
|
+
path: Path
|
|
282
|
+
outside: Outside
|
|
283
|
+
for path, outside in files:
|
|
284
|
+
found.append(check(path, outside))
|
|
285
|
+
collecting.sweep()
|
|
286
|
+
return found
|