pytest-revealtype-injector 0.4.1__tar.gz → 0.5.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.4.1 → pytest_revealtype_injector-0.5.0}/PKG-INFO +1 -1
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/__init__.py +1 -1
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/hooks.py +19 -7
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/main.py +23 -14
- pytest_revealtype_injector-0.5.0/tests/test_ast_mode.py +64 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/.gitignore +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/COPYING +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/COPYING.mit +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/README.md +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/pyproject.toml +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/__init__.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/basedpyright_.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/mypy_.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/pyright_.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/log.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/models.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/plugin.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/py.typed +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/tests/conftest.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/tests/test_import.py +0 -0
- {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.0}/tests/test_options.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-revealtype-injector
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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>
|
|
@@ -17,7 +17,6 @@ adapter_stash_key: pytest.StashKey[set[TypeCheckerAdapter]]
|
|
|
17
17
|
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
|
|
18
18
|
assert pyfuncitem.module is not None
|
|
19
19
|
adapters = pyfuncitem.config.stash[adapter_stash_key].copy()
|
|
20
|
-
injected = functools.partial(revealtype_injector, adapters=adapters)
|
|
21
20
|
|
|
22
21
|
for name in dir(pyfuncitem.module):
|
|
23
22
|
if name.startswith("__") or name.startswith("@py"):
|
|
@@ -25,21 +24,32 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
|
|
|
25
24
|
|
|
26
25
|
item = getattr(pyfuncitem.module, name)
|
|
27
26
|
if inspect.isfunction(item):
|
|
28
|
-
if item.__name__
|
|
27
|
+
if item.__name__ != "reveal_type" or item.__module__ not in {
|
|
29
28
|
"typing",
|
|
30
29
|
"typing_extensions",
|
|
31
30
|
}:
|
|
32
|
-
setattr(pyfuncitem.module, name, injected)
|
|
33
|
-
_logger.info(f"Replaced {name}() from global import with {injected}")
|
|
34
31
|
continue
|
|
32
|
+
injected = functools.partial(
|
|
33
|
+
revealtype_injector,
|
|
34
|
+
adapters=adapters,
|
|
35
|
+
rt_funcname=name,
|
|
36
|
+
)
|
|
37
|
+
setattr(pyfuncitem.module, name, injected)
|
|
38
|
+
_logger.info(f"Replaced {name}() from global import with {injected}")
|
|
39
|
+
break
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
elif inspect.ismodule(item):
|
|
37
42
|
if item.__name__ not in {"typing", "typing_extensions"}:
|
|
38
43
|
continue
|
|
39
44
|
assert hasattr(item, "reveal_type")
|
|
45
|
+
injected = functools.partial(
|
|
46
|
+
revealtype_injector,
|
|
47
|
+
adapters=adapters,
|
|
48
|
+
rt_funcname=f"{name}.reveal_type",
|
|
49
|
+
)
|
|
40
50
|
setattr(item, "reveal_type", injected)
|
|
41
51
|
_logger.info(f"Replaced {name}.reveal_type() with {injected}")
|
|
42
|
-
|
|
52
|
+
break
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
def pytest_collection_finish(session: pytest.Session) -> None:
|
|
@@ -49,7 +59,9 @@ def pytest_collection_finish(session: pytest.Session) -> None:
|
|
|
49
59
|
adp.run_typechecker_on(files)
|
|
50
60
|
except Exception as e:
|
|
51
61
|
_logger.error(f"({adp.id}) {e}")
|
|
52
|
-
pytest.exit(
|
|
62
|
+
pytest.exit(
|
|
63
|
+
f"({type(e).__name__}) " + str(e), pytest.ExitCode.INTERNAL_ERROR
|
|
64
|
+
)
|
|
53
65
|
else:
|
|
54
66
|
_logger.info(f"({adp.id}) Type checker ran successfully")
|
|
55
67
|
|
|
@@ -27,41 +27,50 @@ _logger = log.get_logger()
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class RevealTypeExtractor(ast.NodeVisitor):
|
|
30
|
-
|
|
30
|
+
def __init__(self, funcname: str | None = None) -> None:
|
|
31
|
+
self._rt_funcname = funcname # normally "reveal_type"
|
|
32
|
+
self.target: ast.expr | None = None
|
|
31
33
|
|
|
32
34
|
def visit_Call(self, node: ast.Call) -> Any:
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
# If we don't know how reveal_type is called, just assume
|
|
36
|
+
# it is the outmost call.
|
|
37
|
+
if not self._rt_funcname:
|
|
38
|
+
self.target = node.args[0]
|
|
39
|
+
return node
|
|
40
|
+
if ast.unparse(node.func).strip() == self._rt_funcname:
|
|
41
|
+
self.target = node.args[0]
|
|
42
|
+
else:
|
|
43
|
+
self.generic_visit(node)
|
|
40
44
|
|
|
41
45
|
|
|
42
|
-
def _get_var_name(frame: inspect.Traceback) -> str | None:
|
|
46
|
+
def _get_var_name(frame: inspect.Traceback, rt_funcname: str) -> str | None:
|
|
43
47
|
filename = pathlib.Path(frame.filename)
|
|
44
48
|
if not filename.exists():
|
|
45
49
|
_logger.warning(
|
|
46
50
|
f"Stack frame points to file '{filename}' "
|
|
47
51
|
"which doesn't exist on local system."
|
|
48
52
|
)
|
|
53
|
+
# TODO is it possible to have multiline reveal_type()?
|
|
49
54
|
ctxt, idx = frame.code_context, frame.index
|
|
50
55
|
assert ctxt is not None
|
|
51
56
|
assert idx is not None
|
|
52
57
|
code = ctxt[idx].strip()
|
|
53
58
|
|
|
54
|
-
walker = RevealTypeExtractor()
|
|
55
|
-
#
|
|
59
|
+
walker = RevealTypeExtractor(rt_funcname)
|
|
60
|
+
# 'exec' mode results in more complex AST but doesn't impose
|
|
56
61
|
# as much restriction on test code as 'eval' mode does.
|
|
57
|
-
walker.visit(ast.parse(code, mode="
|
|
62
|
+
walker.visit(ast.parse(code, mode="exec"))
|
|
58
63
|
assert walker.target is not None
|
|
59
64
|
result = ast.get_source_segment(code, walker.target)
|
|
60
65
|
_logger.debug(f"Extraction OK: {code=}, {result=}")
|
|
61
66
|
return result
|
|
62
67
|
|
|
63
68
|
|
|
64
|
-
def revealtype_injector(
|
|
69
|
+
def revealtype_injector(
|
|
70
|
+
var: _T,
|
|
71
|
+
adapters: set[TypeCheckerAdapter],
|
|
72
|
+
rt_funcname: str,
|
|
73
|
+
) -> _T:
|
|
65
74
|
"""Replacement of `reveal_type()` that matches static and runtime type
|
|
66
75
|
checking result
|
|
67
76
|
|
|
@@ -99,7 +108,7 @@ def revealtype_injector(var: _T, adapters: set[TypeCheckerAdapter]) -> _T:
|
|
|
99
108
|
# get data from my caller, not mine
|
|
100
109
|
caller_frame = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
|
|
101
110
|
caller = inspect.getframeinfo(caller_frame)
|
|
102
|
-
var_name = _get_var_name(caller)
|
|
111
|
+
var_name = _get_var_name(caller, rt_funcname)
|
|
103
112
|
pos = FilePos(pathlib.Path(caller.filename).name, caller.lineno)
|
|
104
113
|
|
|
105
114
|
globalns = caller_frame.f_globals
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestAstExecMode:
|
|
7
|
+
def test_return_result(self, pytester: pytest.Pytester) -> None:
|
|
8
|
+
pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
|
|
9
|
+
pytester.makepyprojecttoml(
|
|
10
|
+
"""
|
|
11
|
+
[tool.basedpyright]
|
|
12
|
+
reportUnreachable = false
|
|
13
|
+
"""
|
|
14
|
+
)
|
|
15
|
+
pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
|
|
16
|
+
"""
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
from typing import reveal_type
|
|
21
|
+
else:
|
|
22
|
+
from typing_extensions import reveal_type
|
|
23
|
+
|
|
24
|
+
def test_result_int() -> None:
|
|
25
|
+
x = 1
|
|
26
|
+
assert reveal_type(x) == 0 + 1
|
|
27
|
+
|
|
28
|
+
def test_result_str() -> None:
|
|
29
|
+
x = "foo"
|
|
30
|
+
if hasattr(x, "lower"):
|
|
31
|
+
assert reveal_type(x.lower()) is not None
|
|
32
|
+
"""
|
|
33
|
+
)
|
|
34
|
+
result = pytester.runpytest("--tb=short", "-v")
|
|
35
|
+
result.assert_outcomes(passed=2)
|
|
36
|
+
|
|
37
|
+
def test_nested_call(self, pytester: pytest.Pytester) -> None:
|
|
38
|
+
pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
|
|
39
|
+
pytester.makepyprojecttoml(
|
|
40
|
+
"""
|
|
41
|
+
[tool.basedpyright]
|
|
42
|
+
reportUnreachable = false
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
|
|
46
|
+
"""
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
if sys.version_info >= (3, 11):
|
|
50
|
+
from typing import reveal_type as rt
|
|
51
|
+
else:
|
|
52
|
+
from typing_extensions import reveal_type as rt
|
|
53
|
+
|
|
54
|
+
def test_inner_call() -> None:
|
|
55
|
+
x = 42
|
|
56
|
+
assert int(rt(str(x).upper())) == x
|
|
57
|
+
|
|
58
|
+
def test_outer_call() -> None:
|
|
59
|
+
x = "42"
|
|
60
|
+
assert rt(str(int(x)).upper()) == x
|
|
61
|
+
"""
|
|
62
|
+
)
|
|
63
|
+
result = pytester.runpytest("--tb=short", "-v")
|
|
64
|
+
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|