pytest-revealtype-injector 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

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.
@@ -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.2.1"
3
+ __version__ = "0.2.3"
@@ -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 (
@@ -19,6 +18,7 @@ import mypy.api
19
18
  import pytest
20
19
  import schema as s
21
20
 
21
+ from ..log import get_logger
22
22
  from ..models import (
23
23
  FilePos,
24
24
  NameCollectorBase,
@@ -27,8 +27,7 @@ from ..models import (
27
27
  VarType,
28
28
  )
29
29
 
30
- _logger = logging.getLogger(__name__)
31
- _logger.setLevel(logging.INFO)
30
+ _logger = get_logger()
32
31
 
33
32
 
34
33
  class _MypyDiagObj(TypedDict):
@@ -68,7 +67,9 @@ class _NameCollector(NameCollectorBase):
68
67
  _ = self.visit(node.value)
69
68
 
70
69
  if resolved := getattr(self.collected[prefix], name, False):
71
- self.collected[ast.unparse(node)] = resolved
70
+ code = ast.unparse(node)
71
+ self.collected[code] = resolved
72
+ _logger.debug(f"Mypy NameCollector resolved '{code}' as {resolved}")
72
73
  return node
73
74
 
74
75
  # For class defined in local scope, mypy just prepends test
@@ -101,10 +102,13 @@ class _NameCollector(NameCollectorBase):
101
102
  pass
102
103
  else:
103
104
  self.collected[name] = mod
105
+ _logger.debug(f"Mypy NameCollector resolved '{name}' as {mod}")
104
106
  return node
105
107
 
106
108
  if hasattr(self.collected["typing"], name):
107
- self.collected[name] = getattr(self.collected["typing"], name)
109
+ obj = getattr(self.collected["typing"], name)
110
+ self.collected[name] = obj
111
+ _logger.debug(f"Mypy NameCollector resolved '{name}' as {obj}")
108
112
  return node
109
113
 
110
114
  raise NameError(f'Cannot resolve "{name}"')
@@ -162,6 +166,8 @@ class _MypyAdapter(TypeCheckerAdapter):
162
166
  # So-called mypy json output is merely a line-by-line
163
167
  # transformation of plain text output into json object
164
168
  for line in stdout.splitlines():
169
+ if len(line) <= 2 or line[0] != "{":
170
+ continue
165
171
  obj = json.loads(line)
166
172
  diag = cast(_MypyDiagObj, cls._schema.validate(obj))
167
173
  filename = pathlib.Path(diag["file"]).name
@@ -210,7 +216,6 @@ class _MypyAdapter(TypeCheckerAdapter):
210
216
 
211
217
  @classmethod
212
218
  def set_config_file(cls, config: pytest.Config) -> None:
213
- # Mypy doesn't have a default config file
214
219
  if (path_str := config.option.revealtype_mypy_config) is None:
215
220
  _logger.info("Using default mypy configuration")
216
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
@@ -13,12 +12,14 @@ from typing import (
13
12
  ForwardRef,
14
13
  Literal,
15
14
  TypedDict,
15
+ TypeVar,
16
16
  cast,
17
17
  )
18
18
 
19
19
  import pytest
20
20
  import schema as s
21
21
 
22
+ from ..log import get_logger
22
23
  from ..models import (
23
24
  FilePos,
24
25
  NameCollectorBase,
@@ -27,18 +28,19 @@ from ..models import (
27
28
  VarType,
28
29
  )
29
30
 
30
- _logger = logging.getLogger(__name__)
31
- _logger.setLevel(logging.INFO)
31
+ _logger = get_logger()
32
32
 
33
33
 
34
34
  class _PyrightDiagPosition(TypedDict):
35
35
  line: int
36
36
  character: int
37
37
 
38
+
38
39
  class _PyrightDiagRange(TypedDict):
39
40
  start: _PyrightDiagPosition
40
41
  end: _PyrightDiagPosition
41
42
 
43
+
42
44
  class _PyrightDiagItem(TypedDict):
43
45
  file: str
44
46
  severity: Literal["information", "warning", "error"]
@@ -47,6 +49,13 @@ class _PyrightDiagItem(TypedDict):
47
49
 
48
50
 
49
51
  class _NameCollector(NameCollectorBase):
52
+ # Pre-register common used bare names from typing
53
+ collected = NameCollectorBase.collected | {
54
+ k: v
55
+ for k, v in NameCollectorBase.collected["typing"].__dict__.items()
56
+ if k[0].isupper() and not isinstance(v, TypeVar)
57
+ }
58
+
50
59
  # Pyright inferred type results always contain bare names only,
51
60
  # so don't need to bother with visit_Attribute()
52
61
  def visit_Name(self, node: ast.Name) -> ast.Name:
@@ -55,9 +64,12 @@ class _NameCollector(NameCollectorBase):
55
64
  eval(name, self._globalns, self._localns | self.collected)
56
65
  except NameError:
57
66
  for m in ("typing", "typing_extensions"):
58
- if hasattr(self.collected[m], name):
59
- self.collected[name] = getattr(self.collected[m], name)
60
- return node
67
+ if not hasattr(self.collected[m], name):
68
+ continue
69
+ obj = getattr(self.collected[m], name)
70
+ self.collected[name] = obj
71
+ _logger.debug(f"Pyright NameCollector resolved '{name}' as {obj}")
72
+ return node
61
73
  raise
62
74
  return node
63
75
 
@@ -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 = logging.getLogger(__name__)
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 = logging.getLogger(__name__)
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 and idx 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
- return ast.get_source_segment(code, walker.target)
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:
@@ -119,22 +126,21 @@ def revealtype_injector(var: _T) -> _T:
119
126
  adp.typechecker_result[pos] = VarType(var_name, tc_result.type)
120
127
 
121
128
  ref = tc_result.type
129
+ walker = adp.create_collector(globalns, localns)
122
130
  try:
123
- _ = eval(ref.__forward_arg__, globalns, localns)
131
+ _ = eval(ref.__forward_arg__, globalns, localns | walker.collected)
124
132
  except (TypeError, NameError):
125
133
  ref_ast = ast.parse(ref.__forward_arg__, mode="eval")
126
- walker = adp.create_collector(globalns, localns)
127
134
  new_ast = walker.visit(ref_ast)
128
135
  if walker.modified:
129
136
  ref = ForwardRef(ast.unparse(new_ast))
130
- memo = TypeCheckMemo(globalns, localns | walker.collected)
131
- else:
132
- memo = TypeCheckMemo(globalns, localns)
137
+ memo = TypeCheckMemo(globalns, localns | walker.collected)
133
138
 
134
139
  try:
135
140
  check_type_internal(var, ref, memo)
136
141
  except TypeCheckError as e:
137
- e.args = (f"({adp.id}) " + e.args[0],) + e.args[1:]
142
+ # Only args[0] contains message
143
+ e.args = (e.args[0] + f" (from {adp.id})",) + e.args[1:]
138
144
  raise
139
145
 
140
146
  return var
@@ -47,6 +47,12 @@ class TypeCheckerError(Exception):
47
47
 
48
48
 
49
49
  class NameCollectorBase(ast.NodeTransformer):
50
+ # typing_extensions guaranteed to be present,
51
+ # as a dependency of typeguard
52
+ collected: dict[str, Any] = {
53
+ m: importlib.import_module(m)
54
+ for m in ("builtins", "typing", "typing_extensions")
55
+ }
50
56
  def __init__(
51
57
  self,
52
58
  globalns: dict[str, Any],
@@ -56,12 +62,7 @@ class NameCollectorBase(ast.NodeTransformer):
56
62
  self._globalns = globalns
57
63
  self._localns = localns
58
64
  self.modified: bool = False
59
- # typing_extensions guaranteed to be present,
60
- # as a dependency of typeguard
61
- self.collected: dict[str, Any] = {
62
- m: importlib.import_module(m)
63
- for m in ("builtins", "typing", "typing_extensions")
64
- }
65
+ self.collected = type(self).collected.copy()
65
66
 
66
67
  def visit_Subscript(self, node: ast.Subscript) -> ast.expr:
67
68
  node.value = cast("ast.expr", self.visit(node.value))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-revealtype-injector
3
- Version: 0.2.1
3
+ Version: 0.2.3
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>
@@ -84,6 +84,31 @@ def test_something():
84
84
 
85
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).
86
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
+
87
112
  ## History
88
113
 
89
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.
@@ -0,0 +1,16 @@
1
+ pytest_revealtype_injector/__init__.py,sha256=I8FXkeB3TED-tbJ3KWZ60nazA1c8pSsF_10q0NGof-I,211
2
+ pytest_revealtype_injector/hooks.py,sha256=cj1iXJJrVtqylXvvdLfjZ61HGBt5wsGBUe-iAgu9WiU,2434
3
+ pytest_revealtype_injector/log.py,sha256=U9IvsoZzhFQcdFmVtuSmt2OTYxxFuIP5o321dyE4hiA,417
4
+ pytest_revealtype_injector/main.py,sha256=ZNb_hDeuEIWDC_eSCLmXXfGCarHF1lsXIOE_BWL-940,4731
5
+ pytest_revealtype_injector/models.py,sha256=FKYznMaNP9a4_AUN3vMdRQBvQUkbdCYTYjDccgdrV6w,3238
6
+ pytest_revealtype_injector/plugin.py,sha256=fkI6yF0dFVba0jEikIrsRp1NUQd2ohWLq4x2lSvFyH0,211
7
+ pytest_revealtype_injector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ pytest_revealtype_injector/adapter/__init__.py,sha256=wQg3Ply0Xrfe9US_lzgqRgKFE89ZBvDFti6_77X_NKk,339
9
+ pytest_revealtype_injector/adapter/mypy_.py,sha256=q-stxOY2bffTAm2SoHiCAb7VA_f9jXc4UxlyimXV-RY,8840
10
+ pytest_revealtype_injector/adapter/pyright_.py,sha256=Um_HiYDehffci2l9DYXvOfxHNEVuKQsQ9MOsO5mXal4,5126
11
+ pytest_revealtype_injector-0.2.3.dist-info/METADATA,sha256=zrqkK8CDEbkQi2yI1rCnRAf41QAsPkgNIx-dV9UzBvU,5520
12
+ pytest_revealtype_injector-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ pytest_revealtype_injector-0.2.3.dist-info/entry_points.txt,sha256=UfOm7y3WQnOoGV1mgTMb42MI6iBRPIl88FJiAOnt6SY,74
14
+ pytest_revealtype_injector-0.2.3.dist-info/licenses/COPYING,sha256=LSYUX8PcSMvHCkhM5oi07eOrSLV89qdEJ-FVZmbcpNE,355
15
+ pytest_revealtype_injector-0.2.3.dist-info/licenses/COPYING.mit,sha256=IzYEFDIOECyuupg_B3O9FvgjnU9i4JtambpbleoYHdQ,1060
16
+ pytest_revealtype_injector-0.2.3.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- pytest_revealtype_injector/__init__.py,sha256=QWrBXB4AnDHb7ughEzF0sP-1-dZxmRR45Dw0EiLkCrY,211
2
- pytest_revealtype_injector/hooks.py,sha256=5V2r19I47xQtCDd8ZAzXlqt3nYaBZ4CaMAyGP6w_Y0k,2414
3
- pytest_revealtype_injector/main.py,sha256=dBlXcGc2NBcEHp0YX75Bp4Q7KY-t1oxlmPQS6o08U24,4490
4
- pytest_revealtype_injector/models.py,sha256=XsxUQGdv1U4CFi1bB1VeEojPpiwCZp7uxRIWuTGhIaM,3214
5
- pytest_revealtype_injector/plugin.py,sha256=fkI6yF0dFVba0jEikIrsRp1NUQd2ohWLq4x2lSvFyH0,211
6
- pytest_revealtype_injector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- pytest_revealtype_injector/adapter/__init__.py,sha256=wQg3Ply0Xrfe9US_lzgqRgKFE89ZBvDFti6_77X_NKk,339
8
- pytest_revealtype_injector/adapter/mypy_.py,sha256=5lWExaKWDx_SK2alEsDtjg_vSGLriFgG7E-81TL1bb0,8569
9
- pytest_revealtype_injector/adapter/pyright_.py,sha256=LGaJcA-HLCE8cxRFCmPQ5TTURud-ZapmdXgoFJXrw78,4755
10
- pytest_revealtype_injector-0.2.1.dist-info/METADATA,sha256=6dIidtF2B7QYkiFR8ul9FjChh1IW8DTbAKsddamYk3k,4160
11
- pytest_revealtype_injector-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- pytest_revealtype_injector-0.2.1.dist-info/entry_points.txt,sha256=UfOm7y3WQnOoGV1mgTMb42MI6iBRPIl88FJiAOnt6SY,74
13
- pytest_revealtype_injector-0.2.1.dist-info/licenses/COPYING,sha256=LSYUX8PcSMvHCkhM5oi07eOrSLV89qdEJ-FVZmbcpNE,355
14
- pytest_revealtype_injector-0.2.1.dist-info/licenses/COPYING.mit,sha256=IzYEFDIOECyuupg_B3O9FvgjnU9i4JtambpbleoYHdQ,1060
15
- pytest_revealtype_injector-0.2.1.dist-info/RECORD,,