pytest-revealtype-injector 0.4.1__tar.gz → 0.5.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.
Files changed (21) hide show
  1. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/PKG-INFO +20 -5
  2. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/README.md +19 -4
  3. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/__init__.py +1 -1
  4. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/hooks.py +21 -7
  5. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/main.py +23 -14
  6. pytest_revealtype_injector-0.5.1/tests/test_ast_mode.py +64 -0
  7. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/.gitignore +0 -0
  8. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/COPYING +0 -0
  9. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/COPYING.mit +0 -0
  10. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/pyproject.toml +0 -0
  11. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/adapter/__init__.py +0 -0
  12. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/adapter/basedpyright_.py +0 -0
  13. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/adapter/mypy_.py +0 -0
  14. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/adapter/pyright_.py +0 -0
  15. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/log.py +0 -0
  16. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/models.py +0 -0
  17. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/plugin.py +0 -0
  18. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/src/pytest_revealtype_injector/py.typed +0 -0
  19. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/tests/conftest.py +0 -0
  20. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/tests/test_import.py +0 -0
  21. {pytest_revealtype_injector-0.4.1 → pytest_revealtype_injector-0.5.1}/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.1
3
+ Version: 0.5.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>
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  `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:
39
39
 
40
- - Launch external static type checkers (`pyright` and `mypy`) and store `reveal_type` results.
40
+ - Launch external static type checkers (`basesdpyright`, `pyright` and `mypy`) and store `reveal_type` results.
41
41
  - Use [`typeguard`](https://github.com/agronholm/typeguard) to verify the aforementioned static type checker result _really_ matches runtime code result.
42
42
 
43
43
  ## Usage
@@ -54,7 +54,17 @@ For using `reveal_type()` inside tests, there is no boiler plate code involved.
54
54
  from typing import reveal_type
55
55
  ```
56
56
 
57
- Just importing `typing` module is fine too (or import `typing_extensions` for Python 3.10, because `reveal_type()` is only available officially since 3.11):
57
+ If you care about compatibility with older pythons, use:
58
+
59
+ ```python
60
+ import sys
61
+ if sys.version >= (3, 11):
62
+ from typing import reveal_type
63
+ else:
64
+ from typing_extensions import reveal_type
65
+ ```
66
+
67
+ Just importing `typing` (or `typing_extensions`) module is fine too:
58
68
 
59
69
  ```python
60
70
  import typing
@@ -64,7 +74,7 @@ def test_something():
64
74
  typing.reveal_type(x) # typeguard fails here
65
75
  ```
66
76
 
67
- Since this plugin scans for `reveal_type()` for replacement under carpet, even `import ... as ...` syntax works too:
77
+ Since this plugin scans for `reveal_type()` for replacement under carpet, even `import ... as ...` syntax works:
68
78
 
69
79
  ```python
70
80
  import typing as typ # or...
@@ -84,7 +94,12 @@ def test_something():
84
94
  reveal_type(x) # calls vanilla reveal_type()
85
95
  ```
86
96
 
87
- 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).
97
+ 2. `reveal_type()` calls have to stay within a single line, although you can use `reveal_type` result in assertion or other purpose:
98
+
99
+ ```python
100
+ x = "1"
101
+ assert reveal_type(str(int(x))) == x
102
+ ```
88
103
 
89
104
  ## Logging
90
105
 
@@ -5,7 +5,7 @@
5
5
 
6
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:
7
7
 
8
- - Launch external static type checkers (`pyright` and `mypy`) and store `reveal_type` results.
8
+ - Launch external static type checkers (`basesdpyright`, `pyright` and `mypy`) and store `reveal_type` results.
9
9
  - Use [`typeguard`](https://github.com/agronholm/typeguard) to verify the aforementioned static type checker result _really_ matches runtime code result.
10
10
 
11
11
  ## Usage
@@ -22,7 +22,17 @@ For using `reveal_type()` inside tests, there is no boiler plate code involved.
22
22
  from typing import reveal_type
23
23
  ```
24
24
 
25
- Just importing `typing` module is fine too (or import `typing_extensions` for Python 3.10, because `reveal_type()` is only available officially since 3.11):
25
+ If you care about compatibility with older pythons, use:
26
+
27
+ ```python
28
+ import sys
29
+ if sys.version >= (3, 11):
30
+ from typing import reveal_type
31
+ else:
32
+ from typing_extensions import reveal_type
33
+ ```
34
+
35
+ Just importing `typing` (or `typing_extensions`) module is fine too:
26
36
 
27
37
  ```python
28
38
  import typing
@@ -32,7 +42,7 @@ def test_something():
32
42
  typing.reveal_type(x) # typeguard fails here
33
43
  ```
34
44
 
35
- Since this plugin scans for `reveal_type()` for replacement under carpet, even `import ... as ...` syntax works too:
45
+ Since this plugin scans for `reveal_type()` for replacement under carpet, even `import ... as ...` syntax works:
36
46
 
37
47
  ```python
38
48
  import typing as typ # or...
@@ -52,7 +62,12 @@ def test_something():
52
62
  reveal_type(x) # calls vanilla reveal_type()
53
63
  ```
54
64
 
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).
65
+ 2. `reveal_type()` calls have to stay within a single line, although you can use `reveal_type` result in assertion or other purpose:
66
+
67
+ ```python
68
+ x = "1"
69
+ assert reveal_type(str(int(x))) == x
70
+ ```
56
71
 
57
72
  ## Logging
58
73
 
@@ -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.1"
3
+ __version__ = "0.5.1"
@@ -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,31 +24,46 @@ 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:
46
56
  files = {i.path for i in session.items}
57
+ if not files:
58
+ return
47
59
  for adp in session.config.stash[adapter_stash_key]:
48
60
  try:
49
61
  adp.run_typechecker_on(files)
50
62
  except Exception as e:
51
63
  _logger.error(f"({adp.id}) {e}")
52
- pytest.exit(f"({type(e).__name__}) " + str(e), pytest.ExitCode.INTERNAL_ERROR)
64
+ pytest.exit(
65
+ f"({type(e).__name__}) " + str(e), pytest.ExitCode.INTERNAL_ERROR
66
+ )
53
67
  else:
54
68
  _logger.info(f"({adp.id}) Type checker ran successfully")
55
69
 
@@ -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)