pytest-revealtype-injector 0.2.3__tar.gz → 0.3.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 (24) hide show
  1. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/PKG-INFO +5 -4
  2. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/pyproject.toml +7 -10
  3. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/__init__.py +1 -1
  4. pytest_revealtype_injector-0.3.0/src/pytest_revealtype_injector/adapter/__init__.py +21 -0
  5. pytest_revealtype_injector-0.3.0/src/pytest_revealtype_injector/adapter/basedpyright_.py +21 -0
  6. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/adapter/mypy_.py +56 -60
  7. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/adapter/pyright_.py +38 -53
  8. pytest_revealtype_injector-0.3.0/src/pytest_revealtype_injector/hooks.py +92 -0
  9. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/log.py +1 -0
  10. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/main.py +21 -9
  11. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/models.py +62 -14
  12. pytest_revealtype_injector-0.3.0/tests/conftest.py +7 -0
  13. pytest_revealtype_injector-0.3.0/tests/test_import.py +103 -0
  14. pytest_revealtype_injector-0.3.0/tests/test_options.py +95 -0
  15. pytest_revealtype_injector-0.2.3/src/pytest_revealtype_injector/adapter/__init__.py +0 -13
  16. pytest_revealtype_injector-0.2.3/src/pytest_revealtype_injector/hooks.py +0 -73
  17. pytest_revealtype_injector-0.2.3/tests/conftest.py +0 -1
  18. pytest_revealtype_injector-0.2.3/tests/test_basic.py +0 -91
  19. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/.gitignore +0 -0
  20. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/COPYING +0 -0
  21. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/COPYING.mit +0 -0
  22. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/README.md +0 -0
  23. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/plugin.py +0 -0
  24. {pytest_revealtype_injector-0.2.3 → pytest_revealtype_injector-0.3.0}/src/pytest_revealtype_injector/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-revealtype-injector
3
- Version: 0.2.3
3
+ Version: 0.3.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>
@@ -21,11 +21,12 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development :: Testing
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
+ Requires-Dist: basedpyright>=1.0
24
25
  Requires-Dist: mypy>=1.11.2
25
- Requires-Dist: pyright~=1.1
26
- Requires-Dist: pytest>=7.0
26
+ Requires-Dist: pyright>=1.1
27
+ Requires-Dist: pytest<9,>=7.0
27
28
  Requires-Dist: schema==0.7.7
28
- Requires-Dist: typeguard~=4.3
29
+ Requires-Dist: typeguard>=4.3
29
30
  Description-Content-Type: text/markdown
30
31
 
31
32
  ![PyPI - Version](https://img.shields.io/pypi/v/pytest-revealtype-injector)
@@ -16,9 +16,10 @@ license = 'MIT'
16
16
  license-files = ['COPYING*']
17
17
  dependencies = [
18
18
  'mypy >= 1.11.2',
19
- 'pyright ~= 1.1',
20
- 'pytest >= 7.0',
21
- 'typeguard ~= 4.3',
19
+ 'pyright >= 1.1',
20
+ 'basedpyright >= 1.0',
21
+ 'pytest >=7.0,<9',
22
+ 'typeguard >= 4.3',
22
23
  # schema with annotation support is still unreleased
23
24
  'schema == 0.7.7',
24
25
  ]
@@ -58,9 +59,6 @@ homepage = 'https://github.com/abelcheung/pytest-revealtype-injector'
58
59
  [project.entry-points.pytest11]
59
60
  pytest-revealtype-injector = "pytest_revealtype_injector.plugin"
60
61
 
61
- [tool.flit.module]
62
- name = 'pytest_revealtype_injector'
63
-
64
62
  [tool.hatch.version]
65
63
  path = 'src/pytest_revealtype_injector/__init__.py'
66
64
 
@@ -92,10 +90,9 @@ target-version = "py312"
92
90
  preview = true
93
91
 
94
92
  [tool.ruff.lint]
95
- select = [
96
- 'E',
97
- 'F',
98
- 'I',
93
+ select = ['E', 'F', 'I']
94
+ ignore = [
95
+ "E501",
99
96
  ]
100
97
  task-tags = [
101
98
  "BUG",
@@ -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.3"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from ..models import TypeCheckerAdapter
4
+ from . import basedpyright_, mypy_, pyright_
5
+
6
+
7
+ # Hardcode will do for now, it's not like we're going to have more
8
+ # adapters soon. Pyre and PyType are not there yet.
9
+ def generate() -> set[TypeCheckerAdapter]:
10
+ return {
11
+ basedpyright_.generate_adapter(),
12
+ pyright_.generate_adapter(),
13
+ mypy_.generate_adapter(),
14
+ }
15
+
16
+ def get_adapter_classes() -> list[type[TypeCheckerAdapter]]:
17
+ return [
18
+ basedpyright_.BasedPyrightAdapter,
19
+ pyright_.PyrightAdapter,
20
+ mypy_.MypyAdapter,
21
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from ..log import get_logger
4
+ from ..models import TypeCheckerAdapter
5
+ from . import pyright_
6
+
7
+ _logger = get_logger()
8
+
9
+
10
+ class NameCollector(pyright_.NameCollector):
11
+ type_checker = "basedpyright"
12
+
13
+
14
+ class BasedPyrightAdapter(pyright_.PyrightAdapter):
15
+ id = "basedpyright"
16
+ _executable = "basedpyright"
17
+ _namecollector_class = NameCollector
18
+
19
+
20
+ def generate_adapter() -> TypeCheckerAdapter:
21
+ return BasedPyrightAdapter()
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import ast
2
4
  import importlib
3
5
  import json
@@ -7,7 +9,6 @@ from collections.abc import (
7
9
  Iterable,
8
10
  )
9
11
  from typing import (
10
- Any,
11
12
  ForwardRef,
12
13
  Literal,
13
14
  TypedDict,
@@ -15,7 +16,6 @@ from typing import (
15
16
  )
16
17
 
17
18
  import mypy.api
18
- import pytest
19
19
  import schema as s
20
20
 
21
21
  from ..log import get_logger
@@ -40,7 +40,9 @@ class _MypyDiagObj(TypedDict):
40
40
  severity: Literal["note", "warning", "error"]
41
41
 
42
42
 
43
- class _NameCollector(NameCollectorBase):
43
+ class NameCollector(NameCollectorBase):
44
+ type_checker = "mypy"
45
+
44
46
  def visit_Attribute(self, node: ast.Attribute) -> ast.expr:
45
47
  prefix = ast.unparse(node.value)
46
48
  name = node.attr
@@ -69,7 +71,9 @@ class _NameCollector(NameCollectorBase):
69
71
  if resolved := getattr(self.collected[prefix], name, False):
70
72
  code = ast.unparse(node)
71
73
  self.collected[code] = resolved
72
- _logger.debug(f"Mypy NameCollector resolved '{code}' as {resolved}")
74
+ _logger.debug(
75
+ f"{self.type_checker} NameCollector resolved '{code}' as {resolved}"
76
+ )
73
77
  return node
74
78
 
75
79
  # For class defined in local scope, mypy just prepends test
@@ -102,13 +106,17 @@ class _NameCollector(NameCollectorBase):
102
106
  pass
103
107
  else:
104
108
  self.collected[name] = mod
105
- _logger.debug(f"Mypy NameCollector resolved '{name}' as {mod}")
109
+ _logger.debug(
110
+ f"{self.type_checker} NameCollector resolved '{name}' as {mod}"
111
+ )
106
112
  return node
107
113
 
108
114
  if hasattr(self.collected["typing"], name):
109
115
  obj = getattr(self.collected["typing"], name)
110
116
  self.collected[name] = obj
111
- _logger.debug(f"Mypy NameCollector resolved '{name}' as {obj}")
117
+ _logger.debug(
118
+ f"{self.type_checker} NameCollector resolved '{name}' as {obj}"
119
+ )
112
120
  return node
113
121
 
114
122
  raise NameError(f'Cannot resolve "{name}"')
@@ -118,17 +126,17 @@ class _NameCollector(NameCollectorBase):
118
126
  # Return only the left operand after processing.
119
127
  def visit_BinOp(self, node: ast.BinOp) -> ast.expr:
120
128
  if isinstance(node.op, ast.MatMult) and isinstance(node.right, ast.Constant):
121
- # Mypy disallows returning Any
122
129
  return cast("ast.expr", self.visit(node.left))
123
130
  # For expression that haven't been accounted for, just don't
124
131
  # process and allow name resolution to fail
125
132
  return node
126
133
 
127
134
 
128
- class _MypyAdapter(TypeCheckerAdapter):
135
+ class MypyAdapter(TypeCheckerAdapter):
129
136
  id = "mypy"
130
- typechecker_result = {}
131
- _type_mesg_re = re.compile(r'^Revealed type is "(?P<type>.+?)"$')
137
+ _executable = "" # unused, calls mypy.api.run() here
138
+ _type_mesg_re = re.compile(r'Revealed type is "(?P<type>.+?)"')
139
+ _namecollector_class = NameCollector
132
140
  _schema = s.Schema({
133
141
  "file": str,
134
142
  "line": int,
@@ -143,19 +151,20 @@ class _MypyAdapter(TypeCheckerAdapter):
143
151
  ),
144
152
  })
145
153
 
146
- @classmethod
147
- def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None:
154
+ def run_typechecker_on(self, paths: Iterable[pathlib.Path]) -> None:
148
155
  mypy_args = [
149
156
  "--output=json",
150
157
  ]
151
- if cls.config_file is not None:
152
- cfg_str = str(cls.config_file)
153
- if cfg_str == ".": # see set_config_file() below
154
- cfg_str = ""
158
+ if self.config_file is not None:
159
+ if self.config_file == pathlib.Path():
160
+ cfg_str = "" # see preprocess_config_file() below
161
+ else:
162
+ cfg_str = str(self.config_file)
155
163
  mypy_args.append(f"--config-file={cfg_str}")
156
164
 
157
165
  mypy_args.extend(str(p) for p in paths)
158
166
 
167
+ _logger.debug(f"({self.id}) api.run(): {mypy_args}")
159
168
  stdout, stderr, returncode = mypy.api.run(mypy_args)
160
169
 
161
170
  # fatal error, before evaluation happens
@@ -163,15 +172,31 @@ class _MypyAdapter(TypeCheckerAdapter):
163
172
  if stderr:
164
173
  raise TypeCheckerError(stderr, None, None)
165
174
 
175
+ lines = stdout.splitlines()
176
+ _logger.info(
177
+ "({}) Return code = {}, diagnostic count = {}.{}".format(
178
+ self.id,
179
+ returncode,
180
+ len(lines),
181
+ " pytest -vv shows all items." if self.log_verbosity < 2 else "",
182
+ )
183
+ )
184
+
166
185
  # So-called mypy json output is merely a line-by-line
167
186
  # transformation of plain text output into json object
168
- for line in stdout.splitlines():
187
+ for line in lines:
169
188
  if len(line) <= 2 or line[0] != "{":
170
189
  continue
190
+ if self.log_verbosity >= 2:
191
+ _logger.debug(f"({self.id}) {line}")
171
192
  obj = json.loads(line)
172
- diag = cast(_MypyDiagObj, cls._schema.validate(obj))
193
+ diag = cast(_MypyDiagObj, self._schema.validate(obj))
173
194
  filename = pathlib.Path(diag["file"]).name
174
195
  pos = FilePos(filename, diag["line"])
196
+ # HACK: Never trust return code from mypy. During early 1.11.x
197
+ # versions, mypy always return 1 for JSON output even when
198
+ # there's no error. Later on mypy command line has fixed this,
199
+ # but not mypy.api.run(), as of 1.13.
175
200
  if diag["severity"] != "note":
176
201
  raise TypeCheckerError(
177
202
  "Mypy {} with exit code {}: {}".format(
@@ -180,7 +205,7 @@ class _MypyAdapter(TypeCheckerAdapter):
180
205
  diag["file"],
181
206
  diag["line"],
182
207
  )
183
- if (m := cls._type_mesg_re.match(diag["message"])) is None:
208
+ if (m := self._type_mesg_re.fullmatch(diag["message"])) is None:
184
209
  continue
185
210
  # Mypy can insert extra character into expression so that it
186
211
  # becomes invalid and unparsable. 0.9x days there
@@ -190,14 +215,14 @@ class _MypyAdapter(TypeCheckerAdapter):
190
215
  expression = m["type"].translate({ord(c): None for c in "*?="})
191
216
  try:
192
217
  # Unlike pyright, mypy output doesn't contain variable name
193
- cls.typechecker_result[pos] = VarType(None, ForwardRef(expression))
218
+ self.typechecker_result[pos] = VarType(None, ForwardRef(expression))
194
219
  except SyntaxError as e:
195
220
  if (
196
221
  m := re.fullmatch(r"<Deleted '(?P<var>.+)'>", expression)
197
222
  ) is not None:
198
223
  raise TypeCheckerError(
199
224
  "{} does not support reusing deleted variable '{}'".format(
200
- cls.id, m["var"]
225
+ self.id, m["var"]
201
226
  ),
202
227
  diag["file"],
203
228
  diag["line"],
@@ -208,46 +233,17 @@ class _MypyAdapter(TypeCheckerAdapter):
208
233
  diag["line"],
209
234
  ) from e
210
235
 
211
- @classmethod
212
- def create_collector(
213
- cls, globalns: dict[str, Any], localns: dict[str, Any]
214
- ) -> _NameCollector:
215
- return _NameCollector(globalns, localns)
216
-
217
- @classmethod
218
- def set_config_file(cls, config: pytest.Config) -> None:
219
- if (path_str := config.option.revealtype_mypy_config) is None:
220
- _logger.info("Using default mypy configuration")
221
- return
222
-
236
+ def preprocess_config_file(self, path_str: str) -> bool:
237
+ if path_str:
238
+ return False
223
239
  # HACK: when path_str is empty string, use no config file
224
240
  # ('mypy --config-file=')
225
- # Take advantage of pathlib.Path() behavior that empty string
226
- # is treated as current directory, which is not a valid
227
- # config file name, while satisfying typing constraint
228
- if not path_str:
229
- cls.config_file = pathlib.Path()
230
- return
231
-
232
- relpath = pathlib.Path(path_str)
233
- if relpath.is_absolute():
234
- raise ValueError(f"Path '{path_str}' must be relative to pytest rootdir")
235
- result = (config.rootpath / relpath).resolve()
236
- if not result.exists():
237
- raise FileNotFoundError(f"Path '{result}' not found")
238
-
239
- _logger.info(f"Using mypy configuration file at {result}")
240
- cls.config_file = result
241
-
242
- @staticmethod
243
- def add_pytest_option(group: pytest.OptionGroup) -> None:
244
- group.addoption(
245
- "--revealtype-mypy-config",
246
- type=str,
247
- default=None,
248
- help="Mypy configuration file, path is relative to pytest rootdir. "
249
- "If unspecified, use mypy default behavior",
250
- )
241
+ # The special value is for satisfying typing constraint;
242
+ # it will be treated specially in run_typechecker_on()
243
+ self.config_file = pathlib.Path()
244
+ self._logger.info(f"({self.id}) Config file usage forbidden")
245
+ return True
251
246
 
252
247
 
253
- adapter = _MypyAdapter()
248
+ def generate_adapter() -> TypeCheckerAdapter:
249
+ return MypyAdapter()
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import ast
2
4
  import json
3
5
  import pathlib
@@ -8,7 +10,6 @@ from collections.abc import (
8
10
  Iterable,
9
11
  )
10
12
  from typing import (
11
- Any,
12
13
  ForwardRef,
13
14
  Literal,
14
15
  TypedDict,
@@ -16,7 +17,6 @@ from typing import (
16
17
  cast,
17
18
  )
18
19
 
19
- import pytest
20
20
  import schema as s
21
21
 
22
22
  from ..log import get_logger
@@ -48,7 +48,8 @@ class _PyrightDiagItem(TypedDict):
48
48
  range: _PyrightDiagRange
49
49
 
50
50
 
51
- class _NameCollector(NameCollectorBase):
51
+ class NameCollector(NameCollectorBase):
52
+ type_checker = "pyright"
52
53
  # Pre-register common used bare names from typing
53
54
  collected = NameCollectorBase.collected | {
54
55
  k: v
@@ -68,18 +69,21 @@ class _NameCollector(NameCollectorBase):
68
69
  continue
69
70
  obj = getattr(self.collected[m], name)
70
71
  self.collected[name] = obj
71
- _logger.debug(f"Pyright NameCollector resolved '{name}' as {obj}")
72
+ _logger.debug(
73
+ f"{self.type_checker} NameCollector resolved '{name}' as {obj}"
74
+ )
72
75
  return node
73
76
  raise
74
77
  return node
75
78
 
76
79
 
77
- class _PyrightAdapter(TypeCheckerAdapter):
80
+ class PyrightAdapter(TypeCheckerAdapter):
78
81
  id = "pyright"
79
- typechecker_result = {}
80
- _type_mesg_re = re.compile('^Type of "(?P<var>.+?)" is "(?P<type>.+?)"$')
81
- # We only care about diagnostic messages that contain type information.
82
- # Metadata not specified here.
82
+ _executable = "pyright"
83
+ _type_mesg_re = re.compile('Type of "(?P<var>.+?)" is "(?P<type>.+?)"')
84
+ _namecollector_class = NameCollector
85
+ # We only care about diagnostic messages that contain type information, that
86
+ # is, items under "generalDiagnostics" key. Metadata not validated here.
83
87
  _schema = s.Schema({
84
88
  "file": str,
85
89
  "severity": s.Or(
@@ -92,30 +96,42 @@ class _PyrightAdapter(TypeCheckerAdapter):
92
96
  "start": {"line": int, "character": int},
93
97
  "end": {"line": int, "character": int},
94
98
  },
99
+ s.Optional("rule"): str,
95
100
  })
96
101
 
97
- @classmethod
98
- def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None:
102
+ def run_typechecker_on(self, paths: Iterable[pathlib.Path]) -> None:
99
103
  cmd: list[str] = []
100
- if shutil.which("pyright") is not None:
101
- cmd.append("pyright")
104
+ if shutil.which(self._executable) is not None:
105
+ cmd.append(self._executable)
102
106
  elif shutil.which("npx") is not None:
103
- cmd.extend(["npx", "pyright"])
107
+ cmd.extend(["npx", self._executable])
104
108
  else:
105
- raise FileNotFoundError("Pyright is required to run test suite")
109
+ raise FileNotFoundError(f"{self._executable} is required to run test suite")
106
110
 
107
111
  cmd.append("--outputjson")
108
- if cls.config_file is not None:
109
- cmd.extend(["--project", str(cls.config_file)])
112
+ if self.config_file is not None:
113
+ cmd.extend(["--project", str(self.config_file)])
110
114
  cmd.extend(str(p) for p in paths)
111
115
 
116
+ _logger.debug(f"({self.id}) Run command: {cmd}")
112
117
  proc = subprocess.run(cmd, capture_output=True)
113
118
  if len(proc.stderr):
114
119
  raise TypeCheckerError(proc.stderr.decode(), None, None)
115
120
 
116
121
  report = json.loads(proc.stdout)
122
+ _logger.info(
123
+ "({}) Return code = {}, diagnostic count = {}.{}".format(
124
+ self.id,
125
+ proc.returncode,
126
+ len(report["generalDiagnostics"]),
127
+ " pytest -vv shows all items." if self.log_verbosity < 2 else "",
128
+ )
129
+ )
130
+
117
131
  for item in report["generalDiagnostics"]:
118
- diag = cast(_PyrightDiagItem, cls._schema.validate(item))
132
+ diag = cast(_PyrightDiagItem, self._schema.validate(item))
133
+ if self.log_verbosity >= 2:
134
+ _logger.debug(f"({self.id}) {diag}")
119
135
  if diag["severity"] != ("error" if proc.returncode else "information"):
120
136
  continue
121
137
  # Pyright report lineno is 0-based, while
@@ -124,42 +140,11 @@ class _PyrightAdapter(TypeCheckerAdapter):
124
140
  filename = pathlib.Path(diag["file"]).name
125
141
  if proc.returncode:
126
142
  raise TypeCheckerError(diag["message"], filename, lineno)
127
- if (m := cls._type_mesg_re.match(diag["message"])) is None:
143
+ if (m := self._type_mesg_re.fullmatch(diag["message"])) is None:
128
144
  continue
129
145
  pos = FilePos(filename, lineno)
130
- cls.typechecker_result[pos] = VarType(m["var"], ForwardRef(m["type"]))
131
-
132
- @classmethod
133
- def create_collector(
134
- cls, globalns: dict[str, Any], localns: dict[str, Any]
135
- ) -> _NameCollector:
136
- return _NameCollector(globalns, localns)
137
-
138
- @classmethod
139
- def set_config_file(cls, config: pytest.Config) -> None:
140
- if (path_str := config.option.revealtype_pyright_config) is None:
141
- _logger.info("Using default pyright configuration")
142
- return
143
-
144
- relpath = pathlib.Path(path_str)
145
- if relpath.is_absolute():
146
- raise ValueError(f"Path '{path_str}' must be relative to pytest rootdir")
147
- result = (config.rootpath / relpath).resolve()
148
- if not result.exists():
149
- raise FileNotFoundError(f"Path '{result}' not found")
150
-
151
- _logger.info(f"Using pyright configuration file at {result}")
152
- cls.config_file = result
153
-
154
- @staticmethod
155
- def add_pytest_option(group: pytest.OptionGroup) -> None:
156
- group.addoption(
157
- "--revealtype-pyright-config",
158
- type=str,
159
- default=None,
160
- help="Pyright configuration file, path is relative to pytest rootdir. "
161
- "If unspecified, use pyright default behavior",
162
- )
146
+ self.typechecker_result[pos] = VarType(m["var"], ForwardRef(m["type"]))
163
147
 
164
148
 
165
- adapter = _PyrightAdapter()
149
+ def generate_adapter() -> TypeCheckerAdapter:
150
+ return PyrightAdapter()
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import inspect
5
+ from typing import cast
6
+
7
+ import pytest
8
+
9
+ from . import adapter, log
10
+ from .main import revealtype_injector
11
+ from .models import TypeCheckerAdapter
12
+
13
+ _logger = log.get_logger()
14
+ adapter_stash_key: pytest.StashKey[set[TypeCheckerAdapter]]
15
+
16
+
17
+ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
18
+ assert pyfuncitem.module is not None
19
+ adapters = pyfuncitem.config.stash[adapter_stash_key].copy()
20
+ injected = functools.partial(revealtype_injector, adapters=adapters)
21
+
22
+ for name in dir(pyfuncitem.module):
23
+ if name.startswith("__") or name.startswith("@py"):
24
+ continue
25
+
26
+ item = getattr(pyfuncitem.module, name)
27
+ if inspect.isfunction(item):
28
+ if item.__name__ == "reveal_type" and item.__module__ in {
29
+ "typing",
30
+ "typing_extensions",
31
+ }:
32
+ setattr(pyfuncitem.module, name, injected)
33
+ _logger.info(
34
+ f"Replaced {name}() from global import with {injected}"
35
+ )
36
+ continue
37
+
38
+ if inspect.ismodule(item):
39
+ if item.__name__ not in {"typing", "typing_extensions"}:
40
+ continue
41
+ assert hasattr(item, "reveal_type")
42
+ setattr(item, "reveal_type", injected)
43
+ _logger.info(f"Replaced {name}.reveal_type() with {injected}")
44
+ continue
45
+
46
+
47
+ def pytest_collection_finish(session: pytest.Session) -> None:
48
+ files = {i.path for i in session.items}
49
+ for adp in session.config.stash[adapter_stash_key]:
50
+ try:
51
+ adp.run_typechecker_on(files)
52
+ except Exception as e:
53
+ _logger.error(f"({adp.id}) {e}")
54
+ raise e
55
+ else:
56
+ _logger.info(f"({adp.id}) Type checker ran successfully")
57
+
58
+
59
+ def pytest_addoption(parser: pytest.Parser) -> None:
60
+ group = parser.getgroup(
61
+ "revealtype-injector",
62
+ description="Type checker related options for revealtype-injector",
63
+ )
64
+ classes = adapter.get_adapter_classes()
65
+ group.addoption(
66
+ "--revealtype-disable-adapter",
67
+ type=str,
68
+ choices=tuple(c.id for c in classes),
69
+ action="append",
70
+ default=[],
71
+ help="Disable specific type checker. Can be used multiple times"
72
+ " to disable multiple checkers",
73
+ )
74
+ for c in classes:
75
+ c.add_pytest_option(group)
76
+
77
+
78
+ def pytest_configure(config: pytest.Config) -> None:
79
+ global adapter_stash_key
80
+ adapter_stash_key = pytest.StashKey[set[TypeCheckerAdapter]]()
81
+ config.stash[adapter_stash_key] = set()
82
+ verbosity = config.get_verbosity(config.VERBOSITY_TEST_CASES)
83
+ log.set_verbosity(verbosity)
84
+ to_be_disabled = cast(list[str], config.getoption("revealtype_disable_adapter"))
85
+ for klass in adapter.get_adapter_classes():
86
+ if klass.id in to_be_disabled:
87
+ _logger.info(f"({klass.id}) adapter disabled with command line option")
88
+ continue
89
+ adp = klass()
90
+ adp.set_config_file(config)
91
+ adp.log_verbosity = verbosity
92
+ config.stash[adapter_stash_key].add(adp)
@@ -11,6 +11,7 @@ _verbosity_map = {
11
11
  2: logging.DEBUG,
12
12
  }
13
13
 
14
+
14
15
  def get_logger() -> logging.Logger:
15
16
  return _logger
16
17
 
@@ -14,15 +14,15 @@ from typeguard import (
14
14
  check_type_internal,
15
15
  )
16
16
 
17
- from . import adapter, log
17
+ from . import log
18
18
  from .models import (
19
19
  FilePos,
20
+ TypeCheckerAdapter,
20
21
  TypeCheckerError,
21
22
  VarType,
22
23
  )
23
24
 
24
25
  _T = TypeVar("_T")
25
-
26
26
  _logger = log.get_logger()
27
27
 
28
28
 
@@ -61,7 +61,7 @@ def _get_var_name(frame: inspect.Traceback) -> str | None:
61
61
  return result
62
62
 
63
63
 
64
- def revealtype_injector(var: _T) -> _T:
64
+ def revealtype_injector(var: _T, adapters: set[TypeCheckerAdapter]) -> _T:
65
65
  """Replacement of `reveal_type()` that matches static and runtime type
66
66
  checking result
67
67
 
@@ -105,9 +105,7 @@ def revealtype_injector(var: _T) -> _T:
105
105
  globalns = caller_frame.f_globals
106
106
  localns = caller_frame.f_locals
107
107
 
108
- for adp in adapter.discovery():
109
- if not adp.enabled:
110
- continue
108
+ for adp in adapters:
111
109
  try:
112
110
  tc_result = adp.typechecker_result[pos]
113
111
  except KeyError as e:
@@ -128,12 +126,26 @@ def revealtype_injector(var: _T) -> _T:
128
126
  ref = tc_result.type
129
127
  walker = adp.create_collector(globalns, localns)
130
128
  try:
131
- _ = eval(ref.__forward_arg__, globalns, localns | walker.collected)
129
+ evaluated = eval(ref.__forward_arg__, globalns, localns | walker.collected)
132
130
  except (TypeError, NameError):
133
- ref_ast = ast.parse(ref.__forward_arg__, mode="eval")
134
- new_ast = walker.visit(ref_ast)
131
+ old_ast = ast.parse(ref.__forward_arg__, mode="eval")
132
+ new_ast = walker.visit(old_ast)
135
133
  if walker.modified:
136
134
  ref = ForwardRef(ast.unparse(new_ast))
135
+ evaluated = eval(ref.__forward_arg__, globalns, localns | walker.collected)
136
+
137
+ # HACK Mainly serves as a guard against mypy's behavior of blanket
138
+ # inferring to Any when it can't determine the type under non-strict
139
+ # mode. This behavior causes typeguard to remain silent, since Any is
140
+ # compatible with everything. This has a side effect of disallowing
141
+ # use of reveal_type() on data truly of Any type.
142
+ if evaluated is Any:
143
+ raise TypeCheckerError(
144
+ f"Inferred type of '{var_name}' is Any, which "
145
+ "defeats the purpose of type checking",
146
+ pos.file,
147
+ pos.lineno,
148
+ )
137
149
  memo = TypeCheckMemo(globalns, localns | walker.collected)
138
150
 
139
151
  try:
@@ -15,8 +15,11 @@ from typing import (
15
15
  )
16
16
 
17
17
  import pytest
18
+ from _pytest.config import Notset
18
19
  from schema import Schema
19
20
 
21
+ from .log import get_logger
22
+
20
23
 
21
24
  class FilePos(NamedTuple):
22
25
  file: str
@@ -47,12 +50,14 @@ class TypeCheckerError(Exception):
47
50
 
48
51
 
49
52
  class NameCollectorBase(ast.NodeTransformer):
53
+ type_checker: ClassVar[str]
50
54
  # typing_extensions guaranteed to be present,
51
55
  # as a dependency of typeguard
52
56
  collected: dict[str, Any] = {
53
57
  m: importlib.import_module(m)
54
58
  for m in ("builtins", "typing", "typing_extensions")
55
59
  }
60
+
56
61
  def __init__(
57
62
  self,
58
63
  globalns: dict[str, Any],
@@ -86,26 +91,69 @@ class NameCollectorBase(ast.NodeTransformer):
86
91
 
87
92
 
88
93
  class TypeCheckerAdapter:
89
- enabled: bool = True
90
- config_file: ClassVar[pathlib.Path | None] = None
91
94
  # Subclasses need to specify default values for below
92
95
  id: ClassVar[str]
93
- # {('file.py', 10): ('var_name', 'list[str]'), ...}
94
- typechecker_result: ClassVar[dict[FilePos, VarType]]
96
+ _executable: ClassVar[str]
95
97
  _type_mesg_re: ClassVar[re.Pattern[str]]
96
98
  _schema: ClassVar[Schema]
99
+ _namecollector_class: ClassVar[type[NameCollectorBase]]
100
+
101
+ def __init__(self) -> None:
102
+ # {('file.py', 10): ('var_name', 'list[str]'), ...}
103
+ self.typechecker_result: dict[FilePos, VarType] = {}
104
+ self._logger = get_logger()
105
+ # logger level is already set by pytest_configure()
106
+ # this only affects how much debug message is shown
107
+ self.log_verbosity: int = 1
108
+ self.enabled: bool = True
109
+ self.config_file: pathlib.Path | None = None
97
110
 
98
111
  @classmethod
112
+ def longopt_for_config(cls) -> str:
113
+ return f"--revealtype-{cls.id}-config"
114
+
99
115
  @abc.abstractmethod
100
- def run_typechecker_on(cls, paths: Iterable[pathlib.Path]) -> None: ...
101
- @classmethod
102
- @abc.abstractmethod
116
+ def run_typechecker_on(self, paths: Iterable[pathlib.Path]) -> None: ...
117
+
103
118
  def create_collector(
104
- cls, globalns: dict[str, Any], localns: dict[str, Any]
105
- ) -> NameCollectorBase: ...
119
+ self, globalns: dict[str, Any], localns: dict[str, Any]
120
+ ) -> NameCollectorBase:
121
+ return self._namecollector_class(globalns, localns)
122
+
123
+ def preprocess_config_file(self, path_str: str) -> bool:
124
+ """Optional preprocessing of configuration file"""
125
+ return False
126
+
127
+ def set_config_file(self, config: pytest.Config) -> None:
128
+ path_str = config.getoption(self.longopt_for_config())
129
+ # pytest addoption() should have set default value
130
+ # to None even when option is not specified
131
+ assert not isinstance(path_str, Notset)
132
+
133
+ if path_str is None:
134
+ self._logger.info(f"({self.id}) Using default configuration")
135
+ return
136
+
137
+ if self.preprocess_config_file(path_str):
138
+ return
139
+
140
+ relpath = pathlib.Path(path_str)
141
+ if relpath.is_absolute():
142
+ raise ValueError(f"Path '{path_str}' must be relative to pytest rootdir")
143
+ result = (config.rootpath / relpath).resolve()
144
+ if not result.exists():
145
+ raise FileNotFoundError(f"Path '{result}' not found")
146
+
147
+ self._logger.info(f"({self.id}) Using config file at {result}")
148
+ self.config_file = result
149
+
106
150
  @classmethod
107
- @abc.abstractmethod
108
- def set_config_file(cls, config: pytest.Config) -> None: ...
109
- @staticmethod
110
- @abc.abstractmethod
111
- def add_pytest_option(group: pytest.OptionGroup) -> None: ...
151
+ def add_pytest_option(cls, group: pytest.OptionGroup) -> None:
152
+ group.addoption(
153
+ cls.longopt_for_config(),
154
+ type=str,
155
+ default=None,
156
+ metavar="RELATIVE_PATH",
157
+ help=f"{cls.id} configuration file, path is relative to pytest "
158
+ f"rootdir. If unspecified, use {cls.id} default behavior",
159
+ )
@@ -0,0 +1,7 @@
1
+ pytest_plugins = ["pytester"]
2
+
3
+ # Tests in this folder are fragile. If return type is taken away from function
4
+ # (the "-> None" part), mypy will not complain about the type hint under
5
+ # non-strict mode. Instead, it infers a blanket type of "Any" for "x", which
6
+ # nullifies typeguard checking (no TypeCheckError raised). A crude guard against
7
+ # this situation is implemented inside revealtype_injector.
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+
6
+ class TestImport:
7
+ def test_basic(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
+ import pytest
19
+ from typeguard import TypeCheckError
20
+
21
+ if sys.version_info >= (3, 11):
22
+ from typing import reveal_type
23
+ else:
24
+ from typing_extensions import reveal_type
25
+
26
+ def test_inferred() -> None:
27
+ x = 1
28
+ reveal_type(x)
29
+
30
+ def test_bad_inline_hint() -> None:
31
+ x: str = 1 # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
32
+ with pytest.raises(TypeCheckError, match='is not an instance of str'):
33
+ reveal_type(x)
34
+ """
35
+ )
36
+ result = pytester.runpytest("--tb=short", "-v")
37
+ result.assert_outcomes(passed=2)
38
+
39
+ def test_import_as(self, pytester: pytest.Pytester) -> None:
40
+ pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
41
+ pytester.makepyprojecttoml(
42
+ """
43
+ [tool.basedpyright]
44
+ reportUnreachable = false
45
+ reportUnusedCallResult = false
46
+ """
47
+ )
48
+ pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
49
+ """
50
+ import sys
51
+ import pytest
52
+ from typeguard import TypeCheckError
53
+
54
+ if sys.version_info >= (3, 11):
55
+ from typing import reveal_type as rt
56
+ else:
57
+ from typing_extensions import reveal_type as rt
58
+
59
+ def test_inferred() -> None:
60
+ x = 1
61
+ rt(x)
62
+
63
+ def test_bad_inline_hint() -> None:
64
+ x: str = 1 # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
65
+ with pytest.raises(TypeCheckError, match='is not an instance of str'):
66
+ rt(x)
67
+ """
68
+ )
69
+ result = pytester.runpytest("--tb=short", "-v")
70
+ result.assert_outcomes(passed=2)
71
+
72
+ def test_import_module_as(self, pytester: pytest.Pytester) -> None:
73
+ pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
74
+ pytester.makepyprojecttoml(
75
+ """
76
+ [tool.basedpyright]
77
+ reportUnreachable = false
78
+ reportUnusedCallResult = false
79
+ """
80
+ )
81
+ pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
82
+ """
83
+ import sys
84
+ import pytest
85
+ from typeguard import TypeCheckError
86
+
87
+ if sys.version_info >= (3, 11):
88
+ import typing as t
89
+ else:
90
+ import typing_extensions as t
91
+
92
+ def test_inferred() -> None:
93
+ x = 1
94
+ t.reveal_type(x)
95
+
96
+ def test_bad_inline_hint() -> None:
97
+ x: str = 1 # type: ignore[assignment] # pyright: ignore[reportAssignmentType]
98
+ with pytest.raises(TypeCheckError, match='is not an instance of str'):
99
+ t.reveal_type(x)
100
+ """
101
+ )
102
+ result = pytester.runpytest("--tb=short", "-v")
103
+ result.assert_outcomes(passed=2)
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+
5
+ import pytest
6
+
7
+
8
+ class TestDisableTypeChecker:
9
+ content_fail = inspect.cleandoc(
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_bad_inline_hint() -> None:
21
+ x: str = 1 # {}
22
+ with pytest.raises(TypeCheckError, match='is not an instance of str'):
23
+ reveal_type(x)
24
+ """
25
+ )
26
+
27
+ def _gen_pytest_opts(self, adapter: list[str]) -> list[str]:
28
+ result = [f"--revealtype-disable-adapter={a}" for a in adapter]
29
+ result.extend(["--tb=short", "-vv"])
30
+ return result
31
+
32
+ def test_disable_mypy_fail(self, pytester: pytest.Pytester) -> None:
33
+ pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
34
+ pytester.makepyprojecttoml(
35
+ """
36
+ [tool.pyright]
37
+ typeCheckingMode = 'strict'
38
+ enableTypeIgnoreComments = false
39
+ reportUnreachable = false
40
+ reportUnusedCallResult = false
41
+ """
42
+ )
43
+ pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
44
+ self.content_fail
45
+ )
46
+ opts = self._gen_pytest_opts(["mypy"])
47
+ result = pytester.runpytest(*opts)
48
+ assert result.ret == pytest.ExitCode.INTERNAL_ERROR
49
+ result.assert_outcomes(passed=0, failed=0)
50
+
51
+ def test_disable_mypy_pass(self, pytester: pytest.Pytester) -> None:
52
+ pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
53
+ pytester.makepyprojecttoml(
54
+ """
55
+ [tool.pyright]
56
+ typeCheckingMode = 'strict'
57
+ enableTypeIgnoreComments = false
58
+ reportUnreachable = false
59
+ reportUnusedCallResult = false
60
+ """
61
+ )
62
+ content_masked = self.content_fail.format(
63
+ "pyright: ignore[reportAssignmentType]",
64
+ )
65
+ pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
66
+ content_masked
67
+ )
68
+ opts = self._gen_pytest_opts(["mypy"])
69
+ result = pytester.runpytest(*opts)
70
+ assert result.ret == pytest.ExitCode.OK
71
+ result.assert_outcomes(passed=1, failed=0)
72
+
73
+ def test_enable_mypy_only(self, pytester: pytest.Pytester) -> None:
74
+ pytester.makeconftest("pytest_plugins = ['pytest_revealtype_injector.plugin']")
75
+ pytester.makepyprojecttoml(
76
+ """
77
+ [tool.mypy]
78
+ strict = true
79
+ """
80
+ )
81
+ pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
82
+ self.content_fail
83
+ )
84
+ opts = self._gen_pytest_opts(["basedpyright", "pyright"])
85
+ result = pytester.runpytest(*opts)
86
+ assert result.ret == pytest.ExitCode.INTERNAL_ERROR
87
+ result.assert_outcomes(passed=0, failed=0)
88
+
89
+ content_masked = self.content_fail.format("type: ignore[assignment]")
90
+ pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
91
+ content_masked
92
+ )
93
+ result = pytester.runpytest(*opts)
94
+ assert result.ret == pytest.ExitCode.OK
95
+ result.assert_outcomes(passed=1, failed=0)
@@ -1,13 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from ..models import TypeCheckerAdapter
4
- from . import mypy_, pyright_
5
-
6
-
7
- # Hardcode will do for now, it's not like we're going to have more
8
- # adapters soon. Pyre and PyType are not there yet.
9
- def discovery() -> set[TypeCheckerAdapter]:
10
- return {
11
- pyright_.adapter,
12
- mypy_.adapter,
13
- }
@@ -1,73 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import inspect
4
-
5
- import pytest
6
-
7
- from . import adapter, log
8
- from .main import revealtype_injector
9
-
10
- _logger = log.get_logger()
11
-
12
-
13
- def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> None:
14
- assert pyfuncitem.module is not None
15
- for name in dir(pyfuncitem.module):
16
- if name.startswith("__") or name.startswith("@py"):
17
- continue
18
-
19
- item = getattr(pyfuncitem.module, name)
20
- if inspect.isfunction(item):
21
- if item.__name__ == "reveal_type" and item.__module__ in {
22
- "typing",
23
- "typing_extensions",
24
- }:
25
- setattr(pyfuncitem.module, name, revealtype_injector)
26
- _logger.info(
27
- f"Replaced {name}() from global import with {revealtype_injector}"
28
- )
29
- continue
30
-
31
- if inspect.ismodule(item):
32
- if item.__name__ not in {"typing", "typing_extensions"}:
33
- continue
34
- assert hasattr(item, "reveal_type")
35
- setattr(item, "reveal_type", revealtype_injector)
36
- _logger.info(f"Replaced {name}.reveal_type() with {revealtype_injector}")
37
- continue
38
-
39
-
40
- def pytest_collection_finish(session: pytest.Session) -> None:
41
- files = {i.path for i in session.items}
42
- for adp in adapter.discovery():
43
- if adp.enabled:
44
- adp.run_typechecker_on(files)
45
-
46
-
47
- def pytest_addoption(parser: pytest.Parser) -> None:
48
- group = parser.getgroup(
49
- "revealtype-injector",
50
- description="Type checker related options for revealtype-injector",
51
- )
52
- adapters = adapter.discovery()
53
- choices = tuple(adp.id for adp in adapters)
54
- group.addoption(
55
- "--revealtype-disable-adapter",
56
- type=str,
57
- choices=choices,
58
- default=None,
59
- help="Disable this type checker when using revealtype-injector plugin",
60
- )
61
- for adp in adapters:
62
- adp.add_pytest_option(group)
63
-
64
-
65
- def pytest_configure(config: pytest.Config) -> None:
66
- _logger.setLevel(config.get_verbosity(config.VERBOSITY_TEST_CASES))
67
- # Forget config stash, it can't store collection of unserialized objects
68
- for adp in adapter.discovery():
69
- if config.option.revealtype_disable_adapter == adp.id:
70
- adp.enabled = False
71
- _logger.info(f"Disable {adp.id} adapter based on command line option")
72
- else:
73
- adp.set_config_file(config)
@@ -1 +0,0 @@
1
- pytest_plugins = ["pytester"]
@@ -1,91 +0,0 @@
1
- import pytest
2
-
3
-
4
- def test_basic(pytester: pytest.Pytester) -> None:
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)
32
-
33
-
34
- def test_import_as(pytester: pytest.Pytester) -> None:
35
- pytester.makeconftest(
36
- "pytest_plugins = ['pytest_revealtype_injector.plugin']"
37
- )
38
-
39
- pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
40
- """
41
- import sys
42
- import pytest
43
- from typeguard import TypeCheckError
44
-
45
- if sys.version_info >= (3, 11):
46
- from typing import reveal_type as rt
47
- else:
48
- from typing_extensions import reveal_type as rt
49
-
50
- def test_inferred():
51
- x = 1
52
- rt(x)
53
-
54
- def test_bad_inline_hint():
55
- x: str = 1 # type: ignore # pyright: ignore
56
- with pytest.raises(TypeCheckError, match='is not an instance of str'):
57
- rt(x)
58
- """
59
- )
60
- result = pytester.runpytest()
61
- result.assert_outcomes(passed=2)
62
-
63
-
64
- def test_import_module_as(pytester: pytest.Pytester) -> None:
65
- pytester.makeconftest(
66
- "pytest_plugins = ['pytest_revealtype_injector.plugin']"
67
- )
68
-
69
- pytester.makepyfile( # pyright: ignore[reportUnknownMemberType]
70
- """
71
- import sys
72
- import pytest
73
- from typeguard import TypeCheckError
74
-
75
- if sys.version_info >= (3, 11):
76
- import typing as t
77
- else:
78
- import typing_extensions as t
79
-
80
- def test_inferred():
81
- x = 1
82
- t.reveal_type(x)
83
-
84
- def test_bad_inline_hint():
85
- x: str = 1 # type: ignore # pyright: ignore
86
- with pytest.raises(TypeCheckError, match='is not an instance of str'):
87
- t.reveal_type(x)
88
- """
89
- )
90
- result = pytester.runpytest()
91
- result.assert_outcomes(passed=2)