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
constricter/jsonc.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""JSON with comments (`//`, `/* */`) and trailing commas, for the files constricter reads.
|
|
3
|
+
|
|
4
|
+
Comments and trailing commas become spaces (newlines kept), so every position the JSON parser reports
|
|
5
|
+
is still the original file's. What constricter writes stays plain JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from typing import TypeGuard, cast
|
|
11
|
+
|
|
12
|
+
# A string (left alone), or a comment.
|
|
13
|
+
_COMMENT: re.Pattern[str] = re.compile(r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*.*?\*/', re.DOTALL)
|
|
14
|
+
# A string (left alone), or a comma with only whitespace before the `]` or `}` it trails.
|
|
15
|
+
_TRAILING: re.Pattern[str] = re.compile(r'"(?:\\.|[^"\\])*"|,(?=\s*[\]}])')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _blank(match: re.Match[str]) -> str:
|
|
19
|
+
text: str = match.group()
|
|
20
|
+
return text if text.startswith('"') else re.sub(r"[^\n]", " ", text)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def as_text(value: str | bytes) -> str:
|
|
24
|
+
"""Decode `value` if it's bytes.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
It, as text.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
return value.decode("utf-8") if isinstance(value, bytes) else value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_int(value: object) -> TypeGuard[int]:
|
|
34
|
+
"""Check whether `value` is a plain whole number, not a `bool` (a `bool` is an `int` in Python).
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Whether it is.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def loads(text: str | bytes) -> object:
|
|
44
|
+
"""Parse JSON that may have comments and trailing commas.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The value. Raises `ValueError` as `json.loads` does.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
return cast("object", json.loads(_TRAILING.sub(_blank, _COMMENT.sub(_blank, as_text(text)))))
|
constricter/noqa.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`# noqa` comments, read the same way by the CLI and the pylint plugin (flake8 reads its own)."""
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from constricter.offences import Offence
|
|
9
|
+
|
|
10
|
+
_NOQA: re.Pattern[str] = re.compile(
|
|
11
|
+
r"#\s*noqa(?::\s*(?P<codes>[A-Z]+[0-9]+(?:[,\s]+[A-Z]+[0-9]+)*))?",
|
|
12
|
+
re.IGNORECASE,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lines(text: str) -> list[str]:
|
|
17
|
+
"""Split `text` into lines as Python does (LF, CRLF or CR).
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The lines, each keeping its ending.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
return io.StringIO(text, newline="").readlines()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def suppressed(line: str, code: str) -> bool:
|
|
27
|
+
"""Check `line` for a `# noqa` covering `code`: a bare one, or one naming it.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Whether it has one.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
match_: re.Match[str] | None
|
|
34
|
+
if (match_ := _NOQA.search(line)) is None:
|
|
35
|
+
return False
|
|
36
|
+
codes: str | None = match_.group("codes")
|
|
37
|
+
return codes is None or code in re.split(r"[,\s]+", codes.upper())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def unsuppressed(offences: Sequence[Offence], source: Sequence[str]) -> list[Offence]:
|
|
41
|
+
"""Apply the `# noqa` comments in `source`'s lines.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The offences none on their line suppresses.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
return [o for o in offences if not (o.line <= len(source) and suppressed(source[o.line - 1], o.code))]
|
constricter/notebook.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Jupyter notebooks: their code cells, joined into one module, and where each line came from."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Final, NamedTuple, TypeAlias, cast
|
|
8
|
+
|
|
9
|
+
from constricter import jsonc
|
|
10
|
+
from constricter.fix.core import fixes
|
|
11
|
+
from constricter.offences import Offence
|
|
12
|
+
|
|
13
|
+
SUFFIX: Final = ".ipynb"
|
|
14
|
+
_Json: TypeAlias = "str | int | float | bool | list[_Json] | dict[str, _Json] | None"
|
|
15
|
+
_CODE: Final = "code" # a code cell's `cell_type`
|
|
16
|
+
_SOURCE: Final = "source" # its lines
|
|
17
|
+
# IPython syntax that isn't Python: line magics, shell escapes (`!ls`), and help (`obj?`, `?obj`).
|
|
18
|
+
_MAGIC: re.Pattern[str] = re.compile(r"\s*([%!?]|[\w.]+\?{1,2}\s*$)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Line(NamedTuple):
|
|
22
|
+
"""Where a line of the joined module is in the notebook: its cell (from 1) and line in it."""
|
|
23
|
+
|
|
24
|
+
cell: int
|
|
25
|
+
line: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Cell(NamedTuple):
|
|
29
|
+
"""A cell `fix` changed: its number (from 1), and its lines before and after."""
|
|
30
|
+
|
|
31
|
+
number: int
|
|
32
|
+
old: list[str]
|
|
33
|
+
new: list[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _text(source: _Json) -> str:
|
|
37
|
+
"""Join a cell's source, which a notebook keeps as a string or a list of lines.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The source, as one string.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
return "".join(str(part) for part in source) if isinstance(source, list) else str(source)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cell_lines(source: _Json) -> list[str]:
|
|
47
|
+
"""Split a cell into lines; IPython-only lines become blank.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The lines, each ending in a newline.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
lines: list[str] = _text(source).splitlines()
|
|
54
|
+
if lines and lines[0].lstrip().startswith("%%"): # a cell magic: the whole cell isn't Python
|
|
55
|
+
return ["\n"] * len(lines)
|
|
56
|
+
return ["\n" if _MAGIC.match(line) else f"{line}\n" for line in lines]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse(raw: str, name: str) -> tuple[str, list[Line]]:
|
|
60
|
+
"""Return notebook JSON's code cells as one module, and where each of its lines came from.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The module's source, and a `Line` for each of its lines.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: It isn't a notebook (JSON with a `cells` list).
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
document: _Json = cast("_Json", jsonc.loads(raw))
|
|
70
|
+
cells: list[_Json]
|
|
71
|
+
match document:
|
|
72
|
+
case {"cells": list() as cells}:
|
|
73
|
+
pass
|
|
74
|
+
case _:
|
|
75
|
+
message: str = f"{name}: not a Jupyter notebook"
|
|
76
|
+
raise ValueError(message)
|
|
77
|
+
joined: list[str] = []
|
|
78
|
+
where: list[Line] = []
|
|
79
|
+
number: int
|
|
80
|
+
cell: _Json
|
|
81
|
+
for number, cell in enumerate(cells, start=1):
|
|
82
|
+
if isinstance(cell, dict) and cell.get("cell_type") == _CODE and _SOURCE in cell:
|
|
83
|
+
lines: list[str] = _cell_lines(cell[_SOURCE])
|
|
84
|
+
joined += lines
|
|
85
|
+
where += [Line(number, line) for line in range(1, len(lines) + 1)]
|
|
86
|
+
return "".join(joined), where
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fix(raw: str, offences: Sequence[Offence]) -> tuple[str, list[Cell]]:
|
|
90
|
+
"""Add each fixable offence's annotation in its cell of the notebook JSON `raw`.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The notebook's new JSON (its indent, key order and final newline kept), and each changed
|
|
94
|
+
cell's number and old and new lines.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
document: dict[str, _Json] = cast("dict[str, _Json]", jsonc.loads(raw))
|
|
98
|
+
cells: list[_Json] = cast("list[_Json]", document["cells"])
|
|
99
|
+
by_cell: dict[int, list[Offence]] = {}
|
|
100
|
+
o: Offence
|
|
101
|
+
for o in offences:
|
|
102
|
+
if o.edit is not None and o.cell is not None:
|
|
103
|
+
by_cell.setdefault(o.cell, []).append(o)
|
|
104
|
+
changed: list[Cell] = []
|
|
105
|
+
number: int
|
|
106
|
+
for number in sorted(by_cell):
|
|
107
|
+
cell: dict[str, _Json] = cast("dict[str, _Json]", cells[number - 1])
|
|
108
|
+
source: _Json = cell["source"]
|
|
109
|
+
old: list[str] = _text(source).splitlines(keepends=True)
|
|
110
|
+
new: list[str] = fixes.apply(old, by_cell[number])
|
|
111
|
+
cell["source"] = list[_Json](new) if isinstance(source, list) else "".join(new)
|
|
112
|
+
changed.append(Cell(number, old, new))
|
|
113
|
+
indent: re.Match[str] | None = re.match(r"\{\r?\n( +)", raw)
|
|
114
|
+
text: str = json.dumps(document, indent=len(indent.group(1)) if indent else 1, ensure_ascii=False)
|
|
115
|
+
return text + ("\n" if raw.endswith("\n") else ""), changed
|
constricter/offences.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""The codes, their messages and levels, and the `Offence` and `Checks` types every module shares."""
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import IntEnum, StrEnum
|
|
7
|
+
from typing import Final, NamedTuple
|
|
8
|
+
|
|
9
|
+
from constricter.rules.flow import Narrower
|
|
10
|
+
|
|
11
|
+
UNANNOTATED: Final = "LVA001"
|
|
12
|
+
UNTYPED_TARGET: Final = "LVA002"
|
|
13
|
+
COMMENT_TYPED_TARGET: Final = "LVA003"
|
|
14
|
+
UNANNOTATED_MEMBER: Final = "LVA004"
|
|
15
|
+
VAGUE_TYPE: Final = "LVA005"
|
|
16
|
+
NESTED_TYPE: Final = "LVA006"
|
|
17
|
+
REDUNDANT_TYPE: Final = "LVA007"
|
|
18
|
+
NARROWABLE_TYPE: Final = "LVA008"
|
|
19
|
+
MISMATCHED_TYPE: Final = "LVA009"
|
|
20
|
+
UNUSED_UNION_MEMBER: Final = "LVA010"
|
|
21
|
+
LONG_TUPLE: Final = "LVA011"
|
|
22
|
+
CAN_BE_FINAL: Final = "LVA012"
|
|
23
|
+
MESSAGES: dict[str, str] = {
|
|
24
|
+
UNANNOTATED: "local variable {name} is not annotated where it's first bound",
|
|
25
|
+
UNTYPED_TARGET: "for/match variable {name} is untyped; declare it before the statement",
|
|
26
|
+
COMMENT_TYPED_TARGET: "for variable {name} is typed only by a type comment; declare it before the loop",
|
|
27
|
+
UNANNOTATED_MEMBER: "module or class variable {name} is not annotated where it's first bound",
|
|
28
|
+
VAGUE_TYPE: "the annotation of {name} is vague: Any, object, or a generic without its parameters",
|
|
29
|
+
NESTED_TYPE: "the annotation of {name} nests too deeply; name a part of it with a `type` alias",
|
|
30
|
+
REDUNDANT_TYPE: "{name} is annotated again with the type it already has, in the same block",
|
|
31
|
+
NARROWABLE_TYPE: "{name} only ever holds `{detail}`; its annotation could narrow to that",
|
|
32
|
+
MISMATCHED_TYPE: "{name} is bound to `{detail}` here, which doesn't fit its annotation",
|
|
33
|
+
UNUSED_UNION_MEMBER: "{name}'s annotation allows `{detail}`, which no value it's bound to ever is",
|
|
34
|
+
LONG_TUPLE: "the annotation of {name} lists a tuple of {detail} elements; name them (a NamedTuple)",
|
|
35
|
+
CAN_BE_FINAL: "{name} is bound once and never rebound; it could be `Final`",
|
|
36
|
+
}
|
|
37
|
+
# Codes reported only when selected by their full code (`--select LVA012`), never by a prefix.
|
|
38
|
+
OPT_IN: Final = frozenset({CAN_BE_FINAL})
|
|
39
|
+
# Each way `--fix` can decide an annotation, by its stable id (`--show-fixes`, `fix-select`).
|
|
40
|
+
FIX_KINDS: dict[str, str] = {
|
|
41
|
+
"literal": "a literal, an f-string, `not x`, or `x in y` or `x is y`",
|
|
42
|
+
"container": "a list, set, tuple or dict display whose elements' types agree",
|
|
43
|
+
"joined": "a list, set or dict display whose elements' types differ, as their union (a guess)",
|
|
44
|
+
"copy": "a copy of a local whose type is known",
|
|
45
|
+
"subscript": "a subscript of a known container",
|
|
46
|
+
"attribute": "an attribute of a class the module defines",
|
|
47
|
+
"method": "a method with a fixed or declared return type, on a known local",
|
|
48
|
+
"builtin": "a builtin with a fixed return type (`len`, `str`, ...)",
|
|
49
|
+
"call": "a function that declares its return type (this module's, or another checked file's)",
|
|
50
|
+
"constructor": "a call to a capitalised name, taken to construct one (a guess)",
|
|
51
|
+
"conditional": "both sides of `a if c else b`, or one side and `None`",
|
|
52
|
+
"boolean": "`a or b` or `a and b`, its operands of one type",
|
|
53
|
+
"arithmetic": "arithmetic on builtin scalars",
|
|
54
|
+
"compare": "a comparison of builtin values (`n < 3`), always a `bool`",
|
|
55
|
+
"comprehension": "a list, set or dict comprehension's elements",
|
|
56
|
+
"builder": "`sorted`, `list`, `set`, `frozenset` or `tuple` of known elements",
|
|
57
|
+
"await": "`await` of a checked file's `async def`, a function or a method",
|
|
58
|
+
"cast": "`typing.cast(T, x)`: its `T`",
|
|
59
|
+
"stdlib": "a standard-library function with a builtin result or class (`time.time`, `uuid4`)",
|
|
60
|
+
"optional": "`x = None`, then only ever a value of one known type `T`: `T | None`",
|
|
61
|
+
"rebound": "a name later bound to a wider type: the type every value fits (`int`, then `float`)",
|
|
62
|
+
"filled": "an empty container, then only what the function adds to it (a guess)",
|
|
63
|
+
"returned": "an unannotated function's own `return`s (a method's: a guess)",
|
|
64
|
+
"assigned": "an unannotated instance attribute's every `self.x = value` in its class (a guess)",
|
|
65
|
+
"callers": "an unannotated parameter every call in the checked files passes one type (a guess)",
|
|
66
|
+
"member": "a plain class's variable, by its value in the class's body (a guess)",
|
|
67
|
+
"fixture": "a test's parameter, by its pytest fixture's value or its `parametrize` literals (a guess)",
|
|
68
|
+
"alias": "a module's type alias, a subscript or a union of types: `TypeAlias`",
|
|
69
|
+
"callable": "a function or a bound method bound to a name, by what its call gives",
|
|
70
|
+
"final": "LVA012's `Final`: around its annotation, or with LVA001's type (`Final[int]`)",
|
|
71
|
+
"checker": "a type checker's inferred type, from its inlay hints (`--infer-with`; a guess)",
|
|
72
|
+
"traced": "what a traced run bound the name to (`--infer-from`; a guess)",
|
|
73
|
+
"open": "`open(path, mode)`'s file object, by its literal mode (`io.TextIOWrapper`, ...)",
|
|
74
|
+
"loop": "what a loop (or `sorted`, `list`, ...) iterates over",
|
|
75
|
+
"unpack": "an unpacking, split over its names",
|
|
76
|
+
"narrow": "LVA008's or LVA010's narrower annotation (a guess)",
|
|
77
|
+
"comment": "LVA003: the loop's own `# type:` comment, as a declaration",
|
|
78
|
+
"redundant": "LVA007: the repeated annotation, dropped",
|
|
79
|
+
"untyped-parameters": "what comes of a parameter no annotation types: `Any`, marked (`fix-widen`)",
|
|
80
|
+
"unions": "a name bound to values of two or three types: their union, marked (`fix-widen`; a guess)",
|
|
81
|
+
"vague": "a type vaguer than `vague` allows, written anyway, marked (`fix-widen`; a guess)",
|
|
82
|
+
"empty-containers": "an empty container nothing in sight fills: `list[Any]`, marked (`fix-widen`)",
|
|
83
|
+
"mixed-containers": "a display of mixed or unknown elements: `list[Any]`, marked (`fix-widen`)",
|
|
84
|
+
"untyped-calls": "a call of a checked file's function declaring no return: `Any`, marked (`fix-widen`)",
|
|
85
|
+
"unknown-calls": "any other call of no known type: `Any`, marked (`fix-widen`)",
|
|
86
|
+
}
|
|
87
|
+
UNTYPED_PARAMETERS: Final = "untyped-parameters"
|
|
88
|
+
UNIONS: Final = "unions"
|
|
89
|
+
VAGUE_KIND: Final = "vague"
|
|
90
|
+
EMPTY_CONTAINERS: Final = "empty-containers"
|
|
91
|
+
MIXED_CONTAINERS: Final = "mixed-containers"
|
|
92
|
+
UNTYPED_CALLS: Final = "untyped-calls"
|
|
93
|
+
UNKNOWN_CALLS: Final = "unknown-calls"
|
|
94
|
+
# The kinds that write a wider type than the value's own, each offered only where `fix-widen` names it.
|
|
95
|
+
WIDEN_KINDS: Final = frozenset(
|
|
96
|
+
{
|
|
97
|
+
UNTYPED_PARAMETERS,
|
|
98
|
+
UNIONS,
|
|
99
|
+
VAGUE_KIND,
|
|
100
|
+
EMPTY_CONTAINERS,
|
|
101
|
+
MIXED_CONTAINERS,
|
|
102
|
+
UNTYPED_CALLS,
|
|
103
|
+
UNKNOWN_CALLS,
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
# What ends a line a widening wrote: its annotation is `--fix`'s to replace, and isn't LVA005.
|
|
107
|
+
MARK: Final = "# constricter: auto"
|
|
108
|
+
CONSTRUCTOR: Final = "constructor"
|
|
109
|
+
MEMBER: Final = "member"
|
|
110
|
+
NARROW: Final = "narrow"
|
|
111
|
+
NESTING: Final = 3 # LVA006's default depth
|
|
112
|
+
MAX_LENGTH: Final = 4 # LVA011's default: the longest fixed-length tuple an annotation may list
|
|
113
|
+
VAGUE: Final = -1 # LVA005's default level: no vague part at all (see `annotations.vague_fits`)
|
|
114
|
+
STARRED_SUBSCRIPTS: Final = (3, 11) # the first Python to parse `tuple[int, *Ts]`
|
|
115
|
+
# The guesses `--likely` applies: each a whole set of mechanisms, measured (see docs/LIKELY.md).
|
|
116
|
+
LIKELY: Final = frozenset(
|
|
117
|
+
{
|
|
118
|
+
frozenset({"arithmetic", "copy"}),
|
|
119
|
+
frozenset({"arithmetic", "copy", "literal"}),
|
|
120
|
+
frozenset({"arithmetic", "literal"}),
|
|
121
|
+
frozenset({"arithmetic", "literal", "stdlib"}),
|
|
122
|
+
frozenset({"assigned"}),
|
|
123
|
+
frozenset({"assigned", "loop"}),
|
|
124
|
+
frozenset({"assigned", "method"}),
|
|
125
|
+
frozenset({"assigned", "method", "unpack"}),
|
|
126
|
+
frozenset({"assigned", "stdlib"}),
|
|
127
|
+
frozenset({"assigned", "subscript"}),
|
|
128
|
+
frozenset({"attribute"}),
|
|
129
|
+
frozenset({"attribute", "constructor"}),
|
|
130
|
+
frozenset({"attribute", "loop"}),
|
|
131
|
+
frozenset({"attribute", "rebound"}),
|
|
132
|
+
frozenset({"attribute", "subscript"}),
|
|
133
|
+
frozenset({"builtin", "copy"}),
|
|
134
|
+
frozenset({"call"}),
|
|
135
|
+
frozenset({"call", "constructor"}),
|
|
136
|
+
frozenset({"call", "constructor", "container", "copy"}),
|
|
137
|
+
frozenset({"call", "constructor", "container", "literal"}),
|
|
138
|
+
frozenset({"call", "container", "copy"}),
|
|
139
|
+
frozenset({"call", "container", "literal"}),
|
|
140
|
+
frozenset({"call", "copy"}),
|
|
141
|
+
frozenset({"call", "rebound"}),
|
|
142
|
+
frozenset({"comprehension", "constructor"}),
|
|
143
|
+
frozenset({"comprehension", "constructor", "copy", "loop"}),
|
|
144
|
+
frozenset({"conditional", "literal"}),
|
|
145
|
+
frozenset({"constructor"}),
|
|
146
|
+
frozenset({"constructor", "container"}),
|
|
147
|
+
frozenset({"constructor", "container", "literal"}),
|
|
148
|
+
frozenset({"constructor", "copy"}),
|
|
149
|
+
frozenset({"constructor", "filled"}),
|
|
150
|
+
frozenset({"constructor", "fixture"}),
|
|
151
|
+
frozenset({"constructor", "joined"}),
|
|
152
|
+
frozenset({"constructor", "method"}),
|
|
153
|
+
frozenset({"constructor", "method", "rebound"}),
|
|
154
|
+
frozenset({"constructor", "optional"}),
|
|
155
|
+
frozenset({"constructor", "rebound"}),
|
|
156
|
+
frozenset({"constructor", "returned"}),
|
|
157
|
+
frozenset({"constructor", "unpack"}),
|
|
158
|
+
frozenset({"container", "copy", "literal"}),
|
|
159
|
+
frozenset({"container", "copy", "loop"}),
|
|
160
|
+
frozenset({"container", "joined", "literal"}),
|
|
161
|
+
frozenset({"container", "joined", "literal", "loop"}),
|
|
162
|
+
frozenset({"container", "literal", "rebound"}),
|
|
163
|
+
frozenset({"copy"}),
|
|
164
|
+
frozenset({"copy", "filled"}),
|
|
165
|
+
frozenset({"copy", "literal"}),
|
|
166
|
+
frozenset({"copy", "loop"}),
|
|
167
|
+
frozenset({"copy", "stdlib"}),
|
|
168
|
+
frozenset({"copy", "unpack"}),
|
|
169
|
+
frozenset({"filled", "literal"}),
|
|
170
|
+
frozenset({"literal", "rebound", "unpack"}),
|
|
171
|
+
frozenset({"loop", "member"}),
|
|
172
|
+
frozenset({"loop", "rebound"}),
|
|
173
|
+
frozenset({"method"}),
|
|
174
|
+
frozenset({"method", "rebound", "unpack"}),
|
|
175
|
+
frozenset({"method", "unpack"}),
|
|
176
|
+
frozenset({"rebound", "returned"}),
|
|
177
|
+
frozenset({"rebound", "stdlib"}),
|
|
178
|
+
frozenset({"returned", "unpack"}),
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
UNION_SYNTAX: Final = (3, 10) # the first Python where `int | None` is a type
|
|
182
|
+
BUILTIN_GENERICS: Final = (3, 9) # and where `list[int]` is
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Level(IntEnum):
|
|
186
|
+
"""How strict: each level makes one more code an error rather than a warning."""
|
|
187
|
+
|
|
188
|
+
RELAXED = 0
|
|
189
|
+
STRICT = 1
|
|
190
|
+
CONSTRICT = 2
|
|
191
|
+
SUFFOCATE = 3
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Each level by name and by number, as the options take it.
|
|
195
|
+
LEVELS: dict[str, Level] = {key: level for level in Level for key in (level.name.lower(), str(level.value))}
|
|
196
|
+
_ERROR_FROM: dict[str, Level] = {
|
|
197
|
+
UNANNOTATED: Level.STRICT,
|
|
198
|
+
UNTYPED_TARGET: Level.CONSTRICT,
|
|
199
|
+
COMMENT_TYPED_TARGET: Level.SUFFOCATE,
|
|
200
|
+
UNANNOTATED_MEMBER: Level.STRICT,
|
|
201
|
+
VAGUE_TYPE: Level.SUFFOCATE,
|
|
202
|
+
NESTED_TYPE: Level.SUFFOCATE,
|
|
203
|
+
REDUNDANT_TYPE: Level.SUFFOCATE,
|
|
204
|
+
NARROWABLE_TYPE: Level.SUFFOCATE,
|
|
205
|
+
MISMATCHED_TYPE: Level.CONSTRICT,
|
|
206
|
+
UNUSED_UNION_MEMBER: Level.SUFFOCATE,
|
|
207
|
+
LONG_TUPLE: Level.SUFFOCATE,
|
|
208
|
+
CAN_BE_FINAL: Level.SUFFOCATE,
|
|
209
|
+
}
|
|
210
|
+
# Codes reported only from a level up (the rest are reported at every level).
|
|
211
|
+
_REPORTED_FROM: dict[str, Level] = {
|
|
212
|
+
VAGUE_TYPE: Level.STRICT,
|
|
213
|
+
NESTED_TYPE: Level.STRICT,
|
|
214
|
+
LONG_TUPLE: Level.STRICT,
|
|
215
|
+
NARROWABLE_TYPE: Level.CONSTRICT,
|
|
216
|
+
UNUSED_UNION_MEMBER: Level.CONSTRICT,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Edit(StrEnum):
|
|
221
|
+
"""Where a `Fix` writes its annotation."""
|
|
222
|
+
|
|
223
|
+
ANNOTATE = "annotate" # `: T` after the offence's name
|
|
224
|
+
REPLACE = "replace" # over an annotation already there (`Fix.span`: its start and end column)
|
|
225
|
+
DECLARE = "declare" # `name: T` on a line of its own before a statement (`Fix.span`: its line and column)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Fix(NamedTuple):
|
|
229
|
+
"""The annotation `--fix` would write for an offence, how its value decided it, and where."""
|
|
230
|
+
|
|
231
|
+
annotation: str
|
|
232
|
+
reason: str = "" # `--show-fixes`
|
|
233
|
+
unsafe: bool = False # a guess, applied only with `--unsafe-fixes`
|
|
234
|
+
edit: Edit = Edit.ANNOTATE
|
|
235
|
+
span: tuple[int, int] = (0, 0) # see `Edit`; columns count UTF-8 bytes, as `ast`'s do
|
|
236
|
+
kinds: frozenset[str] = frozenset() # every `FIX_KINDS` mechanism that decided it
|
|
237
|
+
# The columns to delete too: with `Edit.DECLARE`, on the statement's line (the type comment it
|
|
238
|
+
# replaces); with `Edit.REPLACE`, on line `mark` (the `MARK` of the widening it replaces).
|
|
239
|
+
drop: tuple[int, int] | None = None
|
|
240
|
+
imports: tuple[str, ...] = () # statements the annotation needs added (`from io import BytesIO`)
|
|
241
|
+
after: int = 0 # the line they go after (see `fix.core.imports.plan`)
|
|
242
|
+
guarded: tuple[str, ...] = () # statements the annotation needs added under `if TYPE_CHECKING:`
|
|
243
|
+
guard: str = "" # how the module names `TYPE_CHECKING`, for a new such block
|
|
244
|
+
block: tuple[int, int] = (0, 0) # the first and last line of the body of one there is (see `ImportPlan`)
|
|
245
|
+
# The line whose end a widening's `MARK` goes at (see `WIDEN_KINDS`), or its replacement's is
|
|
246
|
+
# dropped from: its statement's last, where that isn't the offence's own (0).
|
|
247
|
+
mark: int = 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Tier(StrEnum):
|
|
251
|
+
"""How far a fix is trusted: what `--fix` applies it with."""
|
|
252
|
+
|
|
253
|
+
CERTAIN = "certain" # always
|
|
254
|
+
LIKELY = "likely" # a guess `--likely` applies too
|
|
255
|
+
GUESS = "guess" # one only `--unsafe-fixes` does
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def of(cls, *, unsafe: bool, likely: bool) -> "Tier":
|
|
259
|
+
"""Name the tier of a fix that's a guess or not (`unsafe`), of a likely set or not.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
It.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
return cls.CERTAIN if not unsafe else cls.LIKELY if likely else cls.GUESS
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class FixPolicy(NamedTuple):
|
|
269
|
+
"""Which `FIX_KINDS` `--fix` offers (`select`, empty for all, less `ignore`), and which guesses it trusts.
|
|
270
|
+
|
|
271
|
+
A guess is certain when every guessing mechanism it rests on (`constructor`, `narrow`, through
|
|
272
|
+
any guessed local it copies) is in `unsafe_select`, as ruff's `extend-safe-fixes` does. A
|
|
273
|
+
widening (see `WIDEN_KINDS`) is offered only if it's in `widen`.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
select: frozenset[str] = frozenset()
|
|
277
|
+
ignore: frozenset[str] = frozenset()
|
|
278
|
+
unsafe_select: frozenset[str] = frozenset()
|
|
279
|
+
widen: frozenset[str] = frozenset()
|
|
280
|
+
|
|
281
|
+
def allows(self, kinds: frozenset[str]) -> bool:
|
|
282
|
+
"""Check whether a fix decided by `kinds` is offered.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Whether every one is selected, none ignored, and each widening asked for.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
return (
|
|
289
|
+
(not self.select or kinds <= self.select)
|
|
290
|
+
and not kinds & self.ignore
|
|
291
|
+
and kinds & WIDEN_KINDS <= self.widen
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def trusts(self, origins: frozenset[str]) -> bool:
|
|
295
|
+
"""Check whether a guess resting on `origins` is promoted to certain.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Whether it rests on something, and all of it is in `unsafe_select`.
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
return bool(origins) and origins <= self.unsafe_select
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass(frozen=True, order=True)
|
|
305
|
+
class Offence:
|
|
306
|
+
"""One untyped first binding; `col` is 0-based."""
|
|
307
|
+
|
|
308
|
+
line: int
|
|
309
|
+
col: int
|
|
310
|
+
name: str
|
|
311
|
+
code: str = UNANNOTATED
|
|
312
|
+
# What `--fix` would write, where the value makes it unambiguous (or, if `unsafe`, a guess).
|
|
313
|
+
edit: Fix | None = field(default=None, compare=False)
|
|
314
|
+
# In a notebook, the cell (from 1); `line` is then the line in that cell.
|
|
315
|
+
cell: int | None = field(default=None, compare=False)
|
|
316
|
+
# What the message names besides the variable: LVA008's to LVA010's type, LVA011's length.
|
|
317
|
+
detail: str = field(default="", compare=False)
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def fix(self) -> str | None:
|
|
321
|
+
"""The annotation `--fix` would write, if any."""
|
|
322
|
+
return None if self.edit is None else self.edit.annotation
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def unsafe(self) -> bool:
|
|
326
|
+
"""Whether the fix is a guess, applied only with `--unsafe-fixes` (or `--likely`, if `likely`)."""
|
|
327
|
+
return self.edit is not None and self.edit.unsafe
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def tier(self) -> Tier | None:
|
|
331
|
+
"""The fix's tier, a guess likely where its mechanisms are a set of `LIKELY`; `None` with no fix."""
|
|
332
|
+
if self.edit is None:
|
|
333
|
+
return None
|
|
334
|
+
return Tier.of(unsafe=self.edit.unsafe, likely=self.edit.kinds in LIKELY)
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def likely(self) -> bool:
|
|
338
|
+
"""Whether the fix is a guess `--likely` applies."""
|
|
339
|
+
return self.tier is Tier.LIKELY
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def reason(self) -> str:
|
|
343
|
+
"""How the value decided the fix (`--show-fixes`)."""
|
|
344
|
+
return "" if self.edit is None else self.edit.reason
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def message(self) -> str:
|
|
348
|
+
"""The report text."""
|
|
349
|
+
return MESSAGES[self.code].format(name=repr(self.name), detail=self.detail)
|
|
350
|
+
|
|
351
|
+
def is_error(self, level: Level) -> bool:
|
|
352
|
+
"""Check this offence's severity at `level`.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Whether it's an error rather than a warning.
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
return errs(self.code, level)
|
|
359
|
+
|
|
360
|
+
def is_reported(self, level: Level) -> bool:
|
|
361
|
+
"""Check whether `level` reports this offence.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Whether it does at all.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
return reports(self.code, level)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def errs(code: str, level: Level) -> bool:
|
|
371
|
+
"""Check whether an offence of `code` is an error at `level`.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Whether it is: a warning otherwise.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
return level >= _ERROR_FROM[code]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def reports(code: str, level: Level) -> bool:
|
|
381
|
+
"""Check whether `level` reports an offence of `code`.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Whether it does at all.
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
return level >= _REPORTED_FROM.get(code, Level.RELAXED)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# Bases whose class bodies `--fix` annotates as a plain class's (`fix-plain-bases` adds to them): a
|
|
391
|
+
# class, or a package for every class in it; `!` leaves one out. Django reads no annotation in a class
|
|
392
|
+
# body, but its `Choices` are enums, whose members a type checker won't have annotated.
|
|
393
|
+
PLAIN_BASES: Final = (
|
|
394
|
+
"django",
|
|
395
|
+
"!django.db.models.Choices",
|
|
396
|
+
"!django.db.models.IntegerChoices",
|
|
397
|
+
"!django.db.models.TextChoices",
|
|
398
|
+
"!django.db.models.enums",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class Checks(NamedTuple):
|
|
403
|
+
"""What to check, beyond the defaults.
|
|
404
|
+
|
|
405
|
+
With `type_comments`, `x = 1 # type: int` counts as annotated; with `all_scopes`, module and
|
|
406
|
+
class bodies are checked too (LVA004); an annotation nested `nesting` deep is LVA006, and one
|
|
407
|
+
listing a fixed-length tuple longer than `max_length` is LVA011. `min_python`: the oldest Python
|
|
408
|
+
the code runs on (`None`: not known), whose syntax a fix is written in. `vague`: how vague an
|
|
409
|
+
annotation may be before it's LVA005, and a fix isn't offered (see `annotations.vague_fits`).
|
|
410
|
+
`plain_bases`: the bases a plain class may have besides the builtin ones (see `PLAIN_BASES`).
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
type_comments: bool = False
|
|
414
|
+
all_scopes: bool = False
|
|
415
|
+
nesting: int = NESTING
|
|
416
|
+
max_length: int = MAX_LENGTH
|
|
417
|
+
# A project's own type hierarchy, for LVA008-LVA010: each type and the types it's narrower than.
|
|
418
|
+
narrower: tuple[Narrower, ...] = ()
|
|
419
|
+
fixes: FixPolicy = FixPolicy() # which fixes `--fix` offers; it never changes what's reported
|
|
420
|
+
final: bool = False # look for LVA012 (opt-in: see `OPT_IN`)
|
|
421
|
+
min_python: tuple[int, int] | None = None
|
|
422
|
+
vague: int = VAGUE
|
|
423
|
+
plain_bases: tuple[str, ...] = PLAIN_BASES
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
DEFAULT_CHECKS: Final = Checks()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def at(node: ast.expr | ast.pattern | ast.stmt) -> tuple[int, int]:
|
|
430
|
+
"""Find where `node` starts, as an offence is placed.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Its line and (0-based) column.
|
|
434
|
+
|
|
435
|
+
"""
|
|
436
|
+
return node.lineno, node.col_offset
|
constricter/overrides.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""`typing.override`, which is Python 3.12+, on 3.11 too: what type checkers see, and nothing at run time.
|
|
3
|
+
|
|
4
|
+
Apart from the modules mypyc compiles, which takes the code under `if TYPE_CHECKING:` for the code
|
|
5
|
+
that runs, and so this `else` for code no run reaches.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
else:
|
|
13
|
+
|
|
14
|
+
def override(func: object) -> object:
|
|
15
|
+
"""Mark an override (for type checkers only).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
`func`, unchanged.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
return func
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["override"]
|