pytest-revealtype-injector 0.6.2__tar.gz → 0.7.0__tar.gz
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.
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/PKG-INFO +6 -4
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/README.md +3 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/pyproject.toml +3 -5
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/__init__.py +1 -1
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/adapter/__init__.py +14 -4
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/adapter/mypy_.py +1 -1
- pytest_revealtype_injector-0.7.0/src/pytest_revealtype_injector/adapter/pyrefly_.py +133 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/adapter/pyright_.py +2 -29
- pytest_revealtype_injector-0.7.0/src/pytest_revealtype_injector/adapter/ty_.py +162 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/hooks.py +45 -33
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/main.py +1 -1
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/models.py +27 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/tests/test_marker.py +1 -1
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/tests/test_options.py +1 -1
- pytest_revealtype_injector-0.6.2/COPYING +0 -6
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/.gitignore +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/COPYING.mit +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/adapter/basedpyright_.py +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/log.py +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/plugin.py +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/src/pytest_revealtype_injector/py.typed +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/tests/conftest.py +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/tests/test_ast_mode.py +0 -0
- {pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/tests/test_import.py +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-revealtype-injector
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Pytest plugin for replacing reveal_type() calls inside test functions with static and runtime type checking result comparison, for confirming type annotation validity.
|
|
5
5
|
Project-URL: homepage, https://github.com/abelcheung/pytest-revealtype-injector
|
|
6
6
|
Author-email: Abel Cheung <abelcheung@gmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
8
|
-
License-File: COPYING
|
|
9
8
|
License-File: COPYING.mit
|
|
10
9
|
Keywords: annotation,dynamic-typing,pytest,reveal_type,static-typing,stub,stubs,type-checking,types,typing
|
|
11
10
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -22,8 +21,8 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
22
21
|
Classifier: Topic :: Software Development :: Testing
|
|
23
22
|
Classifier: Typing :: Typed
|
|
24
23
|
Requires-Python: >=3.10
|
|
25
|
-
Requires-Dist: pytest
|
|
26
|
-
Requires-Dist: schema
|
|
24
|
+
Requires-Dist: pytest>=7.0
|
|
25
|
+
Requires-Dist: schema>=0.7.8
|
|
27
26
|
Requires-Dist: typeguard>=4.3
|
|
28
27
|
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
|
|
29
28
|
Description-Content-Type: text/markdown
|
|
@@ -156,3 +155,6 @@ FAILED tests/runtime/test_attrib.py::TestAttrib::test_superfluous - typeguard.Ty
|
|
|
156
155
|
## History
|
|
157
156
|
|
|
158
157
|
This pytest plugin starts its life as part of testsuite related utilities within [`types-lxml`](https://github.com/abelcheung/types-lxml). As `lxml` is a `cython` project and probably never incorporate inline python annotation in future, there is need to compare runtime result to static type checker output for discrepancy. As time goes by, it starts to make sense to manage as an independent project.
|
|
158
|
+
|
|
159
|
+
License-wise, originally it was part of `types-lxml` project, which is released under Apache-2.0 license. But as the sole author, it is at my own discretion to follow pytest
|
|
160
|
+
license (which is MIT) because this project is taking shape as a pytest plugin.
|
|
@@ -126,3 +126,6 @@ FAILED tests/runtime/test_attrib.py::TestAttrib::test_superfluous - typeguard.Ty
|
|
|
126
126
|
## History
|
|
127
127
|
|
|
128
128
|
This pytest plugin starts its life as part of testsuite related utilities within [`types-lxml`](https://github.com/abelcheung/types-lxml). As `lxml` is a `cython` project and probably never incorporate inline python annotation in future, there is need to compare runtime result to static type checker output for discrepancy. As time goes by, it starts to make sense to manage as an independent project.
|
|
129
|
+
|
|
130
|
+
License-wise, originally it was part of `types-lxml` project, which is released under Apache-2.0 license. But as the sole author, it is at my own discretion to follow pytest
|
|
131
|
+
license (which is MIT) because this project is taking shape as a pytest plugin.
|
|
@@ -16,10 +16,9 @@ license = 'MIT'
|
|
|
16
16
|
license-files = ['COPYING*']
|
|
17
17
|
dependencies = [
|
|
18
18
|
'typing_extensions >= 4.0; python_version < "3.11"',
|
|
19
|
-
'pytest >=7.0
|
|
19
|
+
'pytest >=7.0',
|
|
20
20
|
'typeguard >= 4.3',
|
|
21
|
-
|
|
22
|
-
'schema == 0.7.7',
|
|
21
|
+
'schema >= 0.7.8',
|
|
23
22
|
]
|
|
24
23
|
keywords = [
|
|
25
24
|
'pytest',
|
|
@@ -74,13 +73,11 @@ packages = ["src/pytest_revealtype_injector"]
|
|
|
74
73
|
typeCheckingMode = 'strict'
|
|
75
74
|
enableTypeIgnoreComments = false
|
|
76
75
|
deprecateTypingAliases = true
|
|
77
|
-
reportMissingTypeStubs = false
|
|
78
76
|
|
|
79
77
|
[tool.mypy]
|
|
80
78
|
mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
|
|
81
79
|
packages = "pytest_revealtype_injector"
|
|
82
80
|
strict = true
|
|
83
|
-
ignore_missing_imports = true
|
|
84
81
|
|
|
85
82
|
[tool.ruff]
|
|
86
83
|
target-version = "py312"
|
|
@@ -128,6 +125,7 @@ pythonpath = [
|
|
|
128
125
|
# and never does any permanent change with it
|
|
129
126
|
[tool.semantic_release]
|
|
130
127
|
version_variables = ['src/pytest_revealtype_injector/__init__.py:__version__']
|
|
128
|
+
allow_zero_version = true # switch off for 1.0.0
|
|
131
129
|
major_on_zero = false # switch on for 1.0.0
|
|
132
130
|
|
|
133
131
|
[tool.semantic_release.changelog]
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from ..models import TypeCheckerAdapter
|
|
4
|
-
from . import
|
|
4
|
+
from . import (
|
|
5
|
+
basedpyright_,
|
|
6
|
+
mypy_,
|
|
7
|
+
pyrefly_,
|
|
8
|
+
pyright_,
|
|
9
|
+
ty_,
|
|
10
|
+
)
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
# Hardcode will do for now, it's not like we're going to have more
|
|
8
|
-
# adapters
|
|
14
|
+
# adapters rapidly or make it user extensible.
|
|
9
15
|
def generate() -> set[TypeCheckerAdapter]:
|
|
10
16
|
return {
|
|
11
17
|
basedpyright_.generate_adapter(),
|
|
12
|
-
pyright_.generate_adapter(),
|
|
13
18
|
mypy_.generate_adapter(),
|
|
19
|
+
pyrefly_.generate_adapter(),
|
|
20
|
+
pyright_.generate_adapter(),
|
|
21
|
+
ty_.generate_adapter(),
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
def get_adapter_classes() -> list[type[TypeCheckerAdapter]]:
|
|
18
26
|
return [
|
|
19
27
|
basedpyright_.BasedPyrightAdapter,
|
|
20
|
-
pyright_.PyrightAdapter,
|
|
21
28
|
mypy_.MypyAdapter,
|
|
29
|
+
pyrefly_.PyreflyAdapter,
|
|
30
|
+
pyright_.PyrightAdapter,
|
|
31
|
+
ty_.TyAdapter,
|
|
22
32
|
]
|
|
@@ -150,7 +150,7 @@ def _strip_unwanted_char(input: str) -> str:
|
|
|
150
150
|
_ = ast.parse(result)
|
|
151
151
|
except SyntaxError as e:
|
|
152
152
|
assert e.offset is not None
|
|
153
|
-
result = result[:e.offset-1] + result[e.offset:]
|
|
153
|
+
result = result[: e.offset - 1] + result[e.offset :]
|
|
154
154
|
else:
|
|
155
155
|
return result
|
|
156
156
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from typing import ForwardRef, Literal, TypedDict, cast
|
|
12
|
+
|
|
13
|
+
if sys.version_info >= (3, 11):
|
|
14
|
+
from typing import TypedDict
|
|
15
|
+
else:
|
|
16
|
+
from typing_extensions import TypedDict
|
|
17
|
+
|
|
18
|
+
import schema as s
|
|
19
|
+
|
|
20
|
+
from ..log import get_logger
|
|
21
|
+
from ..models import (
|
|
22
|
+
BareNameCollector,
|
|
23
|
+
FilePos,
|
|
24
|
+
TypeCheckerAdapter,
|
|
25
|
+
TypeCheckerError,
|
|
26
|
+
VarType,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_logger = get_logger()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _PyreflyDiagItem(TypedDict):
|
|
33
|
+
line: int
|
|
34
|
+
column: int
|
|
35
|
+
stop_line: int
|
|
36
|
+
stop_column: int
|
|
37
|
+
path: str
|
|
38
|
+
code: int
|
|
39
|
+
name: str
|
|
40
|
+
description: str
|
|
41
|
+
concise_description: str
|
|
42
|
+
severity: Literal["error", "warn", "info"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NameCollector(BareNameCollector):
|
|
46
|
+
type_checker = "pyrefly"
|
|
47
|
+
|
|
48
|
+
# Pyrefly renders local class as Attribute nodes. Override to
|
|
49
|
+
# only use bare names.
|
|
50
|
+
def visit_Attribute(self, node: ast.Attribute) -> ast.expr:
|
|
51
|
+
self.modified = True
|
|
52
|
+
# TODO should have been more robust by checking if node.value
|
|
53
|
+
# resolves to a pytest function or method
|
|
54
|
+
return ast.Name(id=node.attr, ctx=node.ctx)
|
|
55
|
+
|
|
56
|
+
class PyreflyAdapter(TypeCheckerAdapter):
|
|
57
|
+
id = "pyrefly"
|
|
58
|
+
_executable = "pyrefly"
|
|
59
|
+
_type_mesg_re = re.compile(r'revealed type: (?P<type>.+)')
|
|
60
|
+
_namecollector_class = NameCollector
|
|
61
|
+
_schema = s.Schema({
|
|
62
|
+
"line": int,
|
|
63
|
+
"column": int,
|
|
64
|
+
"stop_line": int,
|
|
65
|
+
"stop_column": int,
|
|
66
|
+
"path": str,
|
|
67
|
+
"code": int,
|
|
68
|
+
"name": str,
|
|
69
|
+
"description": str,
|
|
70
|
+
"concise_description": str,
|
|
71
|
+
"severity": s.Or(s.Schema("error"), s.Schema("warn"), s.Schema("info")),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
def run_typechecker_on(self, paths: Iterable[pathlib.Path]) -> None:
|
|
75
|
+
cmd: list[str] = []
|
|
76
|
+
if shutil.which(self._executable) is not None:
|
|
77
|
+
cmd.append(self._executable)
|
|
78
|
+
else:
|
|
79
|
+
raise FileNotFoundError(f"{self._executable} is required to run test suite")
|
|
80
|
+
|
|
81
|
+
cmd.extend(["check", "--output-format", "json"])
|
|
82
|
+
if True: # TODO Needs to detect python version from project
|
|
83
|
+
cmd.extend(["--python-version", "3.11"])
|
|
84
|
+
cmd.extend(str(p) for p in paths)
|
|
85
|
+
|
|
86
|
+
_logger.debug(f"({self.id}) Run command: {cmd}")
|
|
87
|
+
proc = subprocess.run(cmd, capture_output=True)
|
|
88
|
+
# Return code: 1=normal error, 2=facebook reserved, 3=internal error
|
|
89
|
+
# Pyrefly unconditionally outputs error count to stderr regardless
|
|
90
|
+
# of exit status
|
|
91
|
+
if proc.returncode > 0:
|
|
92
|
+
raise TypeCheckerError(
|
|
93
|
+
"{} error with exit code {}: {}".format(self.id, proc.returncode, proc.stderr.decode()), None, None)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
report = json.loads(proc.stdout)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise TypeCheckerError(f"Failed to parse pyrefly JSON output: {e}", None, None) from e
|
|
99
|
+
|
|
100
|
+
assert isinstance(report, dict) and "errors" in report
|
|
101
|
+
items = cast(list[_PyreflyDiagItem], report["errors"])
|
|
102
|
+
|
|
103
|
+
_logger.info(
|
|
104
|
+
"({}) Return code = {}, diagnostic count = {}.{}".format(
|
|
105
|
+
self.id,
|
|
106
|
+
proc.returncode,
|
|
107
|
+
len(items),
|
|
108
|
+
" pytest -vv shows all items." if self.log_verbosity < 2 else "",
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
for item in items:
|
|
113
|
+
diag = cast(_PyreflyDiagItem, self._schema.validate(item))
|
|
114
|
+
if self.log_verbosity >= 2:
|
|
115
|
+
_logger.debug(f"({self.id}) {diag}")
|
|
116
|
+
|
|
117
|
+
if diag["name"] != "reveal-type":
|
|
118
|
+
continue
|
|
119
|
+
if (m := self._type_mesg_re.fullmatch(diag["description"])) is None:
|
|
120
|
+
raise TypeCheckerError(
|
|
121
|
+
f"({self.id}) unexpected reveal-type message: {diag['description']}",
|
|
122
|
+
diag["path"],
|
|
123
|
+
diag["line"],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
filename = pathlib.Path(diag["path"]).name
|
|
127
|
+
lineno = diag["line"]
|
|
128
|
+
pos = FilePos(filename, lineno)
|
|
129
|
+
self.typechecker_result[pos] = VarType(None, ForwardRef(m["type"]))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def generate_adapter() -> TypeCheckerAdapter:
|
|
133
|
+
return PyreflyAdapter()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import ast
|
|
4
3
|
import json
|
|
5
4
|
import pathlib
|
|
6
5
|
import re
|
|
@@ -13,7 +12,6 @@ from collections.abc import (
|
|
|
13
12
|
from typing import (
|
|
14
13
|
ForwardRef,
|
|
15
14
|
Literal,
|
|
16
|
-
TypeVar,
|
|
17
15
|
cast,
|
|
18
16
|
)
|
|
19
17
|
|
|
@@ -26,8 +24,8 @@ import schema as s
|
|
|
26
24
|
|
|
27
25
|
from ..log import get_logger
|
|
28
26
|
from ..models import (
|
|
27
|
+
BareNameCollector,
|
|
29
28
|
FilePos,
|
|
30
|
-
NameCollectorBase,
|
|
31
29
|
TypeCheckerAdapter,
|
|
32
30
|
TypeCheckerError,
|
|
33
31
|
VarType,
|
|
@@ -54,33 +52,8 @@ class _PyrightDiagItem(TypedDict):
|
|
|
54
52
|
rule: NotRequired[str]
|
|
55
53
|
|
|
56
54
|
|
|
57
|
-
class NameCollector(
|
|
55
|
+
class NameCollector(BareNameCollector):
|
|
58
56
|
type_checker = "pyright"
|
|
59
|
-
# Pre-register common used bare names from typing
|
|
60
|
-
collected = NameCollectorBase.collected | {
|
|
61
|
-
k: v
|
|
62
|
-
for k, v in NameCollectorBase.collected["typing"].__dict__.items()
|
|
63
|
-
if k[0].isupper() and not isinstance(v, TypeVar)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# Pyright inferred type results always contain bare names only,
|
|
67
|
-
# so don't need to bother with visit_Attribute()
|
|
68
|
-
def visit_Name(self, node: ast.Name) -> ast.Name:
|
|
69
|
-
name = node.id
|
|
70
|
-
try:
|
|
71
|
-
eval(name, self._globalns, self._localns | self.collected)
|
|
72
|
-
except NameError:
|
|
73
|
-
for m in ("typing", "typing_extensions"):
|
|
74
|
-
if not hasattr(self.collected[m], name):
|
|
75
|
-
continue
|
|
76
|
-
obj = getattr(self.collected[m], name)
|
|
77
|
-
self.collected[name] = obj
|
|
78
|
-
_logger.debug(
|
|
79
|
-
f"{self.type_checker} NameCollector resolved '{name}' as {obj}"
|
|
80
|
-
)
|
|
81
|
-
return node
|
|
82
|
-
raise
|
|
83
|
-
return node
|
|
84
57
|
|
|
85
58
|
|
|
86
59
|
class PyrightAdapter(TypeCheckerAdapter):
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Iterable
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
import schema as s
|
|
13
|
+
|
|
14
|
+
from ..log import get_logger
|
|
15
|
+
from ..models import (
|
|
16
|
+
BareNameCollector,
|
|
17
|
+
FilePos,
|
|
18
|
+
TypeCheckerAdapter,
|
|
19
|
+
TypeCheckerError,
|
|
20
|
+
VarType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
from typing import TypedDict
|
|
25
|
+
else:
|
|
26
|
+
from typing_extensions import TypedDict
|
|
27
|
+
|
|
28
|
+
if sys.version_info >= (3, 14):
|
|
29
|
+
from annotationlib import ForwardRef
|
|
30
|
+
else:
|
|
31
|
+
from typing import ForwardRef
|
|
32
|
+
|
|
33
|
+
_logger = get_logger()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _TyDiagPosition(TypedDict):
|
|
37
|
+
line: int
|
|
38
|
+
column: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _TyDiagRange(TypedDict):
|
|
42
|
+
begin: _TyDiagPosition
|
|
43
|
+
end: _TyDiagPosition
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _TyDiagLocation(TypedDict):
|
|
47
|
+
path: str
|
|
48
|
+
positions: _TyDiagRange
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _TyDiagItem(TypedDict):
|
|
52
|
+
check_name: str
|
|
53
|
+
description: str
|
|
54
|
+
severity: str
|
|
55
|
+
fingerprint: str
|
|
56
|
+
location: _TyDiagLocation
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NameCollector(BareNameCollector):
|
|
60
|
+
type_checker = "ty"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TyAdapter(TypeCheckerAdapter):
|
|
64
|
+
id = "ty"
|
|
65
|
+
_executable = "ty"
|
|
66
|
+
_type_mesg_re = re.compile(r'Revealed type: `(?P<type>.+?)`')
|
|
67
|
+
_namecollector_class = BareNameCollector
|
|
68
|
+
_schema = s.Schema({
|
|
69
|
+
"check_name": str,
|
|
70
|
+
"description": str,
|
|
71
|
+
"severity": s.Or(
|
|
72
|
+
s.Schema("info"),
|
|
73
|
+
s.Schema("minor"),
|
|
74
|
+
s.Schema("major"),
|
|
75
|
+
),
|
|
76
|
+
"fingerprint": str,
|
|
77
|
+
"location": {
|
|
78
|
+
"path": str,
|
|
79
|
+
"positions": {
|
|
80
|
+
"begin": {"line": int, "column": int},
|
|
81
|
+
"end" : {"line": int, "column": int},
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
def run_typechecker_on(self, paths: Iterable[pathlib.Path]) -> None:
|
|
87
|
+
if shutil.which(self._executable) is None:
|
|
88
|
+
raise FileNotFoundError(f"{self._executable} is required to run test suite")
|
|
89
|
+
|
|
90
|
+
cmd = [
|
|
91
|
+
self._executable,
|
|
92
|
+
"check",
|
|
93
|
+
"--no-progress",
|
|
94
|
+
"--output-format",
|
|
95
|
+
"gitlab",
|
|
96
|
+
]
|
|
97
|
+
cmd.extend(str(p) for p in paths)
|
|
98
|
+
|
|
99
|
+
if self.config_file is not None:
|
|
100
|
+
cmd.extend(["--config-file", str(self.config_file)])
|
|
101
|
+
|
|
102
|
+
_logger.debug(f"({self.id}) Run command: {cmd}")
|
|
103
|
+
proc = subprocess.run(cmd, capture_output=True)
|
|
104
|
+
if proc.returncode == 101: # internal error
|
|
105
|
+
raise TypeCheckerError(proc.stderr.decode(), None, None)
|
|
106
|
+
|
|
107
|
+
report = json.loads(proc.stdout)
|
|
108
|
+
_logger.info(
|
|
109
|
+
"({}) Return code = {}, diagnostic count = {}.{}".format(
|
|
110
|
+
self.id,
|
|
111
|
+
proc.returncode,
|
|
112
|
+
len(report),
|
|
113
|
+
" pytest -vv shows all items." if self.log_verbosity < 2 else "",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for item in report:
|
|
118
|
+
diag = cast(_TyDiagItem, self._schema.validate(item))
|
|
119
|
+
if self.log_verbosity >= 2:
|
|
120
|
+
_logger.debug(f"({self.id}) {diag}")
|
|
121
|
+
if diag["severity"] != ("major" if proc.returncode else "info"):
|
|
122
|
+
continue
|
|
123
|
+
match proc.returncode:
|
|
124
|
+
case 1 | 2:
|
|
125
|
+
filename, lineno = (
|
|
126
|
+
pathlib.Path(diag["location"]["path"]).name,
|
|
127
|
+
diag["location"]["positions"]["begin"]["line"],
|
|
128
|
+
)
|
|
129
|
+
raise TypeCheckerError(
|
|
130
|
+
"{} error with exit code {}: {}".format(
|
|
131
|
+
self.id,
|
|
132
|
+
proc.returncode,
|
|
133
|
+
diag["description"],
|
|
134
|
+
),
|
|
135
|
+
filename,
|
|
136
|
+
lineno,
|
|
137
|
+
diag["check_name"],
|
|
138
|
+
)
|
|
139
|
+
case 0:
|
|
140
|
+
pass
|
|
141
|
+
case _: # Some future error code
|
|
142
|
+
raise TypeCheckerError(
|
|
143
|
+
"Unknown {} error with exit code {}: {}".format(
|
|
144
|
+
self.id,
|
|
145
|
+
proc.returncode,
|
|
146
|
+
proc.stderr.decode(),
|
|
147
|
+
),
|
|
148
|
+
None,
|
|
149
|
+
None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
filename, lineno = (
|
|
153
|
+
pathlib.Path(diag["location"]["path"]).name,
|
|
154
|
+
diag["location"]["positions"]["begin"]["line"],
|
|
155
|
+
)
|
|
156
|
+
if (m := self._type_mesg_re.search(diag["description"])) is None:
|
|
157
|
+
continue
|
|
158
|
+
pos = FilePos(filename, lineno)
|
|
159
|
+
self.typechecker_result[pos] = VarType(None, ForwardRef(m["type"]))
|
|
160
|
+
|
|
161
|
+
def generate_adapter() -> TypeCheckerAdapter:
|
|
162
|
+
return TyAdapter()
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
4
|
import inspect
|
|
5
|
+
from collections.abc import Iterator
|
|
5
6
|
from typing import cast
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -14,7 +15,8 @@ _logger = log.get_logger()
|
|
|
14
15
|
adapter_stash_key: pytest.StashKey[set[TypeCheckerAdapter]]
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
@pytest.hookimpl(wrapper=True)
|
|
19
|
+
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Iterator[None]:
|
|
18
20
|
assert pyfuncitem.module is not None
|
|
19
21
|
adp_stash = pyfuncitem.config.stash[adapter_stash_key]
|
|
20
22
|
|
|
@@ -23,7 +25,9 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
|
|
|
23
25
|
if mark:
|
|
24
26
|
disabled_adapters = {a.id for a in adp_stash if a.id in mark.args}
|
|
25
27
|
for a in disabled_adapters:
|
|
26
|
-
_logger.info(
|
|
28
|
+
_logger.info(
|
|
29
|
+
f"{a} adapter disabled by 'notypechecker' marker in {pyfuncitem.name} test"
|
|
30
|
+
)
|
|
27
31
|
adapters = {a for a in adp_stash if a.id not in disabled_adapters}
|
|
28
32
|
else:
|
|
29
33
|
adapters = {a for a in adp_stash}
|
|
@@ -31,39 +35,47 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
|
|
|
31
35
|
if not adapters:
|
|
32
36
|
pytest.fail("All type checkers have been disabled.")
|
|
33
37
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
item = getattr(pyfuncitem.module, name)
|
|
40
|
-
if inspect.isfunction(item):
|
|
41
|
-
if item.__name__ != "reveal_type" or item.__module__ not in {
|
|
42
|
-
"typing",
|
|
43
|
-
"typing_extensions",
|
|
44
|
-
}:
|
|
38
|
+
# Monkeypatch reveal_type() with our own function, to guarantee
|
|
39
|
+
# each test func can receive different adapters
|
|
40
|
+
with pytest.MonkeyPatch.context() as mp:
|
|
41
|
+
for name in dir(pyfuncitem.module):
|
|
42
|
+
if name.startswith("__") or name.startswith("@py"):
|
|
45
43
|
continue
|
|
46
|
-
injected = functools.partial(
|
|
47
|
-
revealtype_injector,
|
|
48
|
-
adapters=adapters,
|
|
49
|
-
rt_funcname=name,
|
|
50
|
-
)
|
|
51
|
-
setattr(pyfuncitem.module, name, injected)
|
|
52
|
-
_logger.info(f"Replaced {name}() from global import with {injected}")
|
|
53
|
-
break
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
if item
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
item = getattr(pyfuncitem.module, name)
|
|
46
|
+
if inspect.isfunction(item):
|
|
47
|
+
if item.__name__ != "reveal_type" or item.__module__ not in {
|
|
48
|
+
"typing",
|
|
49
|
+
"typing_extensions",
|
|
50
|
+
}:
|
|
51
|
+
continue
|
|
52
|
+
injected = functools.partial(
|
|
53
|
+
revealtype_injector,
|
|
54
|
+
adapters=adapters,
|
|
55
|
+
rt_funcname=name,
|
|
56
|
+
)
|
|
57
|
+
mp.setattr(pyfuncitem.module, name, injected)
|
|
58
|
+
_logger.info(
|
|
59
|
+
f"Replaced {name}() from global import with {injected} in {pyfuncitem.name} test"
|
|
60
|
+
)
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
elif inspect.ismodule(item):
|
|
64
|
+
if item.__name__ not in {"typing", "typing_extensions"}:
|
|
65
|
+
continue
|
|
66
|
+
assert hasattr(item, "reveal_type")
|
|
67
|
+
injected = functools.partial(
|
|
68
|
+
revealtype_injector,
|
|
69
|
+
adapters=adapters,
|
|
70
|
+
rt_funcname=f"{name}.reveal_type",
|
|
71
|
+
)
|
|
72
|
+
mp.setattr(item, "reveal_type", injected)
|
|
73
|
+
_logger.info(
|
|
74
|
+
f"Replaced {name}.reveal_type() with {injected} in {pyfuncitem.name} test"
|
|
75
|
+
)
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
return cast(None, (yield)) # type: ignore[redundant-cast]
|
|
67
79
|
|
|
68
80
|
|
|
69
81
|
def pytest_collection_finish(session: pytest.Session) -> None:
|
|
@@ -136,7 +136,7 @@ def revealtype_injector(
|
|
|
136
136
|
walker = adp.create_collector(globalns, localns)
|
|
137
137
|
try:
|
|
138
138
|
evaluated = eval(ref.__forward_arg__, globalns, localns | walker.collected)
|
|
139
|
-
except (TypeError, NameError):
|
|
139
|
+
except (TypeError, NameError, AttributeError):
|
|
140
140
|
old_ast = ast.parse(ref.__forward_arg__, mode="eval")
|
|
141
141
|
new_ast = walker.visit(old_ast)
|
|
142
142
|
if walker.modified:
|
|
@@ -11,6 +11,7 @@ from typing import (
|
|
|
11
11
|
ClassVar,
|
|
12
12
|
ForwardRef,
|
|
13
13
|
NamedTuple,
|
|
14
|
+
TypeVar,
|
|
14
15
|
cast,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -98,6 +99,32 @@ class NameCollectorBase(ast.NodeTransformer):
|
|
|
98
99
|
return node
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
# Some type checkers always produce bare names only,
|
|
103
|
+
# so we can skip Attribute nodes entirely
|
|
104
|
+
class BareNameCollector(NameCollectorBase):
|
|
105
|
+
type_checker = ""
|
|
106
|
+
# Pre-register common used bare names from typing
|
|
107
|
+
collected = NameCollectorBase.collected | {
|
|
108
|
+
k: v
|
|
109
|
+
for k, v in NameCollectorBase.collected["typing"].__dict__.items()
|
|
110
|
+
if k[0].isupper() and not isinstance(v, TypeVar)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def visit_Name(self, node: ast.Name) -> ast.Name:
|
|
114
|
+
name = node.id
|
|
115
|
+
try:
|
|
116
|
+
eval(name, self._globalns, self._localns | self.collected)
|
|
117
|
+
except NameError:
|
|
118
|
+
for m in ("typing", "typing_extensions"):
|
|
119
|
+
if not hasattr(self.collected[m], name):
|
|
120
|
+
continue
|
|
121
|
+
obj = getattr(self.collected[m], name)
|
|
122
|
+
self.collected[name] = obj
|
|
123
|
+
return node
|
|
124
|
+
raise
|
|
125
|
+
return node
|
|
126
|
+
|
|
127
|
+
|
|
101
128
|
class TypeCheckerAdapter:
|
|
102
129
|
# Subclasses need to specify default values for below
|
|
103
130
|
id: ClassVar[str]
|
|
@@ -79,7 +79,7 @@ class TestClassMarker:
|
|
|
79
79
|
else:
|
|
80
80
|
from typing_extensions import reveal_type
|
|
81
81
|
|
|
82
|
-
# @pytest.mark.notypechecker('pyright', 'basedpyright')
|
|
82
|
+
# @pytest.mark.notypechecker('pyright', 'basedpyright', 'ty')
|
|
83
83
|
class TestFoo:
|
|
84
84
|
def test_foo(self) -> None:
|
|
85
85
|
MYPY = False
|
|
@@ -60,7 +60,7 @@ class TestDisableTypeChecker:
|
|
|
60
60
|
"""
|
|
61
61
|
)
|
|
62
62
|
content_masked = self.content_fail.format(
|
|
63
|
-
"pyright: ignore[reportAssignmentType]",
|
|
63
|
+
"pyright: ignore[reportAssignmentType] # ty: ignore[invalid-assignment] # pyrefly: ignore[bad-assignment]",
|
|
64
64
|
)
|
|
65
65
|
pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
|
|
66
66
|
content_masked
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
pytest-revealtype-injector is released under MIT license (see COPYING.mit).
|
|
2
|
-
|
|
3
|
-
Original source code comes from part of types-lxml project, which is
|
|
4
|
-
release under Apache-2.0 license. But as the sole author of all source
|
|
5
|
-
code inside this repository, it is at my own discretion to follow pytest
|
|
6
|
-
license because this project is taking shape as a pytest plugin.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_revealtype_injector-0.6.2 → pytest_revealtype_injector-0.7.0}/tests/test_ast_mode.py
RENAMED
|
File without changes
|
|
File without changes
|