pytest-revealtype-injector 0.1.1__tar.gz → 0.2.2__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.2.2/COPYING +6 -0
- pytest_revealtype_injector-0.1.1/LICENSE → pytest_revealtype_injector-0.2.2/COPYING.mit +1 -11
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/PKG-INFO +36 -4
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/README.md +30 -0
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/pyproject.toml +13 -8
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/__init__.py +1 -1
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/adapter/mypy_.py +27 -8
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/adapter/pyright_.py +49 -18
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/hooks.py +3 -4
- pytest_revealtype_injector-0.2.2/src/pytest_revealtype_injector/log.py +19 -0
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/main.py +15 -7
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/models.py +2 -0
- pytest_revealtype_injector-0.2.2/tests/conftest.py +1 -0
- pytest_revealtype_injector-0.2.2/tests/test_basic.py +91 -0
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/.gitignore +0 -0
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/adapter/__init__.py +0 -0
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/plugin.py +0 -0
- {pytest_revealtype_injector-0.1.1 → pytest_revealtype_injector-0.2.2}/src/pytest_revealtype_injector/py.typed +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2023-
|
|
1
|
+
Copyright (c) 2023-2024 Abel Cheung
|
|
2
2
|
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
4
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -17,13 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
17
17
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
18
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
19
|
SOFTWARE.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
---------------------
|
|
23
|
-
|
|
24
|
-
pytest-revealtype-injector is released under MIT license (see above).
|
|
25
|
-
|
|
26
|
-
Original source code comes from part of types-lxml project, which is released
|
|
27
|
-
under Apache-2.0 license. But as the sole author of all relevant source code, it
|
|
28
|
-
is at my own discretion to follow pytest license because this project is taking
|
|
29
|
-
form of a pytest plugin.
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-revealtype-injector
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
-
License: MIT
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: COPYING
|
|
9
|
+
License-File: COPYING.mit
|
|
8
10
|
Keywords: annotation,dynamic-typing,pytest,reveal_type,static-typing,stub,stubs,type-checking,types,typing
|
|
9
11
|
Classifier: Development Status :: 4 - Beta
|
|
10
12
|
Classifier: Framework :: Pytest
|
|
11
13
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
14
|
Classifier: Programming Language :: Python
|
|
14
15
|
Classifier: Programming Language :: Python :: 3
|
|
15
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -23,9 +24,15 @@ Requires-Python: >=3.10
|
|
|
23
24
|
Requires-Dist: mypy>=1.11.2
|
|
24
25
|
Requires-Dist: pyright~=1.1
|
|
25
26
|
Requires-Dist: pytest>=7.0
|
|
27
|
+
Requires-Dist: schema==0.7.7
|
|
26
28
|
Requires-Dist: typeguard~=4.3
|
|
27
29
|
Description-Content-Type: text/markdown
|
|
28
30
|
|
|
31
|
+

|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
29
36
|
`pytest-revealtype-injector` is a `pytest` plugin for replacing [`reveal_type()`](https://docs.python.org/3/library/typing.html#typing.reveal_type) calls inside test functions as something more sophisticated. It does the following tasks in parallel:
|
|
30
37
|
|
|
31
38
|
- Launch external static type checkers (`pyright` and `mypy`) and store `reveal_type` results.
|
|
@@ -77,6 +84,31 @@ def test_something():
|
|
|
77
84
|
|
|
78
85
|
2. `reveal_type()` calls have to stay in a single line, without anything else. This limitation comes from using [`eval` mode in AST parsing](https://docs.python.org/3/library/ast.html#ast.Expression).
|
|
79
86
|
|
|
87
|
+
## Logging
|
|
88
|
+
|
|
89
|
+
This plugin uses standard [`logging`](https://docs.python.org/3/library/logging.html) internally. `pytest -v` can be used to reveal `INFO` and `DEBUG` logs. Given following example:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
def test_superfluous(self) -> None:
|
|
93
|
+
x: list[str] = ['a', 'b', 'c', 1] # type: ignore # pyright: ignore
|
|
94
|
+
reveal_type(x)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Something like this will be shown as test result:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
...
|
|
101
|
+
raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
|
|
102
|
+
E typeguard.TypeCheckError: item 3 is not an instance of str (from pyright)
|
|
103
|
+
------------------------------------------------------------- Captured log call -------------------------------------------------------------
|
|
104
|
+
INFO revealtype-injector:hooks.py:26 Replaced reveal_type() from global import with <function revealtype_injector at 0x00000238DB923D00>
|
|
105
|
+
DEBUG revealtype-injector:main.py:60 Extraction OK: code='reveal_type(x)', result='x'
|
|
106
|
+
========================================================== short test summary info ==========================================================
|
|
107
|
+
FAILED tests/runtime/test_attrib.py::TestAttrib::test_superfluous - typeguard.TypeCheckError: item 3 is not an instance of str (from pyright)
|
|
108
|
+
============================================================= 1 failed in 3.38s =============================================================
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
|
|
80
112
|
## History
|
|
81
113
|
|
|
82
114
|
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.
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
1
6
|
`pytest-revealtype-injector` is a `pytest` plugin for replacing [`reveal_type()`](https://docs.python.org/3/library/typing.html#typing.reveal_type) calls inside test functions as something more sophisticated. It does the following tasks in parallel:
|
|
2
7
|
|
|
3
8
|
- Launch external static type checkers (`pyright` and `mypy`) and store `reveal_type` results.
|
|
@@ -49,6 +54,31 @@ def test_something():
|
|
|
49
54
|
|
|
50
55
|
2. `reveal_type()` calls have to stay in a single line, without anything else. This limitation comes from using [`eval` mode in AST parsing](https://docs.python.org/3/library/ast.html#ast.Expression).
|
|
51
56
|
|
|
57
|
+
## Logging
|
|
58
|
+
|
|
59
|
+
This plugin uses standard [`logging`](https://docs.python.org/3/library/logging.html) internally. `pytest -v` can be used to reveal `INFO` and `DEBUG` logs. Given following example:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
def test_superfluous(self) -> None:
|
|
63
|
+
x: list[str] = ['a', 'b', 'c', 1] # type: ignore # pyright: ignore
|
|
64
|
+
reveal_type(x)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Something like this will be shown as test result:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
...
|
|
71
|
+
raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}")
|
|
72
|
+
E typeguard.TypeCheckError: item 3 is not an instance of str (from pyright)
|
|
73
|
+
------------------------------------------------------------- Captured log call -------------------------------------------------------------
|
|
74
|
+
INFO revealtype-injector:hooks.py:26 Replaced reveal_type() from global import with <function revealtype_injector at 0x00000238DB923D00>
|
|
75
|
+
DEBUG revealtype-injector:main.py:60 Extraction OK: code='reveal_type(x)', result='x'
|
|
76
|
+
========================================================== short test summary info ==========================================================
|
|
77
|
+
FAILED tests/runtime/test_attrib.py::TestAttrib::test_superfluous - typeguard.TypeCheckError: item 3 is not an instance of str (from pyright)
|
|
78
|
+
============================================================= 1 failed in 3.38s =============================================================
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
52
82
|
## History
|
|
53
83
|
|
|
54
84
|
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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#:schema https://json.schemastore.org/pyproject.json
|
|
2
2
|
|
|
3
3
|
[build-system]
|
|
4
|
-
requires = ['hatchling']
|
|
4
|
+
requires = ['hatchling ~= 1.27']
|
|
5
5
|
build-backend = 'hatchling.build'
|
|
6
6
|
|
|
7
7
|
[project]
|
|
@@ -12,12 +12,15 @@ test functions with static and runtime type checking result comparison,
|
|
|
12
12
|
for confirming type annotation validity."""
|
|
13
13
|
readme = 'README.md'
|
|
14
14
|
requires-python = '>=3.10'
|
|
15
|
-
license =
|
|
15
|
+
license = 'MIT'
|
|
16
|
+
license-files = ['COPYING*']
|
|
16
17
|
dependencies = [
|
|
17
18
|
'mypy >= 1.11.2',
|
|
18
19
|
'pyright ~= 1.1',
|
|
19
20
|
'pytest >= 7.0',
|
|
20
|
-
'typeguard ~= 4.3'
|
|
21
|
+
'typeguard ~= 4.3',
|
|
22
|
+
# schema with annotation support is still unreleased
|
|
23
|
+
'schema == 0.7.7',
|
|
21
24
|
]
|
|
22
25
|
keywords = [
|
|
23
26
|
'pytest',
|
|
@@ -45,7 +48,6 @@ classifiers = [
|
|
|
45
48
|
'Programming Language :: Python :: 3.11',
|
|
46
49
|
'Programming Language :: Python :: 3.12',
|
|
47
50
|
'Programming Language :: Python :: 3.13',
|
|
48
|
-
'License :: OSI Approved :: MIT License',
|
|
49
51
|
'Topic :: Software Development :: Testing',
|
|
50
52
|
'Typing :: Typed',
|
|
51
53
|
]
|
|
@@ -75,11 +77,13 @@ packages = ["src/pytest_revealtype_injector"]
|
|
|
75
77
|
typeCheckingMode = 'strict'
|
|
76
78
|
enableTypeIgnoreComments = false
|
|
77
79
|
deprecateTypingAliases = true
|
|
80
|
+
reportMissingTypeStubs = false
|
|
78
81
|
|
|
79
82
|
[tool.mypy]
|
|
80
83
|
mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
|
|
81
84
|
packages = "pytest_revealtype_injector"
|
|
82
85
|
strict = true
|
|
86
|
+
ignore_missing_imports = true
|
|
83
87
|
|
|
84
88
|
[tool.ruff]
|
|
85
89
|
target-version = "py312"
|
|
@@ -117,12 +121,12 @@ addopts = [
|
|
|
117
121
|
"--tb=short",
|
|
118
122
|
"--import-mode=importlib",
|
|
119
123
|
]
|
|
120
|
-
markers = [
|
|
121
|
-
"slow: marks tests as slow",
|
|
122
|
-
]
|
|
123
124
|
testpaths = [
|
|
124
125
|
"tests",
|
|
125
126
|
]
|
|
127
|
+
pythonpath = [
|
|
128
|
+
"src",
|
|
129
|
+
]
|
|
126
130
|
|
|
127
131
|
# We only use version determination logic from python-semantic-release,
|
|
128
132
|
# and never does any permanent change with it
|
|
@@ -132,5 +136,6 @@ major_on_zero = false # switch on for 1.0.0
|
|
|
132
136
|
|
|
133
137
|
[tool.semantic_release.changelog]
|
|
134
138
|
exclude_commit_patterns = [
|
|
135
|
-
'Merge pull request #\d+ from',
|
|
139
|
+
'^Merge pull request #\d+ from',
|
|
140
|
+
'^(build|ci|style)(\(.+?\))?: ',
|
|
136
141
|
]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import importlib
|
|
3
3
|
import json
|
|
4
|
-
import logging
|
|
5
4
|
import pathlib
|
|
6
5
|
import re
|
|
7
6
|
from collections.abc import (
|
|
@@ -17,7 +16,9 @@ from typing import (
|
|
|
17
16
|
|
|
18
17
|
import mypy.api
|
|
19
18
|
import pytest
|
|
19
|
+
import schema as s
|
|
20
20
|
|
|
21
|
+
from ..log import get_logger
|
|
21
22
|
from ..models import (
|
|
22
23
|
FilePos,
|
|
23
24
|
NameCollectorBase,
|
|
@@ -26,8 +27,7 @@ from ..models import (
|
|
|
26
27
|
VarType,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
|
-
_logger =
|
|
30
|
-
_logger.setLevel(logging.INFO)
|
|
30
|
+
_logger = get_logger()
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class _MypyDiagObj(TypedDict):
|
|
@@ -67,7 +67,9 @@ class _NameCollector(NameCollectorBase):
|
|
|
67
67
|
_ = self.visit(node.value)
|
|
68
68
|
|
|
69
69
|
if resolved := getattr(self.collected[prefix], name, False):
|
|
70
|
-
|
|
70
|
+
code = ast.unparse(node)
|
|
71
|
+
self.collected[code] = resolved
|
|
72
|
+
_logger.debug(f"Mypy NameCollector resolved '{code}' as {resolved}")
|
|
71
73
|
return node
|
|
72
74
|
|
|
73
75
|
# For class defined in local scope, mypy just prepends test
|
|
@@ -100,10 +102,13 @@ class _NameCollector(NameCollectorBase):
|
|
|
100
102
|
pass
|
|
101
103
|
else:
|
|
102
104
|
self.collected[name] = mod
|
|
105
|
+
_logger.debug(f"Mypy NameCollector resolved '{name}' as {mod}")
|
|
103
106
|
return node
|
|
104
107
|
|
|
105
108
|
if hasattr(self.collected["typing"], name):
|
|
106
|
-
|
|
109
|
+
obj = getattr(self.collected["typing"], name)
|
|
110
|
+
self.collected[name] = obj
|
|
111
|
+
_logger.debug(f"Mypy NameCollector resolved '{name}' as {obj}")
|
|
107
112
|
return node
|
|
108
113
|
|
|
109
114
|
raise NameError(f'Cannot resolve "{name}"')
|
|
@@ -124,6 +129,19 @@ class _MypyAdapter(TypeCheckerAdapter):
|
|
|
124
129
|
id = "mypy"
|
|
125
130
|
typechecker_result = {}
|
|
126
131
|
_type_mesg_re = re.compile(r'^Revealed type is "(?P<type>.+?)"$')
|
|
132
|
+
_schema = s.Schema({
|
|
133
|
+
"file": str,
|
|
134
|
+
"line": int,
|
|
135
|
+
"column": int,
|
|
136
|
+
"message": str,
|
|
137
|
+
"hint": s.Or(str, s.Schema(None)),
|
|
138
|
+
"code": str,
|
|
139
|
+
"severity": s.Or(
|
|
140
|
+
s.Schema("note"),
|
|
141
|
+
s.Schema("warning"),
|
|
142
|
+
s.Schema("error"),
|
|
143
|
+
),
|
|
144
|
+
})
|
|
127
145
|
|
|
128
146
|
@classmethod
|
|
129
147
|
def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None:
|
|
@@ -148,8 +166,10 @@ class _MypyAdapter(TypeCheckerAdapter):
|
|
|
148
166
|
# So-called mypy json output is merely a line-by-line
|
|
149
167
|
# transformation of plain text output into json object
|
|
150
168
|
for line in stdout.splitlines():
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
if len(line) <= 2 or line[0] != "{":
|
|
170
|
+
continue
|
|
171
|
+
obj = json.loads(line)
|
|
172
|
+
diag = cast(_MypyDiagObj, cls._schema.validate(obj))
|
|
153
173
|
filename = pathlib.Path(diag["file"]).name
|
|
154
174
|
pos = FilePos(filename, diag["line"])
|
|
155
175
|
if diag["severity"] != "note":
|
|
@@ -196,7 +216,6 @@ class _MypyAdapter(TypeCheckerAdapter):
|
|
|
196
216
|
|
|
197
217
|
@classmethod
|
|
198
218
|
def set_config_file(cls, config: pytest.Config) -> None:
|
|
199
|
-
# Mypy doesn't have a default config file
|
|
200
219
|
if (path_str := config.option.revealtype_mypy_config) is None:
|
|
201
220
|
_logger.info("Using default mypy configuration")
|
|
202
221
|
return
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import json
|
|
3
|
-
import logging
|
|
4
3
|
import pathlib
|
|
5
4
|
import re
|
|
6
5
|
import shutil
|
|
@@ -11,10 +10,15 @@ from collections.abc import (
|
|
|
11
10
|
from typing import (
|
|
12
11
|
Any,
|
|
13
12
|
ForwardRef,
|
|
13
|
+
Literal,
|
|
14
|
+
TypedDict,
|
|
15
|
+
cast,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
import pytest
|
|
19
|
+
import schema as s
|
|
17
20
|
|
|
21
|
+
from ..log import get_logger
|
|
18
22
|
from ..models import (
|
|
19
23
|
FilePos,
|
|
20
24
|
NameCollectorBase,
|
|
@@ -23,8 +27,22 @@ from ..models import (
|
|
|
23
27
|
VarType,
|
|
24
28
|
)
|
|
25
29
|
|
|
26
|
-
_logger =
|
|
27
|
-
|
|
30
|
+
_logger = get_logger()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _PyrightDiagPosition(TypedDict):
|
|
34
|
+
line: int
|
|
35
|
+
character: int
|
|
36
|
+
|
|
37
|
+
class _PyrightDiagRange(TypedDict):
|
|
38
|
+
start: _PyrightDiagPosition
|
|
39
|
+
end: _PyrightDiagPosition
|
|
40
|
+
|
|
41
|
+
class _PyrightDiagItem(TypedDict):
|
|
42
|
+
file: str
|
|
43
|
+
severity: Literal["information", "warning", "error"]
|
|
44
|
+
message: str
|
|
45
|
+
range: _PyrightDiagRange
|
|
28
46
|
|
|
29
47
|
|
|
30
48
|
class _NameCollector(NameCollectorBase):
|
|
@@ -36,9 +54,12 @@ class _NameCollector(NameCollectorBase):
|
|
|
36
54
|
eval(name, self._globalns, self._localns | self.collected)
|
|
37
55
|
except NameError:
|
|
38
56
|
for m in ("typing", "typing_extensions"):
|
|
39
|
-
if hasattr(self.collected[m], name):
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
if not hasattr(self.collected[m], name):
|
|
58
|
+
continue
|
|
59
|
+
obj = getattr(self.collected[m], name)
|
|
60
|
+
self.collected[name] = obj
|
|
61
|
+
_logger.debug(f"Pyright NameCollector resolved '{name}' as {obj}")
|
|
62
|
+
return node
|
|
42
63
|
raise
|
|
43
64
|
return node
|
|
44
65
|
|
|
@@ -47,6 +68,21 @@ class _PyrightAdapter(TypeCheckerAdapter):
|
|
|
47
68
|
id = "pyright"
|
|
48
69
|
typechecker_result = {}
|
|
49
70
|
_type_mesg_re = re.compile('^Type of "(?P<var>.+?)" is "(?P<type>.+?)"$')
|
|
71
|
+
# We only care about diagnostic messages that contain type information.
|
|
72
|
+
# Metadata not specified here.
|
|
73
|
+
_schema = s.Schema({
|
|
74
|
+
"file": str,
|
|
75
|
+
"severity": s.Or(
|
|
76
|
+
s.Schema("information"),
|
|
77
|
+
s.Schema("warning"),
|
|
78
|
+
s.Schema("error"),
|
|
79
|
+
),
|
|
80
|
+
"message": str,
|
|
81
|
+
"range": {
|
|
82
|
+
"start": {"line": int, "character": int},
|
|
83
|
+
"end": {"line": int, "character": int},
|
|
84
|
+
},
|
|
85
|
+
})
|
|
50
86
|
|
|
51
87
|
@classmethod
|
|
52
88
|
def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None:
|
|
@@ -67,22 +103,17 @@ class _PyrightAdapter(TypeCheckerAdapter):
|
|
|
67
103
|
if len(proc.stderr):
|
|
68
104
|
raise TypeCheckerError(proc.stderr.decode(), None, None)
|
|
69
105
|
|
|
70
|
-
# TODO Pyright json schema validation
|
|
71
106
|
report = json.loads(proc.stdout)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
continue
|
|
76
|
-
# Pyright report lineno is 0-based,
|
|
77
|
-
# OTOH python frame lineno is 1-based
|
|
78
|
-
lineno = diag["range"]["start"]["line"] + 1
|
|
79
|
-
filename = pathlib.Path(diag["file"]).name
|
|
80
|
-
raise TypeCheckerError(diag["message"], filename, lineno)
|
|
81
|
-
for diag in report["generalDiagnostics"]:
|
|
82
|
-
if diag["severity"] != "information":
|
|
107
|
+
for item in report["generalDiagnostics"]:
|
|
108
|
+
diag = cast(_PyrightDiagItem, cls._schema.validate(item))
|
|
109
|
+
if diag["severity"] != ("error" if proc.returncode else "information"):
|
|
83
110
|
continue
|
|
111
|
+
# Pyright report lineno is 0-based, while
|
|
112
|
+
# python frame lineno is 1-based
|
|
84
113
|
lineno = diag["range"]["start"]["line"] + 1
|
|
85
114
|
filename = pathlib.Path(diag["file"]).name
|
|
115
|
+
if proc.returncode:
|
|
116
|
+
raise TypeCheckerError(diag["message"], filename, lineno)
|
|
86
117
|
if (m := cls._type_mesg_re.match(diag["message"])) is None:
|
|
87
118
|
continue
|
|
88
119
|
pos = FilePos(filename, lineno)
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
import logging
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
7
6
|
|
|
8
|
-
from . import adapter
|
|
7
|
+
from . import adapter, log
|
|
9
8
|
from .main import revealtype_injector
|
|
10
9
|
|
|
11
|
-
_logger =
|
|
12
|
-
_logger.setLevel(logging.INFO)
|
|
10
|
+
_logger = log.get_logger()
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
|
|
@@ -65,6 +63,7 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
65
63
|
|
|
66
64
|
|
|
67
65
|
def pytest_configure(config: pytest.Config) -> None:
|
|
66
|
+
_logger.setLevel(config.get_verbosity(config.VERBOSITY_TEST_CASES))
|
|
68
67
|
# Forget config stash, it can't store collection of unserialized objects
|
|
69
68
|
for adp in adapter.discovery():
|
|
70
69
|
if config.option.revealtype_disable_adapter == adp.id:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
LOGGER_NAME = "revealtype-injector"
|
|
4
|
+
|
|
5
|
+
_logger = logging.getLogger(LOGGER_NAME)
|
|
6
|
+
|
|
7
|
+
# Mapping of pytest.Config.VERBOSITY_TEST_CASES to logging levels
|
|
8
|
+
_verbosity_map = {
|
|
9
|
+
0: logging.WARNING,
|
|
10
|
+
1: logging.INFO,
|
|
11
|
+
2: logging.DEBUG,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def get_logger() -> logging.Logger:
|
|
15
|
+
return _logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_verbosity(verbosity: int) -> None:
|
|
19
|
+
_logger.setLevel(_verbosity_map.get(verbosity, logging.DEBUG))
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import inspect
|
|
3
|
-
import logging
|
|
4
3
|
import pathlib
|
|
5
4
|
import sys
|
|
6
5
|
from typing import (
|
|
@@ -15,7 +14,7 @@ from typeguard import (
|
|
|
15
14
|
check_type_internal,
|
|
16
15
|
)
|
|
17
16
|
|
|
18
|
-
from . import adapter
|
|
17
|
+
from . import adapter, log
|
|
19
18
|
from .models import (
|
|
20
19
|
FilePos,
|
|
21
20
|
TypeCheckerError,
|
|
@@ -24,8 +23,7 @@ from .models import (
|
|
|
24
23
|
|
|
25
24
|
_T = TypeVar("_T")
|
|
26
25
|
|
|
27
|
-
_logger =
|
|
28
|
-
_logger.setLevel(logging.WARN)
|
|
26
|
+
_logger = log.get_logger()
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
class RevealTypeExtractor(ast.NodeVisitor):
|
|
@@ -42,8 +40,15 @@ class RevealTypeExtractor(ast.NodeVisitor):
|
|
|
42
40
|
|
|
43
41
|
|
|
44
42
|
def _get_var_name(frame: inspect.Traceback) -> str | None:
|
|
43
|
+
filename = pathlib.Path(frame.filename)
|
|
44
|
+
if not filename.exists():
|
|
45
|
+
_logger.warning(
|
|
46
|
+
f"Stack frame points to file '{filename}' "
|
|
47
|
+
"which doesn't exist on local system."
|
|
48
|
+
)
|
|
45
49
|
ctxt, idx = frame.code_context, frame.index
|
|
46
|
-
assert ctxt is not None
|
|
50
|
+
assert ctxt is not None
|
|
51
|
+
assert idx is not None
|
|
47
52
|
code = ctxt[idx].strip()
|
|
48
53
|
|
|
49
54
|
walker = RevealTypeExtractor()
|
|
@@ -51,7 +56,9 @@ def _get_var_name(frame: inspect.Traceback) -> str | None:
|
|
|
51
56
|
# as much restriction on test code as 'eval' mode does.
|
|
52
57
|
walker.visit(ast.parse(code, mode="eval"))
|
|
53
58
|
assert walker.target is not None
|
|
54
|
-
|
|
59
|
+
result = ast.get_source_segment(code, walker.target)
|
|
60
|
+
_logger.debug(f"Extraction OK: {code=}, {result=}")
|
|
61
|
+
return result
|
|
55
62
|
|
|
56
63
|
|
|
57
64
|
def revealtype_injector(var: _T) -> _T:
|
|
@@ -134,7 +141,8 @@ def revealtype_injector(var: _T) -> _T:
|
|
|
134
141
|
try:
|
|
135
142
|
check_type_internal(var, ref, memo)
|
|
136
143
|
except TypeCheckError as e:
|
|
137
|
-
|
|
144
|
+
# Only args[0] contains message
|
|
145
|
+
e.args = (e.args[0] + f" (from {adp.id})",) + e.args[1:]
|
|
138
146
|
raise
|
|
139
147
|
|
|
140
148
|
return var
|
|
@@ -15,6 +15,7 @@ from typing import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
import pytest
|
|
18
|
+
from schema import Schema
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class FilePos(NamedTuple):
|
|
@@ -91,6 +92,7 @@ class TypeCheckerAdapter:
|
|
|
91
92
|
# {('file.py', 10): ('var_name', 'list[str]'), ...}
|
|
92
93
|
typechecker_result: ClassVar[dict[FilePos, VarType]]
|
|
93
94
|
_type_mesg_re: ClassVar[re.Pattern[str]]
|
|
95
|
+
_schema: ClassVar[Schema]
|
|
94
96
|
|
|
95
97
|
@classmethod
|
|
96
98
|
@abc.abstractmethod
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_plugins = ["pytester"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_basic(pytester: pytest.Pytester) -> None:
|
|
5
|
+
pytester.makeconftest(
|
|
6
|
+
"pytest_plugins = ['pytest_revealtype_injector.plugin']"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
import pytest
|
|
13
|
+
from typeguard import TypeCheckError
|
|
14
|
+
|
|
15
|
+
if sys.version_info >= (3, 11):
|
|
16
|
+
from typing import reveal_type
|
|
17
|
+
else:
|
|
18
|
+
from typing_extensions import reveal_type
|
|
19
|
+
|
|
20
|
+
def test_inferred():
|
|
21
|
+
x = 1
|
|
22
|
+
reveal_type(x)
|
|
23
|
+
|
|
24
|
+
def test_bad_inline_hint():
|
|
25
|
+
x: str = 1 # type: ignore # pyright: ignore
|
|
26
|
+
with pytest.raises(TypeCheckError, match='is not an instance of str'):
|
|
27
|
+
reveal_type(x)
|
|
28
|
+
"""
|
|
29
|
+
)
|
|
30
|
+
result = pytester.runpytest()
|
|
31
|
+
result.assert_outcomes(passed=2)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_import_as(pytester: pytest.Pytester) -> None:
|
|
35
|
+
pytester.makeconftest(
|
|
36
|
+
"pytest_plugins = ['pytest_revealtype_injector.plugin']"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
|
|
40
|
+
"""
|
|
41
|
+
import sys
|
|
42
|
+
import pytest
|
|
43
|
+
from typeguard import TypeCheckError
|
|
44
|
+
|
|
45
|
+
if sys.version_info >= (3, 11):
|
|
46
|
+
from typing import reveal_type as rt
|
|
47
|
+
else:
|
|
48
|
+
from typing_extensions import reveal_type as rt
|
|
49
|
+
|
|
50
|
+
def test_inferred():
|
|
51
|
+
x = 1
|
|
52
|
+
rt(x)
|
|
53
|
+
|
|
54
|
+
def test_bad_inline_hint():
|
|
55
|
+
x: str = 1 # type: ignore # pyright: ignore
|
|
56
|
+
with pytest.raises(TypeCheckError, match='is not an instance of str'):
|
|
57
|
+
rt(x)
|
|
58
|
+
"""
|
|
59
|
+
)
|
|
60
|
+
result = pytester.runpytest()
|
|
61
|
+
result.assert_outcomes(passed=2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_import_module_as(pytester: pytest.Pytester) -> None:
|
|
65
|
+
pytester.makeconftest(
|
|
66
|
+
"pytest_plugins = ['pytest_revealtype_injector.plugin']"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
|
|
70
|
+
"""
|
|
71
|
+
import sys
|
|
72
|
+
import pytest
|
|
73
|
+
from typeguard import TypeCheckError
|
|
74
|
+
|
|
75
|
+
if sys.version_info >= (3, 11):
|
|
76
|
+
import typing as t
|
|
77
|
+
else:
|
|
78
|
+
import typing_extensions as t
|
|
79
|
+
|
|
80
|
+
def test_inferred():
|
|
81
|
+
x = 1
|
|
82
|
+
t.reveal_type(x)
|
|
83
|
+
|
|
84
|
+
def test_bad_inline_hint():
|
|
85
|
+
x: str = 1 # type: ignore # pyright: ignore
|
|
86
|
+
with pytest.raises(TypeCheckError, match='is not an instance of str'):
|
|
87
|
+
t.reveal_type(x)
|
|
88
|
+
"""
|
|
89
|
+
)
|
|
90
|
+
result = pytester.runpytest()
|
|
91
|
+
result.assert_outcomes(passed=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|