pytest-revealtype-injector 0.1.0__tar.gz → 0.2.1__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.1.0 → pytest_revealtype_injector-0.2.1}/PKG-INFO +11 -4
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/README.md +5 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/pyproject.toml +17 -7
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/__init__.py +1 -1
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/adapter/mypy_.py +35 -4
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/adapter/pyright_.py +41 -12
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/models.py +2 -0
- pytest_revealtype_injector-0.2.1/tests/conftest.py +1 -0
- pytest_revealtype_injector-0.2.1/tests/test_basic.py +31 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/.gitignore +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/COPYING +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/COPYING.mit +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/adapter/__init__.py +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/hooks.py +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/main.py +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/plugin.py +0 -0
- {pytest_revealtype_injector-0.1.0 → pytest_revealtype_injector-0.2.1}/src/pytest_revealtype_injector/py.typed +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-revealtype-injector
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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.
|
|
@@ -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.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#:schema https://json.schemastore.org/pyproject.json
|
|
2
2
|
|
|
3
3
|
[build-system]
|
|
4
|
-
|
|
4
|
+
# replace with 'hatchling >= 1.27.0' when things are sorted out
|
|
5
|
+
requires = ['hatchling @ git+https://github.com/pypa/hatch.git@72d57279ac8fa58b1981734b58d55cf607e84656#subdirectory=backend']
|
|
5
6
|
build-backend = 'hatchling.build'
|
|
6
7
|
|
|
7
8
|
[project]
|
|
@@ -12,12 +13,15 @@ test functions with static and runtime type checking result comparison,
|
|
|
12
13
|
for confirming type annotation validity."""
|
|
13
14
|
readme = 'README.md'
|
|
14
15
|
requires-python = '>=3.10'
|
|
15
|
-
license =
|
|
16
|
+
license = 'MIT'
|
|
17
|
+
license-files = ['COPYING*']
|
|
16
18
|
dependencies = [
|
|
17
19
|
'mypy >= 1.11.2',
|
|
18
20
|
'pyright ~= 1.1',
|
|
19
21
|
'pytest >= 7.0',
|
|
20
|
-
'typeguard ~= 4.3'
|
|
22
|
+
'typeguard ~= 4.3',
|
|
23
|
+
# schema with annotation support is still unreleased
|
|
24
|
+
'schema == 0.7.7',
|
|
21
25
|
]
|
|
22
26
|
keywords = [
|
|
23
27
|
'pytest',
|
|
@@ -45,7 +49,6 @@ classifiers = [
|
|
|
45
49
|
'Programming Language :: Python :: 3.11',
|
|
46
50
|
'Programming Language :: Python :: 3.12',
|
|
47
51
|
'Programming Language :: Python :: 3.13',
|
|
48
|
-
'License :: OSI Approved :: MIT License',
|
|
49
52
|
'Topic :: Software Development :: Testing',
|
|
50
53
|
'Typing :: Typed',
|
|
51
54
|
]
|
|
@@ -75,11 +78,13 @@ packages = ["src/pytest_revealtype_injector"]
|
|
|
75
78
|
typeCheckingMode = 'strict'
|
|
76
79
|
enableTypeIgnoreComments = false
|
|
77
80
|
deprecateTypingAliases = true
|
|
81
|
+
reportMissingTypeStubs = false
|
|
78
82
|
|
|
79
83
|
[tool.mypy]
|
|
80
84
|
mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
|
|
81
85
|
packages = "pytest_revealtype_injector"
|
|
82
86
|
strict = true
|
|
87
|
+
ignore_missing_imports = true
|
|
83
88
|
|
|
84
89
|
[tool.ruff]
|
|
85
90
|
target-version = "py312"
|
|
@@ -117,12 +122,12 @@ addopts = [
|
|
|
117
122
|
"--tb=short",
|
|
118
123
|
"--import-mode=importlib",
|
|
119
124
|
]
|
|
120
|
-
markers = [
|
|
121
|
-
"slow: marks tests as slow",
|
|
122
|
-
]
|
|
123
125
|
testpaths = [
|
|
124
126
|
"tests",
|
|
125
127
|
]
|
|
128
|
+
pythonpath = [
|
|
129
|
+
"src",
|
|
130
|
+
]
|
|
126
131
|
|
|
127
132
|
# We only use version determination logic from python-semantic-release,
|
|
128
133
|
# and never does any permanent change with it
|
|
@@ -130,3 +135,8 @@ testpaths = [
|
|
|
130
135
|
version_variables = ['src/pytest_revealtype_injector/__init__.py:__version__']
|
|
131
136
|
major_on_zero = false # switch on for 1.0.0
|
|
132
137
|
|
|
138
|
+
[tool.semantic_release.changelog]
|
|
139
|
+
exclude_commit_patterns = [
|
|
140
|
+
'^Merge pull request #\d+ from',
|
|
141
|
+
'^(build|ci|style)(\(.+?\))?: ',
|
|
142
|
+
]
|
|
@@ -17,6 +17,7 @@ from typing import (
|
|
|
17
17
|
|
|
18
18
|
import mypy.api
|
|
19
19
|
import pytest
|
|
20
|
+
import schema as s
|
|
20
21
|
|
|
21
22
|
from ..models import (
|
|
22
23
|
FilePos,
|
|
@@ -124,6 +125,19 @@ class _MypyAdapter(TypeCheckerAdapter):
|
|
|
124
125
|
id = "mypy"
|
|
125
126
|
typechecker_result = {}
|
|
126
127
|
_type_mesg_re = re.compile(r'^Revealed type is "(?P<type>.+?)"$')
|
|
128
|
+
_schema = s.Schema({
|
|
129
|
+
"file": str,
|
|
130
|
+
"line": int,
|
|
131
|
+
"column": int,
|
|
132
|
+
"message": str,
|
|
133
|
+
"hint": s.Or(str, s.Schema(None)),
|
|
134
|
+
"code": str,
|
|
135
|
+
"severity": s.Or(
|
|
136
|
+
s.Schema("note"),
|
|
137
|
+
s.Schema("warning"),
|
|
138
|
+
s.Schema("error"),
|
|
139
|
+
),
|
|
140
|
+
})
|
|
127
141
|
|
|
128
142
|
@classmethod
|
|
129
143
|
def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None:
|
|
@@ -148,8 +162,8 @@ class _MypyAdapter(TypeCheckerAdapter):
|
|
|
148
162
|
# So-called mypy json output is merely a line-by-line
|
|
149
163
|
# transformation of plain text output into json object
|
|
150
164
|
for line in stdout.splitlines():
|
|
151
|
-
|
|
152
|
-
diag = cast(_MypyDiagObj,
|
|
165
|
+
obj = json.loads(line)
|
|
166
|
+
diag = cast(_MypyDiagObj, cls._schema.validate(obj))
|
|
153
167
|
filename = pathlib.Path(diag["file"]).name
|
|
154
168
|
pos = FilePos(filename, diag["line"])
|
|
155
169
|
if diag["severity"] != "note":
|
|
@@ -168,8 +182,25 @@ class _MypyAdapter(TypeCheckerAdapter):
|
|
|
168
182
|
# Try stripping those character and pray we get something
|
|
169
183
|
# usable for evaluation
|
|
170
184
|
expression = m["type"].translate({ord(c): None for c in "*?="})
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
try:
|
|
186
|
+
# Unlike pyright, mypy output doesn't contain variable name
|
|
187
|
+
cls.typechecker_result[pos] = VarType(None, ForwardRef(expression))
|
|
188
|
+
except SyntaxError as e:
|
|
189
|
+
if (
|
|
190
|
+
m := re.fullmatch(r"<Deleted '(?P<var>.+)'>", expression)
|
|
191
|
+
) is not None:
|
|
192
|
+
raise TypeCheckerError(
|
|
193
|
+
"{} does not support reusing deleted variable '{}'".format(
|
|
194
|
+
cls.id, m["var"]
|
|
195
|
+
),
|
|
196
|
+
diag["file"],
|
|
197
|
+
diag["line"],
|
|
198
|
+
) from e
|
|
199
|
+
raise TypeCheckerError(
|
|
200
|
+
f"Cannot parse type expression '{expression}'",
|
|
201
|
+
diag["file"],
|
|
202
|
+
diag["line"],
|
|
203
|
+
) from e
|
|
173
204
|
|
|
174
205
|
@classmethod
|
|
175
206
|
def create_collector(
|
|
@@ -11,9 +11,13 @@ from collections.abc import (
|
|
|
11
11
|
from typing import (
|
|
12
12
|
Any,
|
|
13
13
|
ForwardRef,
|
|
14
|
+
Literal,
|
|
15
|
+
TypedDict,
|
|
16
|
+
cast,
|
|
14
17
|
)
|
|
15
18
|
|
|
16
19
|
import pytest
|
|
20
|
+
import schema as s
|
|
17
21
|
|
|
18
22
|
from ..models import (
|
|
19
23
|
FilePos,
|
|
@@ -27,6 +31,21 @@ _logger = logging.getLogger(__name__)
|
|
|
27
31
|
_logger.setLevel(logging.INFO)
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
class _PyrightDiagPosition(TypedDict):
|
|
35
|
+
line: int
|
|
36
|
+
character: int
|
|
37
|
+
|
|
38
|
+
class _PyrightDiagRange(TypedDict):
|
|
39
|
+
start: _PyrightDiagPosition
|
|
40
|
+
end: _PyrightDiagPosition
|
|
41
|
+
|
|
42
|
+
class _PyrightDiagItem(TypedDict):
|
|
43
|
+
file: str
|
|
44
|
+
severity: Literal["information", "warning", "error"]
|
|
45
|
+
message: str
|
|
46
|
+
range: _PyrightDiagRange
|
|
47
|
+
|
|
48
|
+
|
|
30
49
|
class _NameCollector(NameCollectorBase):
|
|
31
50
|
# Pyright inferred type results always contain bare names only,
|
|
32
51
|
# so don't need to bother with visit_Attribute()
|
|
@@ -47,6 +66,21 @@ class _PyrightAdapter(TypeCheckerAdapter):
|
|
|
47
66
|
id = "pyright"
|
|
48
67
|
typechecker_result = {}
|
|
49
68
|
_type_mesg_re = re.compile('^Type of "(?P<var>.+?)" is "(?P<type>.+?)"$')
|
|
69
|
+
# We only care about diagnostic messages that contain type information.
|
|
70
|
+
# Metadata not specified here.
|
|
71
|
+
_schema = s.Schema({
|
|
72
|
+
"file": str,
|
|
73
|
+
"severity": s.Or(
|
|
74
|
+
s.Schema("information"),
|
|
75
|
+
s.Schema("warning"),
|
|
76
|
+
s.Schema("error"),
|
|
77
|
+
),
|
|
78
|
+
"message": str,
|
|
79
|
+
"range": {
|
|
80
|
+
"start": {"line": int, "character": int},
|
|
81
|
+
"end": {"line": int, "character": int},
|
|
82
|
+
},
|
|
83
|
+
})
|
|
50
84
|
|
|
51
85
|
@classmethod
|
|
52
86
|
def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None:
|
|
@@ -67,22 +101,17 @@ class _PyrightAdapter(TypeCheckerAdapter):
|
|
|
67
101
|
if len(proc.stderr):
|
|
68
102
|
raise TypeCheckerError(proc.stderr.decode(), None, None)
|
|
69
103
|
|
|
70
|
-
# TODO Pyright json schema validation
|
|
71
104
|
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":
|
|
105
|
+
for item in report["generalDiagnostics"]:
|
|
106
|
+
diag = cast(_PyrightDiagItem, cls._schema.validate(item))
|
|
107
|
+
if diag["severity"] != ("error" if proc.returncode else "information"):
|
|
83
108
|
continue
|
|
109
|
+
# Pyright report lineno is 0-based, while
|
|
110
|
+
# python frame lineno is 1-based
|
|
84
111
|
lineno = diag["range"]["start"]["line"] + 1
|
|
85
112
|
filename = pathlib.Path(diag["file"]).name
|
|
113
|
+
if proc.returncode:
|
|
114
|
+
raise TypeCheckerError(diag["message"], filename, lineno)
|
|
86
115
|
if (m := cls._type_mesg_re.match(diag["message"])) is None:
|
|
87
116
|
continue
|
|
88
117
|
pos = FilePos(filename, lineno)
|
|
@@ -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,31 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_basic(pytester: pytest.Pytester):
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|