python-constricter 0.4.1__cp313-cp313-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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__.cp313-win32.pyd +0 -0
- constricter/fix/__init__.py +2 -0
- constricter/fix/core/__init__.cp313-win32.pyd +0 -0
- constricter/fix/core/__init__.py +2 -0
- constricter/fix/core/asked.cp313-win32.pyd +0 -0
- constricter/fix/core/asked.py +56 -0
- constricter/fix/core/fixes.cp313-win32.pyd +0 -0
- constricter/fix/core/fixes.py +317 -0
- constricter/fix/core/imports.cp313-win32.pyd +0 -0
- constricter/fix/core/imports.py +580 -0
- constricter/fix/core/inherited.cp313-win32.pyd +0 -0
- constricter/fix/core/inherited.py +311 -0
- constricter/fix/core/known.cp313-win32.pyd +0 -0
- constricter/fix/core/known.py +730 -0
- constricter/fix/core/signatures.cp313-win32.pyd +0 -0
- constricter/fix/core/signatures.py +132 -0
- constricter/fix/index/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/__init__.py +2 -0
- constricter/fix/index/awaits.cp313-win32.pyd +0 -0
- constricter/fix/index/awaits.py +90 -0
- constricter/fix/index/beyond.cp313-win32.pyd +0 -0
- constricter/fix/index/beyond.py +149 -0
- constricter/fix/index/callers.cp313-win32.pyd +0 -0
- constricter/fix/index/callers.py +155 -0
- constricter/fix/index/crossed.cp313-win32.pyd +0 -0
- constricter/fix/index/crossed.py +327 -0
- constricter/fix/index/decorated.cp313-win32.pyd +0 -0
- constricter/fix/index/decorated.py +140 -0
- constricter/fix/index/fixtures.cp313-win32.pyd +0 -0
- constricter/fix/index/fixtures.py +176 -0
- constricter/fix/index/linked.cp313-win32.pyd +0 -0
- constricter/fix/index/linked.py +142 -0
- constricter/fix/index/loose.cp313-win32.pyd +0 -0
- constricter/fix/index/loose.py +103 -0
- constricter/fix/index/managed.cp313-win32.pyd +0 -0
- constricter/fix/index/managed.py +63 -0
- constricter/fix/index/modules.cp313-win32.pyd +0 -0
- constricter/fix/index/modules.py +741 -0
- constricter/fix/index/offers.cp313-win32.pyd +0 -0
- constricter/fix/index/offers.py +252 -0
- constricter/fix/index/order.cp313-win32.pyd +0 -0
- constricter/fix/index/order.py +155 -0
- constricter/fix/index/origins.cp313-win32.pyd +0 -0
- constricter/fix/index/origins.py +238 -0
- constricter/fix/index/own_methods.cp313-win32.pyd +0 -0
- constricter/fix/index/own_methods.py +91 -0
- constricter/fix/index/own_overloads.cp313-win32.pyd +0 -0
- constricter/fix/index/own_overloads.py +70 -0
- constricter/fix/index/own_types.cp313-win32.pyd +0 -0
- constricter/fix/index/own_types.py +330 -0
- constricter/fix/index/params.cp313-win32.pyd +0 -0
- constricter/fix/index/params.py +81 -0
- constricter/fix/index/plain.cp313-win32.pyd +0 -0
- constricter/fix/index/plain.py +221 -0
- constricter/fix/index/project.cp313-win32.pyd +0 -0
- constricter/fix/index/project.py +797 -0
- constricter/fix/index/sides.cp313-win32.pyd +0 -0
- constricter/fix/index/sides.py +160 -0
- constricter/fix/index/strings.cp313-win32.pyd +0 -0
- constricter/fix/index/strings.py +46 -0
- constricter/fix/index/stubs/__init__.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/__init__.py +2 -0
- constricter/fix/index/stubs/atoms.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/atoms.py +425 -0
- constricter/fix/index/stubs/chained.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/chained.py +69 -0
- constricter/fix/index/stubs/classnames.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/classnames.py +124 -0
- constricter/fix/index/stubs/declared.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/declared.py +508 -0
- constricter/fix/index/stubs/installed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/installed.py +425 -0
- constricter/fix/index/stubs/names.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/names.py +200 -0
- constricter/fix/index/stubs/read.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/read.py +70 -0
- constricter/fix/index/stubs/signatures.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/signatures.py +656 -0
- constricter/fix/index/stubs/stubbed.cp313-win32.pyd +0 -0
- constricter/fix/index/stubs/stubbed.py +208 -0
- constricter/fix/index/taken.cp313-win32.pyd +0 -0
- constricter/fix/index/taken.py +45 -0
- constricter/fix/index/tuples.cp313-win32.pyd +0 -0
- constricter/fix/index/tuples.py +112 -0
- constricter/fix/libraries/__init__.cp313-win32.pyd +0 -0
- constricter/fix/libraries/__init__.py +2 -0
- constricter/fix/libraries/library.cp313-win32.pyd +0 -0
- constricter/fix/libraries/library.py +420 -0
- constricter/fix/libraries/opened.cp313-win32.pyd +0 -0
- constricter/fix/libraries/opened.py +147 -0
- constricter/fix/libraries/overloads.cp313-win32.pyd +0 -0
- constricter/fix/libraries/overloads.py +960 -0
- constricter/fix/libraries/receivers.cp313-win32.pyd +0 -0
- constricter/fix/libraries/receivers.py +274 -0
- constricter/fix/libraries/stdlib.cp313-win32.pyd +0 -0
- constricter/fix/libraries/stdlib.py +752 -0
- constricter/fix/libraries/walks.cp313-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__.cp313-win32.pyd +0 -0
- constricter/fix/values/__init__.py +2 -0
- constricter/fix/values/aliased.cp313-win32.pyd +0 -0
- constricter/fix/values/aliased.py +195 -0
- constricter/fix/values/bodies.cp313-win32.pyd +0 -0
- constricter/fix/values/bodies.py +76 -0
- constricter/fix/values/callables.cp313-win32.pyd +0 -0
- constricter/fix/values/callables.py +323 -0
- constricter/fix/values/called.cp313-win32.pyd +0 -0
- constricter/fix/values/called.py +194 -0
- constricter/fix/values/classvars.cp313-win32.pyd +0 -0
- constricter/fix/values/classvars.py +629 -0
- constricter/fix/values/decided.cp313-win32.pyd +0 -0
- constricter/fix/values/decided.py +429 -0
- constricter/fix/values/displays.cp313-win32.pyd +0 -0
- constricter/fix/values/displays.py +101 -0
- constricter/fix/values/doubts.cp313-win32.pyd +0 -0
- constricter/fix/values/doubts.py +677 -0
- constricter/fix/values/entered.cp313-win32.pyd +0 -0
- constricter/fix/values/entered.py +332 -0
- constricter/fix/values/fills.cp313-win32.pyd +0 -0
- constricter/fix/values/fills.py +446 -0
- constricter/fix/values/guesses.cp313-win32.pyd +0 -0
- constricter/fix/values/guesses.py +376 -0
- constricter/fix/values/hinted.cp313-win32.pyd +0 -0
- constricter/fix/values/hinted.py +811 -0
- constricter/fix/values/inference.cp313-win32.pyd +0 -0
- constricter/fix/values/inference.py +698 -0
- constricter/fix/values/loops.cp313-win32.pyd +0 -0
- constricter/fix/values/loops.py +335 -0
- constricter/fix/values/members.cp313-win32.pyd +0 -0
- constricter/fix/values/members.py +578 -0
- constricter/fix/values/narrowed.cp313-win32.pyd +0 -0
- constricter/fix/values/narrowed.py +220 -0
- constricter/fix/values/operated.cp313-win32.pyd +0 -0
- constricter/fix/values/operated.py +202 -0
- constricter/fix/values/ordered.cp313-win32.pyd +0 -0
- constricter/fix/values/ordered.py +74 -0
- constricter/fix/values/returned.cp313-win32.pyd +0 -0
- constricter/fix/values/returned.py +479 -0
- constricter/fix/values/returns.cp313-win32.pyd +0 -0
- constricter/fix/values/returns.py +237 -0
- constricter/fix/values/shapes.cp313-win32.pyd +0 -0
- constricter/fix/values/shapes.py +654 -0
- constricter/fix/values/stored.cp313-win32.pyd +0 -0
- constricter/fix/values/stored.py +564 -0
- constricter/fix/values/targets.cp313-win32.pyd +0 -0
- constricter/fix/values/targets.py +419 -0
- constricter/fix/values/unchecked.cp313-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__.cp313-win32.pyd +0 -0
- constricter/rules/__init__.py +2 -0
- constricter/rules/annotations.cp313-win32.pyd +0 -0
- constricter/rules/annotations.py +296 -0
- constricter/rules/binding.cp313-win32.pyd +0 -0
- constricter/rules/binding.py +652 -0
- constricter/rules/calls.cp313-win32.pyd +0 -0
- constricter/rules/calls.py +460 -0
- constricter/rules/checker.cp313-win32.pyd +0 -0
- constricter/rules/checker.py +536 -0
- constricter/rules/declarations.cp313-win32.pyd +0 -0
- constricter/rules/declarations.py +730 -0
- constricter/rules/decorators.cp313-win32.pyd +0 -0
- constricter/rules/decorators.py +351 -0
- constricter/rules/flow.cp313-win32.pyd +0 -0
- constricter/rules/flow.py +502 -0
- constricter/rules/keys.cp313-win32.pyd +0 -0
- constricter/rules/keys.py +79 -0
- constricter/rules/late.cp313-win32.pyd +0 -0
- constricter/rules/late.py +969 -0
- constricter/rules/narrowing.cp313-win32.pyd +0 -0
- constricter/rules/narrowing.py +98 -0
- constricter/rules/parsed.cp313-win32.pyd +0 -0
- constricter/rules/parsed.py +136 -0
- constricter/rules/quoted.cp313-win32.pyd +0 -0
- constricter/rules/quoted.py +141 -0
- constricter/rules/rebinding.cp313-win32.pyd +0 -0
- constricter/rules/rebinding.py +169 -0
- constricter/rules/recorded.cp313-win32.pyd +0 -0
- constricter/rules/recorded.py +168 -0
- constricter/rules/redundant.cp313-win32.pyd +0 -0
- constricter/rules/redundant.py +160 -0
- constricter/rules/scope.cp313-win32.pyd +0 -0
- constricter/rules/scope.py +802 -0
- constricter/rules/scoped.cp313-win32.pyd +0 -0
- constricter/rules/scoped.py +232 -0
- constricter/rules/scoping.cp313-win32.pyd +0 -0
- constricter/rules/scoping.py +346 -0
- constricter/rules/settings.cp313-win32.pyd +0 -0
- constricter/rules/settings.py +189 -0
- constricter/rules/syntax.cp313-win32.pyd +0 -0
- constricter/rules/syntax.py +472 -0
- constricter/rules/tables.cp313-win32.pyd +0 -0
- constricter/rules/tables.py +169 -0
- constricter/rules/walked.cp313-win32.pyd +0 -0
- constricter/rules/walked.py +127 -0
- constricter/rules/widened.cp313-win32.pyd +0 -0
- constricter/rules/widened.py +322 -0
- constricter/trace.py +374 -0
- e45ce06b2fdd5c43f782__mypyc.cp313-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,420 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Calls to the standard library `--fix` types from the tables (see `constricter.fix.libraries.stdlib`).
|
|
3
|
+
|
|
4
|
+
A class, or a function returning one (`library_class`); a function with a fixed builtin result, one
|
|
5
|
+
whose arguments decide its type (`constricter.fix.libraries.overloads`), an installed package's too (see
|
|
6
|
+
`constricter.fix.index.stubs.stubbed`), or `os.environ.get` (`library_call`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
12
|
+
|
|
13
|
+
from constricter.fix.core.imports import added_dotted
|
|
14
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
15
|
+
from constricter.fix.core.signatures import AWAIT
|
|
16
|
+
from constricter.fix.libraries import overloads, stdlib
|
|
17
|
+
from constricter.rules.annotations import dotted, vague_fits
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from constricter.fix.core.signatures import Expansion
|
|
21
|
+
|
|
22
|
+
_STDLIB: Final = "stdlib" # the fix kind
|
|
23
|
+
_STR: Final = "str"
|
|
24
|
+
_DOT: Final = "."
|
|
25
|
+
_SELF: Final = "Self" # a method's own class, in a template
|
|
26
|
+
_WITH_DEFAULT: Final = 2 # `os.environ.get(key, default)`'s arguments
|
|
27
|
+
_TYPING_ANY: Final = "typing.Any"
|
|
28
|
+
_ARITHMETIC: Final = "arithmetic" # the fix kind
|
|
29
|
+
_NUMBERS: Final = frozenset({"bool", "int", "float", "complex"}) # whose operators take these alone
|
|
30
|
+
_Operator: TypeAlias = type[ast.operator]
|
|
31
|
+
_Operands: TypeAlias = tuple[Inference | None, Inference | None]
|
|
32
|
+
# The method each binary operator calls on its left operand (see `library_operator`).
|
|
33
|
+
_OPERATORS: Final[dict[_Operator, str]] = {
|
|
34
|
+
ast.Add: "__add__",
|
|
35
|
+
ast.Sub: "__sub__",
|
|
36
|
+
ast.Mult: "__mul__",
|
|
37
|
+
ast.Div: "__truediv__",
|
|
38
|
+
ast.FloorDiv: "__floordiv__",
|
|
39
|
+
ast.Mod: "__mod__",
|
|
40
|
+
ast.BitAnd: "__and__",
|
|
41
|
+
ast.BitOr: "__or__",
|
|
42
|
+
ast.BitXor: "__xor__",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def library_class(value: ast.Call, known: Known) -> Inference | None:
|
|
47
|
+
"""Infer a call to a standard-library class, or a function returning one (`stdlib.CLASSES`).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The class, spelled (and imported, if it must be) as the module can; or `None`.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
name: str | None = stdlib.resolved(value.func, known.names.stdlib)
|
|
54
|
+
plan: ImportPlan | None = known.names.plan
|
|
55
|
+
spelled: str | None = (
|
|
56
|
+
None if name not in stdlib.CLASSES or plan is None else plan.spell(stdlib.CLASSES[name])
|
|
57
|
+
)
|
|
58
|
+
return None if spelled is None else Inference(spelled, f"`{name}`'s return type", frozenset({_STDLIB}))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def library_operator(op: ast.operator, sides: _Operands, known: Known) -> Inference | None:
|
|
62
|
+
"""Type an operator by its left operand's standard-library class's method (`__add__`, `__sub__`).
|
|
63
|
+
|
|
64
|
+
`when - start`, two `datetime`s, is a `timedelta` (see `overloads.operated`). Where the right
|
|
65
|
+
operand is a builtin's or a standard-library class's instance too: another class's reflected
|
|
66
|
+
method may be the one that answers. A builtin number's, left of a standard-library class's
|
|
67
|
+
instance (not a class under the number's own), is the right one's reflected method's
|
|
68
|
+
(`2 * span`, a `timedelta`). A class the module defines under a library one is that one's (see
|
|
69
|
+
`_as_library`), its `Self` the module's own; a generic class's instance is matched by its type
|
|
70
|
+
arguments (`Counter[str] + Counter[str]`, see `overloads.operated`).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The inference, or `None`.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
name: str | None = _OPERATORS.get(type(op))
|
|
77
|
+
if name is None or sides[0] is None or sides[1] is None:
|
|
78
|
+
return None
|
|
79
|
+
kinds: frozenset[str] = sides[0].kinds | sides[1].kinds | {_ARITHMETIC}
|
|
80
|
+
receiver: str = sides[0].annotation
|
|
81
|
+
other: str = sides[1].annotation
|
|
82
|
+
named: tuple[str, str] = (name, f"__r{name[2:]}")
|
|
83
|
+
taken: str = _as_library(receiver, named, known)
|
|
84
|
+
given: str = _as_library(other, named, known)
|
|
85
|
+
left: str | None
|
|
86
|
+
right: str | None
|
|
87
|
+
arguments: tuple[str, ...] | None
|
|
88
|
+
left, _ = _operand(taken, known)
|
|
89
|
+
right, arguments = _operand(given, known)
|
|
90
|
+
if left in _NUMBERS and right is not None and _DOT in right and not stdlib.inherits(right, left):
|
|
91
|
+
# A builtin number's own methods take builtin numbers alone: a standard-library class's
|
|
92
|
+
# Reflected method answers (`2 * span`, `timedelta.__rmul__`).
|
|
93
|
+
name, receiver, taken, left, right = named[1], other, given, right, left
|
|
94
|
+
method: stdlib.Method | None = stdlib.overloaded_method(taken, name, known)
|
|
95
|
+
if left is None or right is None or method is None:
|
|
96
|
+
return None
|
|
97
|
+
found: Inference | None = overloads.operated(
|
|
98
|
+
stdlib.for_receiver(method, receiver),
|
|
99
|
+
(left, right),
|
|
100
|
+
known,
|
|
101
|
+
arguments,
|
|
102
|
+
same=other == receiver,
|
|
103
|
+
)
|
|
104
|
+
return None if found is None else found._replace(kinds=found.kinds | kinds)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _as_library(annotation: str, names: tuple[str, str], known: Known) -> str:
|
|
108
|
+
"""Read an operand of a class the module defines as the library class it takes an operator from.
|
|
109
|
+
|
|
110
|
+
`class Money(Decimal)`: a `Decimal`, where neither it nor a class between binds the operator's
|
|
111
|
+
method or its reflected twin (`names`).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
That class as its subclass writes it (`deque[int]`), or `annotation` itself.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
found: set[str | None] = {known.class_side.lineage.definer(annotation, name) for name in names}
|
|
118
|
+
base: str | None = found.pop() if len(found) == 1 else None
|
|
119
|
+
return annotation if base is None or base == annotation or _operand(base, known)[0] is None else base
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _operand(annotation: str, known: Known) -> tuple[str | None, tuple[str, ...] | None]:
|
|
123
|
+
"""Name an operand's type as an operator's `takes` does, a generic class's instance by its class.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Its name (see `stdlib.operand`), and a generic class's instance's type arguments, as written.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
tree: ast.expr = ast.parse(annotation, mode="eval").body
|
|
130
|
+
if not isinstance(tree, ast.Subscript):
|
|
131
|
+
return stdlib.operand(annotation, known), None
|
|
132
|
+
head: str | None = stdlib.operand(ast.unparse(tree.value), known)
|
|
133
|
+
parts: list[ast.expr] = tree.slice.elts if isinstance(tree.slice, ast.Tuple) else [tree.slice]
|
|
134
|
+
return head, tuple(ast.unparse(part) for part in parts)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def library_variable(value: ast.expr, known: Known) -> Inference | None:
|
|
138
|
+
"""Infer a standard-library module's variable (`sys.path`, `os.sep`, `from sys import argv`).
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Its type, a class spelled (and imported, if it must be) as the module can, its type arguments
|
|
142
|
+
too (`sys.modules`: a `dict[str, ModuleType]`); or `None`.
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
name: str | None = (
|
|
146
|
+
stdlib.resolved(value, known.names.stdlib) if isinstance(value, ast.Name | ast.Attribute) else None
|
|
147
|
+
)
|
|
148
|
+
found: str | None = None if name is None else stdlib.VARIABLES.get(name)
|
|
149
|
+
plan: ImportPlan | None = known.names.plan
|
|
150
|
+
if found is not None and stdlib.is_class(found):
|
|
151
|
+
found = None if plan is None else plan.spell(found)
|
|
152
|
+
elif found is not None and _DOT in found: # a template: classes' paths, in a type that says more
|
|
153
|
+
found = overloads.write((found, {}), known)
|
|
154
|
+
return (
|
|
155
|
+
None
|
|
156
|
+
if found is None
|
|
157
|
+
else Inference(found, f"`{name}`'s annotation in typeshed", frozenset({_STDLIB}))
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def installed_call(
|
|
162
|
+
value: ast.expr,
|
|
163
|
+
known: Known,
|
|
164
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
165
|
+
) -> Inference | None:
|
|
166
|
+
"""Infer a call to an installed package's function whose arguments decide its type (`np.empty`).
|
|
167
|
+
|
|
168
|
+
By the signature its arguments certainly match (see `constricter.fix.index.stubs.stubbed`); `infer`
|
|
169
|
+
types an argument.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The inference, or `None` for any other call, or arguments that don't decide it.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
callee: str | None
|
|
176
|
+
func: ast.Name | ast.Attribute
|
|
177
|
+
match value:
|
|
178
|
+
case ast.Call(func=ast.Name() | ast.Attribute() as func) if (
|
|
179
|
+
known.names.installed and (callee := dotted(func)) in known.names.installed
|
|
180
|
+
):
|
|
181
|
+
return overloads.chosen(callee or "", value, known, infer)
|
|
182
|
+
case _:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def installed_method(receiver: str, name: str, known: Known) -> stdlib.Method | None:
|
|
187
|
+
"""Find an installed class's method whose arguments or instance decide its type, on a receiver's type.
|
|
188
|
+
|
|
189
|
+
Its class's type parameters bound to the receiver's type arguments (`np.ndarray[tuple[int], ...]`),
|
|
190
|
+
and `Self` to the receiver's type; through a public alias of the class (`npt.NDArray[np.float64]`),
|
|
191
|
+
the alias's parameters bound instead, and the class's by what the alias writes for them.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
It (its `entry`: the method's key in `LibraryNames.installed`), or `None`.
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
tree: ast.expr = ast.parse(receiver, mode="eval").body
|
|
198
|
+
base: str = _imported(ast.unparse(tree.value if isinstance(tree, ast.Subscript) else tree), known)
|
|
199
|
+
entry: str
|
|
200
|
+
if (entry := f"{base}.{name}") not in known.names.installed:
|
|
201
|
+
return None
|
|
202
|
+
args: list[ast.expr] = []
|
|
203
|
+
if isinstance(tree, ast.Subscript):
|
|
204
|
+
args = list(tree.slice.elts) if isinstance(tree.slice, ast.Tuple) else [tree.slice]
|
|
205
|
+
texts: list[str] = [ast.unparse(arg) for arg in args]
|
|
206
|
+
expansion: Expansion | None
|
|
207
|
+
if (expansion := known.names.aliases.get(base)) is not None:
|
|
208
|
+
own: dict[str, str] = dict(zip(expansion.params, texts, strict=False))
|
|
209
|
+
return stdlib.Method(
|
|
210
|
+
entry,
|
|
211
|
+
None,
|
|
212
|
+
{**own, _SELF: receiver},
|
|
213
|
+
dict(expansion.templates),
|
|
214
|
+
overloads.substituted(expansion.receiver, own),
|
|
215
|
+
)
|
|
216
|
+
params: tuple[str, ...] = known.names.parameters.get(base, ())
|
|
217
|
+
return stdlib.Method(entry, None, {**dict(zip(params, texts, strict=False)), _SELF: receiver})
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _imported(base: str, known: Known) -> str:
|
|
221
|
+
"""Name a class `--fix` is importing (`NDArray`) as the module's imports do (`np.typing.NDArray`).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
It, as `LibraryNames.installed` has its methods (by its dotted path, where no import of the
|
|
225
|
+
module's leads to it); `base` itself for any other.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
plan: ImportPlan | None = known.names.plan
|
|
229
|
+
if plan is None or base not in plan.added:
|
|
230
|
+
return base
|
|
231
|
+
qualified: str = added_dotted(plan.added[base])
|
|
232
|
+
bound: str
|
|
233
|
+
origin: str
|
|
234
|
+
for bound, origin in plan.bound.items():
|
|
235
|
+
if qualified.startswith(f"{origin}{_DOT}"):
|
|
236
|
+
return f"{bound}{qualified.removeprefix(origin)}"
|
|
237
|
+
return qualified
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def installed_chain(
|
|
241
|
+
value: ast.expr,
|
|
242
|
+
known: Known,
|
|
243
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
244
|
+
) -> Inference | None:
|
|
245
|
+
"""Infer an attribute of what an installed class's method returns, off the call (`cap.readouterr().out`).
|
|
246
|
+
|
|
247
|
+
By the entry the index wrote for it beside the method's (see `constricter.fix.index.stubs.chained`),
|
|
248
|
+
the receiver's type arguments binding it as they do the method's own return.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
The inference, or `None` for any other value, a receiver of no known type, or no such entry.
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
receiver: ast.expr
|
|
255
|
+
call: ast.Call
|
|
256
|
+
name: str
|
|
257
|
+
attr: str
|
|
258
|
+
match value:
|
|
259
|
+
case ast.Attribute(
|
|
260
|
+
value=ast.Call(func=ast.Attribute(value=receiver, attr=name)) as call,
|
|
261
|
+
attr=attr,
|
|
262
|
+
):
|
|
263
|
+
owner: Inference | None = infer(receiver)
|
|
264
|
+
method: stdlib.Method | None = (
|
|
265
|
+
None if owner is None else installed_method(owner.annotation, f"{name}().{attr}", known)
|
|
266
|
+
)
|
|
267
|
+
if owner is None or method is None:
|
|
268
|
+
return None
|
|
269
|
+
found: Inference | None = overloads.chosen(
|
|
270
|
+
method.entry,
|
|
271
|
+
call,
|
|
272
|
+
known,
|
|
273
|
+
infer,
|
|
274
|
+
stdlib.for_receiver(method, owner.annotation),
|
|
275
|
+
)
|
|
276
|
+
return None if found is None else found._replace(kinds=found.kinds | owner.kinds - {"copy"})
|
|
277
|
+
case _:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def chains(value: ast.expr, known: Known) -> bool:
|
|
282
|
+
"""Check whether `value` calls a method the index wrote its return's attribute of (see `installed_chain`).
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Whether it may: a method of that name has one, whatever its receiver is.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
name: str
|
|
289
|
+
match value:
|
|
290
|
+
case ast.Call(func=ast.Attribute(attr=name)):
|
|
291
|
+
return any(f".{name}()." in entry for entry in known.names.installed)
|
|
292
|
+
case _:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _any(text: str, known: Known, found: Inference) -> Inference | None:
|
|
297
|
+
"""Spell a table's bare `Any` (`json.loads`'s) as the module can, where `vague` lets it be written.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
`found`, as the module names `Any`; `None` where it's vaguer than `vague` allows, or `Any`
|
|
301
|
+
can't be named. Any other type as it is.
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
if text != stdlib.ANY:
|
|
305
|
+
return found
|
|
306
|
+
plan: ImportPlan | None = known.names.plan
|
|
307
|
+
spelled: str | None = (
|
|
308
|
+
None
|
|
309
|
+
if plan is None or not vague_fits(ast.Name(text), known.limits.vague)
|
|
310
|
+
else plan.spell(_TYPING_ANY)
|
|
311
|
+
)
|
|
312
|
+
return None if spelled is None else found._replace(annotation=spelled)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def library_awaited(
|
|
316
|
+
value: ast.expr,
|
|
317
|
+
known: Known,
|
|
318
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
319
|
+
) -> Inference | None:
|
|
320
|
+
"""Infer `await` of a call of a standard-library coroutine (see `stdlib.awaited_call`).
|
|
321
|
+
|
|
322
|
+
A function's, by its arguments where they decide it (`await asyncio.wait_for(fetch(), 1)`), or a
|
|
323
|
+
method's on a receiver whose type `infer` knows; or of any value typed as an awaitable of
|
|
324
|
+
something (`await asyncio.gather(a(), b())`, an `asyncio.Future[tuple[A, B]]`; `await task`).
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The inference, or `None` for any other value.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
call: ast.Call
|
|
331
|
+
held: ast.expr
|
|
332
|
+
match value:
|
|
333
|
+
case ast.Await(value=ast.Call() as call):
|
|
334
|
+
return _awaited_call(call, known, infer) or _awaited_value(call, known, infer)
|
|
335
|
+
case ast.Await(value=held):
|
|
336
|
+
return _awaited_value(held, known, infer)
|
|
337
|
+
case _:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _awaited_call(
|
|
342
|
+
call: ast.Call,
|
|
343
|
+
known: Known,
|
|
344
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
345
|
+
) -> Inference | None:
|
|
346
|
+
receiver: ast.expr
|
|
347
|
+
name: str
|
|
348
|
+
owner: Inference | None = None
|
|
349
|
+
found: Inference | None = None
|
|
350
|
+
match call.func:
|
|
351
|
+
case ast.Attribute(value=receiver, attr=name) if (owner := infer(receiver)) is not None:
|
|
352
|
+
found = stdlib.awaited_member(owner.annotation, name, known)
|
|
353
|
+
method: stdlib.Method | None
|
|
354
|
+
# A generic class's own coroutine, by the receiver's arguments (`await queue.get()`).
|
|
355
|
+
if found is None and (
|
|
356
|
+
(method := stdlib.overloaded_method(owner.annotation, f"{AWAIT}{name}", known)) is not None
|
|
357
|
+
):
|
|
358
|
+
found = overloads.chosen(
|
|
359
|
+
method.entry,
|
|
360
|
+
call,
|
|
361
|
+
known,
|
|
362
|
+
infer,
|
|
363
|
+
stdlib.for_receiver(method, owner.annotation),
|
|
364
|
+
)
|
|
365
|
+
case _:
|
|
366
|
+
pass
|
|
367
|
+
if found is not None and owner is not None:
|
|
368
|
+
return found._replace(kinds=found.kinds | owner.kinds - {"copy"})
|
|
369
|
+
path: str | None = stdlib.resolved(call.func, known.names.stdlib)
|
|
370
|
+
return stdlib.awaited_call(call.func, known) or (
|
|
371
|
+
overloads.chosen(f"{AWAIT}{path}", call, known, infer) if path in stdlib.AWAITED_OVERLOADS else None
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _awaited_value(
|
|
376
|
+
value: ast.expr,
|
|
377
|
+
known: Known,
|
|
378
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
379
|
+
) -> Inference | None:
|
|
380
|
+
made: Inference | None = infer(value)
|
|
381
|
+
given: str | None = None if made is None else stdlib.awaited_value(made.annotation, known)
|
|
382
|
+
return (
|
|
383
|
+
None
|
|
384
|
+
if made is None or given is None
|
|
385
|
+
else made._replace(annotation=given, reason=f"{made.reason}, awaited")
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def library_call(
|
|
390
|
+
call: ast.Call,
|
|
391
|
+
known: Known,
|
|
392
|
+
infer: Callable[[ast.expr], Inference | None],
|
|
393
|
+
) -> Inference | None:
|
|
394
|
+
"""Infer a call to a standard-library function the tables type (see `constricter.fix.libraries.stdlib`).
|
|
395
|
+
|
|
396
|
+
A fixed builtin result, whatever the arguments; the signature its arguments certainly match, for
|
|
397
|
+
one whose overloads or type variables they decide (see `constricter.fix.libraries.overloads`); and
|
|
398
|
+
`os.environ.get`'s `str | None` (`str` with a `str` default), passed positionally. `infer`
|
|
399
|
+
types an argument.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
The inference, or `None` for any other call, or arguments that don't decide it.
|
|
403
|
+
|
|
404
|
+
"""
|
|
405
|
+
args: list[ast.expr] = call.args
|
|
406
|
+
name: str | None = stdlib.resolved(call.func, known.names.stdlib)
|
|
407
|
+
reason: str = f"`{name}`'s return type"
|
|
408
|
+
kinds: frozenset[str] = frozenset({_STDLIB})
|
|
409
|
+
if name in stdlib.RETURNS:
|
|
410
|
+
return _any(stdlib.RETURNS[name], known, Inference(stdlib.RETURNS[name], reason, kinds))
|
|
411
|
+
if name is not None and name in stdlib.OVERLOADS:
|
|
412
|
+
return overloads.chosen(name, call, known, infer)
|
|
413
|
+
# `os.environ.get`, decided by its positional arguments' types, worked out only for it.
|
|
414
|
+
if call.keywords or name != stdlib.ENVIRONMENT:
|
|
415
|
+
return None
|
|
416
|
+
parts: list[Inference | None] = [infer(arg) for arg in args]
|
|
417
|
+
if len(args) == 1:
|
|
418
|
+
return Inference("str | None", reason, kinds)
|
|
419
|
+
default: Inference | None = parts[1] if len(args) == _WITH_DEFAULT else None
|
|
420
|
+
return Inference(_STR, reason, kinds | default.kinds) if default and default.annotation == _STR else None
|
|
Binary file
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`--fix` for `open(path, mode)` and `path.open(mode)`: the file object it gives, by its literal mode."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
from constricter.fix.core.known import ImportPlan, Inference, Known
|
|
8
|
+
from constricter.fix.libraries import stdlib
|
|
9
|
+
|
|
10
|
+
_OPEN: Final = "open" # the builtin, and the fix kind of what it gives
|
|
11
|
+
# The standard library's functions that open a file as the builtin does, by the mode after their
|
|
12
|
+
# first argument.
|
|
13
|
+
_OPENERS: Final = frozenset({"io.open", "_io.open", "os.fdopen"})
|
|
14
|
+
_READERS: Final = frozenset({"tokenize.open"}) # those that open one as text, whatever they're given
|
|
15
|
+
# Those that give an `io.TextIOWrapper` for a text mode alone (the tables have their other modes).
|
|
16
|
+
_COMPRESSED: Final = frozenset({"gzip.open", "bz2.open", "lzma.open"})
|
|
17
|
+
_SHELF: Final = "shelve.open" # typeshed's `Shelf[Any]`: written where `vague` lets it be
|
|
18
|
+
_ACTIONS: Final = "rwxa" # exactly one of them in a mode
|
|
19
|
+
_MODE_CHARACTERS: Final = "rwxabt+"
|
|
20
|
+
_READ: Final = "r"
|
|
21
|
+
_BINARY: Final = "b"
|
|
22
|
+
_TEXT: Final = "t"
|
|
23
|
+
_UPDATE: Final = "+"
|
|
24
|
+
_WRAPPER: Final = "io.TextIOWrapper"
|
|
25
|
+
_TEXT_FILE: Final = "typing.TextIO" # what typeshed declares `tokenize.open` gives
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def opened(value: ast.expr, known: Known) -> Inference | None:
|
|
29
|
+
"""Infer the file object `open(path, mode)` gives, by its mode (a literal, or the default `r`).
|
|
30
|
+
|
|
31
|
+
Text modes give an `io.TextIOWrapper`; binary ones an `io.BufferedReader` to read,
|
|
32
|
+
`io.BufferedWriter` to write, and `io.BufferedRandom` for both (`+`). `buffering` or `opener`
|
|
33
|
+
(in a position or by name) decides nothing: unbuffered, it's an `io.FileIO`. The builtin `open`,
|
|
34
|
+
never one the module binds itself, and the standard library's own that open as it does
|
|
35
|
+
(`io.open`, `os.fdopen`); `tokenize.open`, always a `typing.TextIO`; `gzip.open`, `bz2.open` and
|
|
36
|
+
`lzma.open` given a text mode; and `shelve.open`, a `shelve.Shelf[Any]`.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The inference (spelled, and imported if it must be, as the module can), or `None`.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
plan: ImportPlan | None = known.names.plan
|
|
43
|
+
func: ast.expr
|
|
44
|
+
args: list[ast.expr]
|
|
45
|
+
keywords: list[ast.keyword]
|
|
46
|
+
match value:
|
|
47
|
+
case ast.Call(func=func, args=[_, *args], keywords=keywords) if plan is not None:
|
|
48
|
+
pass
|
|
49
|
+
case _:
|
|
50
|
+
return None
|
|
51
|
+
if isinstance(func, ast.Name) and func.id == _OPEN and _OPEN not in plan.taken:
|
|
52
|
+
return _by_mode(args, keywords, plan)
|
|
53
|
+
origin: str | None
|
|
54
|
+
if (origin := stdlib.resolved(func, known.names.stdlib)) in _OPENERS:
|
|
55
|
+
return _by_mode(args, keywords, plan)
|
|
56
|
+
return None if origin is None else _by_opener(origin, _mode(args, keywords), plan)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _by_opener(origin: str, mode: str | None, plan: ImportPlan) -> Inference | None:
|
|
60
|
+
"""Infer what a standard-library function that opens a file its own way gives (see `opened`).
|
|
61
|
+
|
|
62
|
+
`mode`: its literal mode, as `_mode` reads it.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The inference, or `None` for any other function, or a mode that isn't a text one.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
spelled: str | None = None
|
|
69
|
+
if origin == _SHELF:
|
|
70
|
+
parts: tuple[str | None, str | None] = (plan.spell("shelve.Shelf"), plan.spell("typing.Any"))
|
|
71
|
+
spelled = None if None in parts else f"{parts[0]}[{parts[1]}]"
|
|
72
|
+
elif origin in _READERS:
|
|
73
|
+
spelled = plan.spell(_TEXT_FILE)
|
|
74
|
+
elif origin in _COMPRESSED and mode is not None and _TEXT in mode and _file_class(mode) is not None:
|
|
75
|
+
spelled = plan.spell(_WRAPPER)
|
|
76
|
+
reason: str = f"`{origin}`'s return type in typeshed"
|
|
77
|
+
return None if spelled is None else Inference(spelled, reason, frozenset({"stdlib"}))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def opened_path(receiver: str, value: ast.Call, known: Known) -> Inference | None:
|
|
81
|
+
"""Infer the file object `path.open(mode)` gives on a `pathlib` path, as `open(path, mode)`'s is.
|
|
82
|
+
|
|
83
|
+
`receiver`: the path's type, as the module spells it.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The inference, or `None` for any other call or receiver.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
plan: ImportPlan | None = known.names.plan
|
|
90
|
+
attr: str
|
|
91
|
+
args: list[ast.expr]
|
|
92
|
+
keywords: list[ast.keyword]
|
|
93
|
+
match value:
|
|
94
|
+
case ast.Call(func=ast.Attribute(attr=attr), args=args, keywords=keywords) if (
|
|
95
|
+
attr == _OPEN and plan is not None and stdlib.is_path(receiver, known)
|
|
96
|
+
):
|
|
97
|
+
return _by_mode(args, keywords, plan)
|
|
98
|
+
case _:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _mode(args: list[ast.expr], keywords: list[ast.keyword]) -> str | None:
|
|
103
|
+
"""Read a call's literal mode from the arguments after its path: the first, or the one named `mode`.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
It (`r`, with none given), or `None` if it isn't a string literal.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
named: dict[str | None, ast.expr] = {keyword.arg: keyword.value for keyword in keywords}
|
|
110
|
+
mode: ast.expr | None
|
|
111
|
+
if (mode := args[0] if args else named.get("mode")) is None:
|
|
112
|
+
return _READ
|
|
113
|
+
return mode.value if isinstance(mode, ast.Constant) and isinstance(mode.value, str) else None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _by_mode(args: list[ast.expr], keywords: list[ast.keyword], plan: ImportPlan) -> Inference | None:
|
|
117
|
+
"""Infer a file object by the arguments after its path: its mode, first or by name.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The inference, or `None` (see `opened`).
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
text: str | None = _mode(args, keywords)
|
|
124
|
+
if text is None or len(args) > 1 or {None, "buffering", "opener"} & {k.arg for k in keywords}:
|
|
125
|
+
return None
|
|
126
|
+
kind: str | None = _file_class(text)
|
|
127
|
+
spelled: str | None = None if kind is None else plan.spell(f"io.{kind}")
|
|
128
|
+
return None if spelled is None else Inference(spelled, f"`open`'s mode `{text}`", frozenset({_OPEN}))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _file_class(mode: str) -> str | None:
|
|
132
|
+
"""Name the `io` class `open` gives for `mode`.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
It, or `None` for a mode that isn't one `open` takes.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
actions: set[str] = set(mode) & set(_ACTIONS)
|
|
139
|
+
if not set(mode) <= set(_MODE_CHARACTERS) or len(actions) != 1 or len(set(mode)) != len(mode):
|
|
140
|
+
return None
|
|
141
|
+
if _BINARY not in mode:
|
|
142
|
+
return "TextIOWrapper"
|
|
143
|
+
if _TEXT in mode:
|
|
144
|
+
return None
|
|
145
|
+
if _UPDATE in mode:
|
|
146
|
+
return "BufferedRandom"
|
|
147
|
+
return "BufferedReader" if _READ in actions else "BufferedWriter"
|
|
Binary file
|