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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""What crosses into a file from the other checked files it imports, for `--fix`.
|
|
3
|
+
|
|
4
|
+
`imported` gives a file the return types of the functions it imports (see `project.calls`), and
|
|
5
|
+
each imported class's attributes and methods' returns, its plain classes' variables, and which are
|
|
6
|
+
generic, each keyed as the file spells the class (`spelled_classes`): only where every name in a
|
|
7
|
+
type means the same in the file as where it was written (see `project.portable`).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterator, Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import NamedTuple, TypeAlias
|
|
13
|
+
|
|
14
|
+
from constricter.fix.core.known import Classes, Guarded, Origin, Partial, Returns
|
|
15
|
+
from constricter.fix.index.modules import (
|
|
16
|
+
SUFFIX,
|
|
17
|
+
Index,
|
|
18
|
+
Memo,
|
|
19
|
+
Module,
|
|
20
|
+
classes_under,
|
|
21
|
+
exported,
|
|
22
|
+
module_name,
|
|
23
|
+
)
|
|
24
|
+
from constricter.fix.index.project import (
|
|
25
|
+
BUILTINS,
|
|
26
|
+
CLASS,
|
|
27
|
+
HOPS,
|
|
28
|
+
NO_TYPE,
|
|
29
|
+
PARTIAL,
|
|
30
|
+
Imported,
|
|
31
|
+
calls,
|
|
32
|
+
definition,
|
|
33
|
+
is_generic,
|
|
34
|
+
portable,
|
|
35
|
+
resolved,
|
|
36
|
+
returned,
|
|
37
|
+
typed_calls,
|
|
38
|
+
)
|
|
39
|
+
from constricter.rules.annotations import roots
|
|
40
|
+
|
|
41
|
+
_Named: TypeAlias = tuple[Mapping[str, Origin], frozenset[str] | None] # see `spelled_classes`
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def imported(
|
|
45
|
+
catalog: Index,
|
|
46
|
+
path: Path,
|
|
47
|
+
seeded: tuple[Mapping[str, Guarded], frozenset[str]] | None = None,
|
|
48
|
+
) -> Imported:
|
|
49
|
+
"""Return, for the file at `path`, what it imports from other checked files that it can name.
|
|
50
|
+
|
|
51
|
+
Each function's return type (`calls`), and each class's attributes and methods' returns, keyed as
|
|
52
|
+
the file spells the class (`Row`, `m.Row`); a type naming the class itself (a `Self` return) is
|
|
53
|
+
spelled that way too. `seeded`: the names its fixtures' types are written with, imported for type
|
|
54
|
+
checking alone (see `fixtures.visible`), whose classes' members it takes as well; and the
|
|
55
|
+
attributes those types take of a module it imports (`MonkeyPatch`, in `pytest.MonkeyPatch`).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Them, and the names their types need imported for type checking alone (see `Guarded`); nothing
|
|
59
|
+
for a file `catalog` doesn't have (a notebook, standard input).
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
taken: _Taken = _Taken({}, {}, {}, {}, set())
|
|
63
|
+
guarded: dict[str, Guarded] = {} if seeded is None else dict(seeded[0])
|
|
64
|
+
target: Module | None
|
|
65
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
|
|
66
|
+
return Imported({}, Classes(taken.attributes, taken.methods))
|
|
67
|
+
# Its own attributes as they are where the fixtures' add none: the index keeps what they find.
|
|
68
|
+
attributes: frozenset[str] = (
|
|
69
|
+
target.attributes
|
|
70
|
+
if seeded is None or seeded[1] <= target.attributes
|
|
71
|
+
else target.attributes | seeded[1]
|
|
72
|
+
)
|
|
73
|
+
checking: dict[str, Origin] = {
|
|
74
|
+
name: found.origin for name, found in guarded.items() if name not in target.names
|
|
75
|
+
}
|
|
76
|
+
named: _Named = ({**checking, **target.names}, attributes)
|
|
77
|
+
_take(catalog, target, (checking, attributes), taken, guarded)
|
|
78
|
+
_take(catalog, target, (target.names, attributes), taken, guarded)
|
|
79
|
+
found: tuple[dict[str, str], Returns, dict[str, str]] = (
|
|
80
|
+
calls(catalog, path, guarded),
|
|
81
|
+
returned(catalog, path, guarded),
|
|
82
|
+
{name: each for name, each, _ in typed_calls(catalog, path, PARTIAL, guarded)},
|
|
83
|
+
)
|
|
84
|
+
known: set[str] = set(named[0])
|
|
85
|
+
for _ in range(HOPS): # a class those types need imported is a receiver's too, needing more in turn
|
|
86
|
+
fresh: dict[str, Origin] = {name: each.origin for name, each in guarded.items() if name not in known}
|
|
87
|
+
# Any class their packages re-export: the types naming them aren't written in the file yet.
|
|
88
|
+
_take(catalog, target, (fresh, None), taken, guarded)
|
|
89
|
+
known.update(fresh)
|
|
90
|
+
return Imported(
|
|
91
|
+
found[0],
|
|
92
|
+
Classes(taken.attributes, taken.methods),
|
|
93
|
+
found[1],
|
|
94
|
+
guarded,
|
|
95
|
+
frozenset(taken.generics),
|
|
96
|
+
taken.members,
|
|
97
|
+
Partial(found[2], {name: each for name, each in taken.partial.items() if each}),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def guarded_classes(catalog: Index, path: Path, found: Imported, guarded: Mapping[str, Guarded]) -> Classes:
|
|
102
|
+
"""Add the members of the classes `guarded` names that `found` hasn't taken, as the file names them.
|
|
103
|
+
|
|
104
|
+
Those of the imports later steps add for type checking (an overloaded method's return):
|
|
105
|
+
`df = pd.DataFrame(...).set_index("C")`, typed `DataFrame` by its import: `df.explode()` is one of
|
|
106
|
+
that class's methods in the same pass, not only once the import is written.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
`found`'s classes, with theirs.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
target: Module | None
|
|
113
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(module_name(path))) is None:
|
|
114
|
+
return found.classes
|
|
115
|
+
fresh: dict[str, Origin] = {
|
|
116
|
+
name: each.origin
|
|
117
|
+
for name, each in guarded.items()
|
|
118
|
+
if name not in found.guarded and name not in target.names and name not in found.classes.methods
|
|
119
|
+
}
|
|
120
|
+
if not fresh:
|
|
121
|
+
return found.classes
|
|
122
|
+
taken: _Taken = _Taken(
|
|
123
|
+
{key: dict(each) for key, each in found.classes.attributes.items()},
|
|
124
|
+
{key: dict(each) for key, each in found.classes.methods.items()},
|
|
125
|
+
{},
|
|
126
|
+
{},
|
|
127
|
+
set(),
|
|
128
|
+
)
|
|
129
|
+
_take(catalog, target, (fresh, None), taken, dict(guarded))
|
|
130
|
+
return Classes(taken.attributes, taken.methods)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _Taken(NamedTuple):
|
|
134
|
+
"""What `imported` takes of the classes a file names, each keyed as the file spells the class."""
|
|
135
|
+
|
|
136
|
+
attributes: dict[str, dict[str, str]]
|
|
137
|
+
methods: dict[str, dict[str, str]]
|
|
138
|
+
partial: dict[str, dict[str, str]]
|
|
139
|
+
members: dict[str, Mapping[str, str]]
|
|
140
|
+
generics: set[str]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _take(catalog: Index, target: Module, named: _Named, taken: _Taken, guarded: dict[str, Guarded]) -> None:
|
|
144
|
+
"""Add to `taken` the members `target` uses of the classes `named` finds (see `spelled_classes`)."""
|
|
145
|
+
key: str
|
|
146
|
+
defined: tuple[Module, str]
|
|
147
|
+
for key, defined in spelled_classes(catalog, target, named, taken.generics):
|
|
148
|
+
if defined[1] in defined[0].generics:
|
|
149
|
+
taken.generics.add(key)
|
|
150
|
+
if defined[1] in defined[0].plain and defined[1] in defined[0].members:
|
|
151
|
+
taken.members[key] = _variables(catalog.modules, (target, defined), key, guarded)
|
|
152
|
+
if _declared_members(catalog, defined).isdisjoint(target.attributes):
|
|
153
|
+
# Most classes it can name: it takes none of their members, and asks whether it names one
|
|
154
|
+
# only by what it writes (`m.Row(...)` constructs one).
|
|
155
|
+
if _written(key, target.attributes):
|
|
156
|
+
taken.attributes[key], taken.methods[key], taken.partial[key] = {}, {}, {}
|
|
157
|
+
continue
|
|
158
|
+
taken.attributes[key], taken.methods[key], taken.partial[key] = (
|
|
159
|
+
_used(catalog.modules, (target, defined), key, table.get(defined[1]), guarded)
|
|
160
|
+
for table in (defined[0].classes, defined[0].methods, defined[0].partial_methods)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _written(spelled: str, attributes: frozenset[str]) -> bool:
|
|
165
|
+
"""Check whether a file can write a class as `spelled`: each part after its first an attribute it takes.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Whether it can.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
return all(part in attributes for part in spelled.split(".")[1:])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _variables(
|
|
175
|
+
modules: Mapping[str, Module],
|
|
176
|
+
where: tuple[Module, tuple[Module, str]],
|
|
177
|
+
key: str,
|
|
178
|
+
guarded: dict[str, Guarded],
|
|
179
|
+
) -> dict[str, str]:
|
|
180
|
+
"""Write a plain class's variables' types as the file (`where[0]`) can (see `portable`).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Each one's type: its own, made of builtins alone; one naming anything else, as the file
|
|
184
|
+
spells it, or `NO_TYPE` where it can't, which still holds a class under it to no other.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
typed: Mapping[str, str] = where[1][0].members[where[1][1]]
|
|
188
|
+
named: dict[str, str] = {name: text for name, text in typed.items() if not roots(text) <= BUILTINS}
|
|
189
|
+
written: dict[str, str] = portable(modules, where, key, named, guarded) if named else {}
|
|
190
|
+
return {name: written.get(name, NO_TYPE) if name in named else text for name, text in typed.items()}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _declared_members(catalog: Index, defined: tuple[Module, str]) -> frozenset[str]:
|
|
194
|
+
"""Name the members a class declares a type for, once for the index.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Its attributes' and methods' names.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
module: Module
|
|
201
|
+
name: str
|
|
202
|
+
module, name = defined
|
|
203
|
+
memo: Memo | None = catalog.memo
|
|
204
|
+
found: frozenset[str] | None
|
|
205
|
+
if (found := None if memo is None else memo.members.get((module.name, name))) is None:
|
|
206
|
+
found = frozenset[str]().union(
|
|
207
|
+
*(table.get(name, ()) for table in (module.classes, module.methods, module.partial_methods)),
|
|
208
|
+
)
|
|
209
|
+
if memo is not None:
|
|
210
|
+
memo.members[module.name, name] = found
|
|
211
|
+
return found
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _used(
|
|
215
|
+
modules: Mapping[str, Module],
|
|
216
|
+
where: tuple[Module, tuple[Module, str]],
|
|
217
|
+
key: str,
|
|
218
|
+
members: Mapping[str, str] | None,
|
|
219
|
+
guarded: dict[str, Guarded],
|
|
220
|
+
) -> dict[str, str]:
|
|
221
|
+
"""Keep the types of a class's `members` that the file (`where[0]`) takes of something, and can write.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Each kept member's type (see `portable`): none for most classes it can name, whose members it
|
|
225
|
+
doesn't use.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
used: frozenset[str] = where[0].attributes
|
|
229
|
+
taken: dict[str, str] = {name: typed for name, typed in (members or {}).items() if name in used}
|
|
230
|
+
return portable(modules, where, key, taken, guarded) if taken else {}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def spelled_classes(
|
|
234
|
+
catalog: Index,
|
|
235
|
+
target: Module,
|
|
236
|
+
named: _Named,
|
|
237
|
+
generics: set[str],
|
|
238
|
+
) -> Iterator[tuple[str, tuple[Module, str]]]:
|
|
239
|
+
"""Find the classes other checked files define that `target` names, each as it spells it.
|
|
240
|
+
|
|
241
|
+
`named`: the names it's to find them by, and the attributes it takes of anything (`None`: any).
|
|
242
|
+
`Row` after `from m import Row`; `m.Row`, or `pkg.m.Row` after `import pkg.m`, for a module's,
|
|
243
|
+
and `pkg.Row` for one `pkg` re-exports, where `Row` is such an attribute.
|
|
244
|
+
The generic classes a module it imports re-exports are added to `generics`, as it spells them.
|
|
245
|
+
|
|
246
|
+
Yields:
|
|
247
|
+
Each spelling, and the module defining the class and its name there.
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
memo: Memo | None = catalog.memo
|
|
251
|
+
if memo is None or named[0] is not target.names or named[1] is not target.attributes:
|
|
252
|
+
yield from _spelled_classes(catalog, target, named, generics)
|
|
253
|
+
return
|
|
254
|
+
kept: tuple[str, str] # with its folder: several `conftest.py`s share the one name
|
|
255
|
+
if (kept := (target.name, target.folder)) not in memo.spelled:
|
|
256
|
+
added: set[str] = set()
|
|
257
|
+
memo.spelled[kept] = (
|
|
258
|
+
[
|
|
259
|
+
(key, defined[0].name, defined[1])
|
|
260
|
+
for key, defined in _spelled_classes(catalog, target, named, added)
|
|
261
|
+
],
|
|
262
|
+
sorted(added),
|
|
263
|
+
)
|
|
264
|
+
generics.update(memo.spelled[kept][1])
|
|
265
|
+
key: str
|
|
266
|
+
module: str
|
|
267
|
+
name: str
|
|
268
|
+
for key, module, name in memo.spelled[kept][0]:
|
|
269
|
+
yield key, (catalog.modules[module], name)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _spelled_classes(
|
|
273
|
+
catalog: Index,
|
|
274
|
+
target: Module,
|
|
275
|
+
named: _Named,
|
|
276
|
+
generics: set[str],
|
|
277
|
+
) -> Iterator[tuple[str, tuple[Module, str]]]:
|
|
278
|
+
"""Find the classes `spelled_classes` yields, from the index itself.
|
|
279
|
+
|
|
280
|
+
Yields:
|
|
281
|
+
Each spelling, and the module defining the class and its name there.
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
local: str
|
|
285
|
+
origin: Origin
|
|
286
|
+
for local, origin in resolved(catalog, named[0]):
|
|
287
|
+
spelled: list[tuple[str, Origin]] = []
|
|
288
|
+
if origin[1] is not None and origin[0] != target.name:
|
|
289
|
+
spelled = [(local, origin)]
|
|
290
|
+
generics.update([local] * is_generic(catalog.modules, origin))
|
|
291
|
+
elif origin[1] is None:
|
|
292
|
+
package: Module | None = catalog.modules.get(origin[0])
|
|
293
|
+
spelled = [
|
|
294
|
+
*classes_under(catalog, local, origin[0], CLASS),
|
|
295
|
+
*(
|
|
296
|
+
(f"{local}.{name}", where)
|
|
297
|
+
for name, where in (() if package is None else exported(package).items())
|
|
298
|
+
if (named[1] is None or name in named[1])
|
|
299
|
+
and where[1] is not None
|
|
300
|
+
and where[0] != origin[0]
|
|
301
|
+
),
|
|
302
|
+
]
|
|
303
|
+
generics.update(_reexported_generics(catalog.modules, local, origin[0]))
|
|
304
|
+
key: str
|
|
305
|
+
where: Origin
|
|
306
|
+
for key, where in spelled:
|
|
307
|
+
defined: tuple[Module, str] | None
|
|
308
|
+
if (defined := definition(catalog.modules, where, CLASS)) is not None:
|
|
309
|
+
yield key, defined
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _reexported_generics(modules: Mapping[str, Module], local: str, name: str) -> Iterator[str]:
|
|
313
|
+
"""Spell the generic classes module `name` re-exports (`from ._c import OrderedSet`) as `local.C`.
|
|
314
|
+
|
|
315
|
+
Only its own names, not its submodules': a package's `__init__` is where they're re-exported,
|
|
316
|
+
for type checking alone too, where it imports them only when they're asked for (pydantic's).
|
|
317
|
+
|
|
318
|
+
Yields:
|
|
319
|
+
Each spelling.
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
module: Module | None = modules.get(name)
|
|
323
|
+
reexported: str
|
|
324
|
+
origin: Origin
|
|
325
|
+
for reexported, origin in ({} if module is None else {**module.guarded, **module.names}).items():
|
|
326
|
+
if origin[1] is not None and origin[0] != name and is_generic(modules, origin):
|
|
327
|
+
yield f"{local}.{reexported}"
|
|
Binary file
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Returns under decorators another module defines: kept where it gives the function back.
|
|
3
|
+
|
|
4
|
+
A module alone vouches for the standard library's such decorators and its own (see
|
|
5
|
+
`constricter.rules.decorators`); with the index, for those it imports from a checked file or an
|
|
6
|
+
installed package too (`@set_module("pandas")`, declared `-> Callable[[F], F]`), once the type
|
|
7
|
+
variables their signatures name are found to be ones. A declared return is kept, and an unannotated
|
|
8
|
+
function's `return`s may type its calls (`unannotated`), as an undecorated one's do.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterator, Mapping
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from constricter.fix.index import project
|
|
17
|
+
from constricter.fix.index.modules import SUFFIX, Index, Module, module_name
|
|
18
|
+
from constricter.fix.index.project import DECORATOR
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from constricter.fix.core.known import Origin
|
|
22
|
+
from constricter.rules.decorators import Pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def passed(catalog: Index) -> Index:
|
|
26
|
+
"""Add to each module's functions' declared returns those under decorators the index vouches for.
|
|
27
|
+
|
|
28
|
+
Its classes' classmethods' and staticmethods' too; and its unannotated functions under such
|
|
29
|
+
decorators (`decorated`) join those a `return` could type (`unannotated`).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The index, with them among each module's `returns`, `sides` and `unannotated`, and named in
|
|
33
|
+
its `vouched` and `vouched_sides`.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
found: dict[str, Module] = {}
|
|
37
|
+
module: Module
|
|
38
|
+
for module in catalog.modules.values():
|
|
39
|
+
if not (module.held or module.held_sides or module.decorated):
|
|
40
|
+
continue
|
|
41
|
+
trusted: frozenset[str] = frozenset(_vouched(catalog, module))
|
|
42
|
+
more: dict[str, str] = {
|
|
43
|
+
name: held.returns for name, held in module.held.items() if trusted.issuperset(held.decorators)
|
|
44
|
+
}
|
|
45
|
+
sides: dict[tuple[str, str], str] = {
|
|
46
|
+
(owner, name): held.returns
|
|
47
|
+
for owner, methods in module.held_sides.items()
|
|
48
|
+
for name, held in methods.items()
|
|
49
|
+
if trusted.issuperset(held.decorators)
|
|
50
|
+
}
|
|
51
|
+
freed: frozenset[str] = frozenset(
|
|
52
|
+
name for name, spellings in module.decorated.items() if trusted.issuperset(spellings)
|
|
53
|
+
)
|
|
54
|
+
if more or sides or freed:
|
|
55
|
+
found[module.name] = replace(
|
|
56
|
+
module,
|
|
57
|
+
returns={**module.returns, **more},
|
|
58
|
+
vouched=frozenset(more),
|
|
59
|
+
sides=_with_sides(module.sides, sides),
|
|
60
|
+
vouched_sides=frozenset(sides),
|
|
61
|
+
unannotated=module.unannotated | freed,
|
|
62
|
+
)
|
|
63
|
+
return catalog._replace(modules={**catalog.modules, **found}) if found else catalog
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def vouched(catalog: Index, path: Path) -> frozenset[str]:
|
|
67
|
+
"""Spell the decorators the file at `path` uses that give a function back, by the index.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Each, as `decorators.spelled` reads its use; nothing for a file `catalog` doesn't have.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
module: Module | None
|
|
74
|
+
if path.suffix != SUFFIX or (module := catalog.modules.get(module_name(path))) is None:
|
|
75
|
+
return frozenset()
|
|
76
|
+
return frozenset(_vouched(catalog, module))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _with_sides(
|
|
80
|
+
sides: Mapping[str, Mapping[str, str]],
|
|
81
|
+
more: Mapping[tuple[str, str], str],
|
|
82
|
+
) -> dict[str, dict[str, str]]:
|
|
83
|
+
"""Add methods' returns (`more`, by class and name) to classes' (`sides`).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Each class's, with them.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
found: dict[str, dict[str, str]] = {owner: dict(methods) for owner, methods in sides.items()}
|
|
90
|
+
owner: str
|
|
91
|
+
name: str
|
|
92
|
+
annotation: str
|
|
93
|
+
for (owner, name), annotation in more.items():
|
|
94
|
+
found.setdefault(owner, {})[name] = annotation
|
|
95
|
+
return found
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _vouched(catalog: Index, module: Module) -> Iterator[str]:
|
|
99
|
+
"""Spell the decorators `module` uses that give a function back, by the index: its own, and imported.
|
|
100
|
+
|
|
101
|
+
Yields:
|
|
102
|
+
Each, as `decorators.spelled` reads its use.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
name: str
|
|
106
|
+
read: Pass
|
|
107
|
+
for name, read in module.passes.items():
|
|
108
|
+
if all(project.is_type_var(catalog.modules, module, word) for word in read.type_vars):
|
|
109
|
+
yield read.spelled(name)
|
|
110
|
+
key: str
|
|
111
|
+
origin: Origin
|
|
112
|
+
for key, origin in project.spellings(catalog, module, DECORATOR):
|
|
113
|
+
defined: tuple[Module, str] | None
|
|
114
|
+
if (defined := project.definition(catalog.modules, origin, DECORATOR)) is not None:
|
|
115
|
+
read = defined[0].passes[defined[1]]
|
|
116
|
+
if all(project.is_type_var(catalog.modules, defined[0], word) for word in read.type_vars):
|
|
117
|
+
yield read.spelled(key)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def own(catalog: Index, path: Path) -> dict[str, str]:
|
|
121
|
+
"""Return, for the file at `path`, its own functions' returns the index vouched for (see `passed`).
|
|
122
|
+
|
|
123
|
+
Its classes' classmethods' and staticmethods' too, as called on the class (`Box.make`), where
|
|
124
|
+
the class's name is the class throughout the file.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Each one's name and declared return; nothing for a file `catalog` doesn't have.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
module: Module | None
|
|
131
|
+
if path.suffix != SUFFIX or (module := catalog.modules.get(module_name(path))) is None:
|
|
132
|
+
return {}
|
|
133
|
+
return {
|
|
134
|
+
**{name: module.returns[name] for name in module.vouched},
|
|
135
|
+
**{
|
|
136
|
+
f"{owner}.{name}": module.sides[owner][name]
|
|
137
|
+
for owner, name in module.vouched_sides
|
|
138
|
+
if module.names.get(owner) == (module.name, owner) and owner not in module.shadowed
|
|
139
|
+
},
|
|
140
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Pytest fixtures' values, for the tests that take them as parameters.
|
|
3
|
+
|
|
4
|
+
A test (or a fixture) names the fixtures it wants as its parameters, which no call passes: pytest
|
|
5
|
+
finds each in the test's module, then in the `conftest.py` of each package above it, the nearest
|
|
6
|
+
first. A fixture's value is what its function returns, or yields: its declared return, or the one
|
|
7
|
+
its `return`s give (see `constricter.fix.values.returned`). A parameter so typed is a guess
|
|
8
|
+
(`fixture`): a plugin's fixture, or one a `conftest.py` out of the checked files defines, may be
|
|
9
|
+
the one pytest takes. A `conftest.py` in a directory that isn't a package is looked in next, by
|
|
10
|
+
where it is: the test's directory's, or its top package's, then each one's above it. Pytest's own
|
|
11
|
+
come last (`capsys`, `monkeypatch`, `caplog`), read from its installed modules where a checked file
|
|
12
|
+
imports `pytest`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Final
|
|
19
|
+
|
|
20
|
+
from constricter.fix.core.known import Guarded, Passed
|
|
21
|
+
from constricter.fix.index import project
|
|
22
|
+
from constricter.fix.index.modules import CONFTEST, SUFFIX, Index, Module
|
|
23
|
+
|
|
24
|
+
KIND: Final = "fixture" # the guessing mechanism (`FIX_KINDS`)
|
|
25
|
+
_DOT: Final = "."
|
|
26
|
+
_PYTEST: Final = "_pytest" # the package pytest defines its own fixtures in
|
|
27
|
+
# pytest's own fixtures whose value is a standard-library class's instance, each with its class:
|
|
28
|
+
# typed without the index, by an import the module has or one added (see `rules.calls`).
|
|
29
|
+
STDLIB_OWN: Final = {"tmp_path": "pathlib.Path"}
|
|
30
|
+
# What a generator fixture declares it returns: its value is the first argument.
|
|
31
|
+
_YIELDING: Final = frozenset({"Iterator", "Generator", "Iterable", "AsyncIterator", "AsyncGenerator"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def providers(catalog: Index, name: str) -> list[Module]:
|
|
35
|
+
"""Find the modules whose fixtures module `name`'s tests can take, the nearest first.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
It, then its packages' `conftest`s, those `catalog` has, then those outside any package (see
|
|
39
|
+
`_above`), then pytest's own modules.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
parts: list[str] = name.split(_DOT)
|
|
43
|
+
names: list[str] = [
|
|
44
|
+
name,
|
|
45
|
+
*(_DOT.join([*parts[:depth], CONFTEST]) for depth in range(len(parts) - 1, 0, -1)),
|
|
46
|
+
]
|
|
47
|
+
found: list[Module] = [catalog.modules[each] for each in dict.fromkeys(names) if each in catalog.modules]
|
|
48
|
+
target: Module | None = catalog.modules.get(name)
|
|
49
|
+
return [*found, *(each for each in _above(catalog, name) if each is not target), *_own(catalog)]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _above(catalog: Index, name: str) -> list[Module]:
|
|
53
|
+
"""Find the `conftest.py`s outside any package that module `name`'s tests take fixtures from.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Those in the directory of `name`'s file, or of its top package, and in each one above it, the
|
|
57
|
+
nearest first.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
target: Module | None = catalog.modules.get(name)
|
|
61
|
+
folder: str = "" if target is None else target.folder
|
|
62
|
+
top: Path = Path(folder).joinpath(*[".."] * name.count(_DOT)).resolve()
|
|
63
|
+
folders: list[str] = [str(each) for each in (top, *top.parents)] if folder else []
|
|
64
|
+
flat: Module | None = catalog.modules.get(CONFTEST)
|
|
65
|
+
if flat is not None and CONFTEST not in catalog.repeated: # the index's: it has what it returns
|
|
66
|
+
return [flat] if flat.folder in folders else []
|
|
67
|
+
return [catalog.conftests[each] for each in folders if each in catalog.conftests]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _own(catalog: Index) -> list[Module]:
|
|
71
|
+
"""Find pytest's modules that define its own fixtures, among the installed ones `catalog` has.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Them, by name.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
return [
|
|
78
|
+
module
|
|
79
|
+
for name, module in sorted(catalog.modules.items())
|
|
80
|
+
if module.installed and module.fixtures and name.partition(_DOT)[0] == _PYTEST
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def needed(catalog: Index, name: str) -> set[str]:
|
|
85
|
+
"""Name the other modules whose fixtures module `name` can take: checked before it, their types are known.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Them.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
return {module.name for module in providers(catalog, name) if module.fixtures} - {name}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def visible(catalog: Index, path: Path, guarded: dict[str, Guarded]) -> dict[str, Passed]:
|
|
95
|
+
"""Type the fixtures the file at `path` can take, as it can write each type (see `project.spelled_in`).
|
|
96
|
+
|
|
97
|
+
`guarded` records the names those types need imported for type checking.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Each typed fixture's value's type, and what it rests on, by its name; nothing for a file
|
|
101
|
+
`catalog` doesn't have.
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
target: Module | None
|
|
105
|
+
if path.suffix != SUFFIX or (target := catalog.modules.get(project.module_name(path))) is None:
|
|
106
|
+
return {}
|
|
107
|
+
if target.name in catalog.repeated: # another file's module, maybe: its `conftest.py`s aren't this one's
|
|
108
|
+
return {}
|
|
109
|
+
found: dict[str, Passed | None] = {}
|
|
110
|
+
module: Module
|
|
111
|
+
for module in providers(catalog, target.name):
|
|
112
|
+
fixture: str
|
|
113
|
+
# A nearer one of the name is the one taken; pytest's of a standard-library class is `STDLIB_OWN`'s.
|
|
114
|
+
skipped: set[str] = found.keys() | (STDLIB_OWN.keys() if module.installed else set())
|
|
115
|
+
for fixture in module.fixtures.keys() - skipped:
|
|
116
|
+
found[fixture] = _typed(catalog, target, module, fixture, guarded)
|
|
117
|
+
return {fixture: typed for fixture, typed in found.items() if typed is not None}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def attributes(seeds: Mapping[str, Passed]) -> frozenset[str]:
|
|
121
|
+
"""Name the attributes fixtures' types are written with: `MonkeyPatch`, in `pytest.MonkeyPatch`.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Them.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
return frozenset(
|
|
128
|
+
node.attr
|
|
129
|
+
for typed, _ in seeds.values()
|
|
130
|
+
for node in ast.walk(ast.parse(typed, mode="eval"))
|
|
131
|
+
if isinstance(node, ast.Attribute)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _typed(
|
|
136
|
+
catalog: Index,
|
|
137
|
+
target: Module,
|
|
138
|
+
module: Module,
|
|
139
|
+
fixture: str,
|
|
140
|
+
guarded: dict[str, Guarded],
|
|
141
|
+
) -> Passed | None:
|
|
142
|
+
"""Type one of `module`'s fixtures' value, as `target` can write it.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
The type, and what it rests on; or `None`.
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
declared: str | None = module.held[fixture].returns if fixture in module.held else None
|
|
149
|
+
annotation: str | None = declared or module.returned.calls.get(fixture)
|
|
150
|
+
if annotation is not None and module.fixtures[fixture]:
|
|
151
|
+
annotation = _yielded(annotation)
|
|
152
|
+
written: str | None = (
|
|
153
|
+
None if annotation is None else project.spelled_in(catalog, target, module, annotation, guarded)
|
|
154
|
+
)
|
|
155
|
+
if written is None:
|
|
156
|
+
return None
|
|
157
|
+
rests: frozenset[str] = frozenset() if declared else module.returned.guesses.get(fixture, frozenset())
|
|
158
|
+
return written, frozenset({KIND}) | rests
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _yielded(annotation: str) -> str | None:
|
|
162
|
+
"""Read what a generator's declared type says it yields: `Frame` of an `Iterator[Frame]`.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
It, or `None` for anything else.
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
head: ast.expr
|
|
169
|
+
inner: ast.expr
|
|
170
|
+
match ast.parse(annotation, mode="eval").body:
|
|
171
|
+
case ast.Subscript(value=ast.Name() | ast.Attribute() as head, slice=inner) if (
|
|
172
|
+
ast.unparse(head).rpartition(_DOT)[2] in _YIELDING
|
|
173
|
+
):
|
|
174
|
+
return ast.unparse(inner.elts[0] if isinstance(inner, ast.Tuple) else inner)
|
|
175
|
+
case _:
|
|
176
|
+
return None
|
|
Binary file
|