pytest-revealtype-injector 0.4.0__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.
Files changed (21) hide show
  1. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/PKG-INFO +1 -1
  2. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/__init__.py +1 -1
  3. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/mypy_.py +1 -1
  4. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/hooks.py +19 -7
  5. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/main.py +23 -14
  6. pytest_revealtype_injector-0.5.0/tests/test_ast_mode.py +64 -0
  7. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/.gitignore +0 -0
  8. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/COPYING +0 -0
  9. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/COPYING.mit +0 -0
  10. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/README.md +0 -0
  11. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/pyproject.toml +0 -0
  12. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/__init__.py +0 -0
  13. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/basedpyright_.py +0 -0
  14. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/adapter/pyright_.py +0 -0
  15. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/log.py +0 -0
  16. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/models.py +0 -0
  17. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/plugin.py +0 -0
  18. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/src/pytest_revealtype_injector/py.typed +0 -0
  19. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/tests/conftest.py +0 -0
  20. {pytest_revealtype_injector-0.4.0 → pytest_revealtype_injector-0.5.0}/tests/test_import.py +0 -0
  21. {pytest_revealtype_injector-0.4.0 → 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.4.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>
@@ -1,3 +1,3 @@
1
1
  """Pytest plugin for replacing reveal_type() calls inside test functions with static and runtime type checking result comparison, for confirming type annotation validity.""" # noqa: E501
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -143,7 +143,7 @@ class MypyAdapter(TypeCheckerAdapter):
143
143
  "column": int,
144
144
  "message": str,
145
145
  "hint": s.Or(str, s.Schema(None)),
146
- "code": str,
146
+ "code": s.Or(str, s.Schema(None)),
147
147
  "severity": s.Or(
148
148
  s.Schema("note"),
149
149
  s.Schema("warning"),
@@ -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__ == "reveal_type" and item.__module__ in {
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
- if inspect.ismodule(item):
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
- continue
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(f"({type(e).__name__}) " + str(e), pytest.ExitCode.INTERNAL_ERROR)
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
- target = None
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
- # HACK node.func is not necessarily "reveal_type" as we allow
34
- # "import as" syntax. We just assume the outmost call is
35
- # reveal_type(), and never descend into recursive ast.Call nodes.
36
- # IDEA Is it possible to retrieve the function name from
37
- # pytest_pyfunc_call() hook and store it in stash somewhere?
38
- self.target = node.args[0]
39
- return node
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
- # TODO Use 'exec' mode which results in more complex AST but doesn't impose
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="eval"))
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(var: _T, adapters: set[TypeCheckerAdapter]) -> _T:
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)