rustest 0.14.0__cp313-cp313-macosx_11_0_arm64.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.
- rustest/__init__.py +39 -0
- rustest/__main__.py +10 -0
- rustest/approx.py +176 -0
- rustest/builtin_fixtures.py +1137 -0
- rustest/cli.py +135 -0
- rustest/compat/__init__.py +3 -0
- rustest/compat/pytest.py +1141 -0
- rustest/core.py +56 -0
- rustest/decorators.py +968 -0
- rustest/fixture_registry.py +130 -0
- rustest/py.typed +0 -0
- rustest/reporting.py +63 -0
- rustest/rust.cpython-313-darwin.so +0 -0
- rustest/rust.py +23 -0
- rustest/rust.pyi +43 -0
- rustest-0.14.0.dist-info/METADATA +151 -0
- rustest-0.14.0.dist-info/RECORD +20 -0
- rustest-0.14.0.dist-info/WHEEL +4 -0
- rustest-0.14.0.dist-info/entry_points.txt +2 -0
- rustest-0.14.0.dist-info/licenses/LICENSE +21 -0
rustest/compat/pytest.py
ADDED
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest compatibility shim for rustest.
|
|
3
|
+
|
|
4
|
+
This module provides a pytest-compatible API that translates to rustest
|
|
5
|
+
under the hood. It allows users to run existing pytest test suites with
|
|
6
|
+
rustest by using: rustest --pytest-compat tests/
|
|
7
|
+
|
|
8
|
+
Supported pytest features:
|
|
9
|
+
- @pytest.fixture() with scopes (function/class/module/session)
|
|
10
|
+
- @pytest.mark.* decorators
|
|
11
|
+
- @pytest.mark.parametrize()
|
|
12
|
+
- @pytest.mark.skip() and @pytest.mark.skipif()
|
|
13
|
+
- @pytest.mark.asyncio (from pytest-asyncio plugin)
|
|
14
|
+
- pytest.raises()
|
|
15
|
+
- pytest.approx()
|
|
16
|
+
- Type annotations: pytest.FixtureRequest, pytest.MonkeyPatch, pytest.TmpPathFactory,
|
|
17
|
+
pytest.TmpDirFactory, pytest.ExceptionInfo
|
|
18
|
+
- Built-in fixtures: tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, monkeypatch, request
|
|
19
|
+
|
|
20
|
+
Note: The request fixture is a basic stub with limited functionality. Many attributes
|
|
21
|
+
will have default/None values. It's provided for compatibility, not full pytest features.
|
|
22
|
+
|
|
23
|
+
Not supported (with clear error messages):
|
|
24
|
+
- Fixture params (@pytest.fixture(params=[...]))
|
|
25
|
+
- Some built-in fixtures (capsys, capfd, caplog, etc.)
|
|
26
|
+
- Assertion rewriting
|
|
27
|
+
- Other pytest plugins
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
# Instead of modifying your tests, just run:
|
|
31
|
+
$ rustest --pytest-compat tests/
|
|
32
|
+
|
|
33
|
+
# Your existing pytest tests will run with rustest:
|
|
34
|
+
import pytest # This gets intercepted
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def database():
|
|
38
|
+
return Database()
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("value", [1, 2, 3])
|
|
41
|
+
def test_values(value):
|
|
42
|
+
assert value > 0
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# pyright: reportMissingImports=false
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
from typing import Any, Callable, TypeVar, TypedDict, cast
|
|
50
|
+
|
|
51
|
+
# Import rustest's actual implementations
|
|
52
|
+
from rustest.decorators import (
|
|
53
|
+
fixture as _rustest_fixture,
|
|
54
|
+
parametrize as _rustest_parametrize,
|
|
55
|
+
skip_decorator as _rustest_skip_decorator,
|
|
56
|
+
mark as _rustest_mark,
|
|
57
|
+
raises as _rustest_raises,
|
|
58
|
+
fail as _rustest_fail,
|
|
59
|
+
Failed as _rustest_Failed,
|
|
60
|
+
Skipped as _rustest_Skipped,
|
|
61
|
+
XFailed as _rustest_XFailed,
|
|
62
|
+
xfail as _rustest_xfail,
|
|
63
|
+
skip as _rustest_skip_function,
|
|
64
|
+
ExceptionInfo,
|
|
65
|
+
ParameterSet,
|
|
66
|
+
)
|
|
67
|
+
from rustest.approx import approx as _rustest_approx
|
|
68
|
+
from rustest.builtin_fixtures import (
|
|
69
|
+
Cache,
|
|
70
|
+
CaptureFixture,
|
|
71
|
+
LogCaptureFixture,
|
|
72
|
+
MonkeyPatch,
|
|
73
|
+
TmpPathFactory,
|
|
74
|
+
TmpDirFactory,
|
|
75
|
+
cache,
|
|
76
|
+
caplog,
|
|
77
|
+
capsys,
|
|
78
|
+
capfd,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"fixture",
|
|
83
|
+
"parametrize",
|
|
84
|
+
"mark",
|
|
85
|
+
"skip",
|
|
86
|
+
"xfail",
|
|
87
|
+
"raises",
|
|
88
|
+
"fail",
|
|
89
|
+
"Failed",
|
|
90
|
+
"Skipped",
|
|
91
|
+
"XFailed",
|
|
92
|
+
"approx",
|
|
93
|
+
"param",
|
|
94
|
+
"warns",
|
|
95
|
+
"deprecated_call",
|
|
96
|
+
"importorskip",
|
|
97
|
+
"Cache",
|
|
98
|
+
"CaptureFixture",
|
|
99
|
+
"LogCaptureFixture",
|
|
100
|
+
"FixtureRequest",
|
|
101
|
+
"Node",
|
|
102
|
+
"Config",
|
|
103
|
+
"MonkeyPatch",
|
|
104
|
+
"TmpPathFactory",
|
|
105
|
+
"TmpDirFactory",
|
|
106
|
+
"ExceptionInfo",
|
|
107
|
+
"cache",
|
|
108
|
+
"caplog",
|
|
109
|
+
"capsys",
|
|
110
|
+
"capfd",
|
|
111
|
+
# Pytest plugin decorator
|
|
112
|
+
"hookimpl",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
# Type variable for generic functions
|
|
116
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class MarkerDict(TypedDict):
|
|
120
|
+
"""Type definition for marker dictionaries."""
|
|
121
|
+
|
|
122
|
+
name: str
|
|
123
|
+
args: tuple[Any, ...]
|
|
124
|
+
kwargs: dict[str, Any]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Node:
|
|
128
|
+
"""
|
|
129
|
+
Pytest-compatible Node object representing a test or collection node.
|
|
130
|
+
|
|
131
|
+
This provides basic node information for compatibility with pytest fixtures
|
|
132
|
+
that access request.node.
|
|
133
|
+
|
|
134
|
+
**Supported:**
|
|
135
|
+
- node.name: Test/node name
|
|
136
|
+
- node.nodeid: Full test identifier
|
|
137
|
+
- node.get_closest_marker(name): Get marker by name
|
|
138
|
+
- node.add_marker(marker): Add marker to node
|
|
139
|
+
- node.keywords: Dictionary of keywords/markers
|
|
140
|
+
|
|
141
|
+
**Limited Support:**
|
|
142
|
+
- node.parent: Always None (not implemented)
|
|
143
|
+
- node.session: Always None (not implemented)
|
|
144
|
+
- node.config: Returns associated Config object if available
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
def test_example(request):
|
|
148
|
+
assert request.node.name == "test_example"
|
|
149
|
+
marker = request.node.get_closest_marker("skip")
|
|
150
|
+
if marker:
|
|
151
|
+
pytest.skip(marker.kwargs.get("reason", ""))
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
name: str = "",
|
|
157
|
+
nodeid: str = "",
|
|
158
|
+
markers: list[MarkerDict] | None = None,
|
|
159
|
+
config: Any = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Initialize a Node.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: Name of the test/node
|
|
165
|
+
nodeid: Full identifier for the test (e.g., "tests/test_foo.py::test_bar")
|
|
166
|
+
markers: List of marker dictionaries
|
|
167
|
+
config: Associated Config object
|
|
168
|
+
"""
|
|
169
|
+
super().__init__()
|
|
170
|
+
self.name: str = name
|
|
171
|
+
self.nodeid: str = nodeid
|
|
172
|
+
self._markers: list[MarkerDict] = markers or []
|
|
173
|
+
self.config: Any = config
|
|
174
|
+
self.parent: Any = None
|
|
175
|
+
self.session: Any = None
|
|
176
|
+
# Keywords dict for pytest compatibility
|
|
177
|
+
self.keywords: dict[str, Any] = {}
|
|
178
|
+
# Add markers to keywords
|
|
179
|
+
for marker in self._markers:
|
|
180
|
+
if "name" in marker:
|
|
181
|
+
self.keywords[marker["name"]] = True
|
|
182
|
+
|
|
183
|
+
def get_closest_marker(self, name: str) -> Any:
|
|
184
|
+
"""Get the closest marker with the given name.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
name: Name of the marker to retrieve
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
A marker object with args and kwargs attributes, or None if not found
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
skip_marker = request.node.get_closest_marker("skip")
|
|
194
|
+
if skip_marker:
|
|
195
|
+
reason = skip_marker.kwargs.get("reason", "")
|
|
196
|
+
"""
|
|
197
|
+
# Find the first marker with the given name
|
|
198
|
+
for marker in reversed(self._markers): # Start from most recently added
|
|
199
|
+
if marker.get("name") == name:
|
|
200
|
+
# Return a simple object with args and kwargs attributes
|
|
201
|
+
return _MarkerInfo(
|
|
202
|
+
name=name,
|
|
203
|
+
args=marker.get("args", ()),
|
|
204
|
+
kwargs=marker.get("kwargs", {}),
|
|
205
|
+
)
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def add_marker(self, marker: Any, append: bool = True) -> None:
|
|
209
|
+
"""Add a marker to this node.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
marker: Marker to add (can be string name or marker object)
|
|
213
|
+
append: If True, append to markers list; if False, prepend
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
request.node.add_marker("slow")
|
|
217
|
+
request.node.add_marker(pytest.mark.xfail(reason="known bug"))
|
|
218
|
+
"""
|
|
219
|
+
marker_dict: MarkerDict
|
|
220
|
+
|
|
221
|
+
# Handle string markers
|
|
222
|
+
if isinstance(marker, str):
|
|
223
|
+
marker_dict = {"name": marker, "args": (), "kwargs": {}}
|
|
224
|
+
# Handle ParameterSet/MarkDecorator objects
|
|
225
|
+
elif hasattr(marker, "__rustest_marks__"):
|
|
226
|
+
# This is a decorated object with marks
|
|
227
|
+
marks: list[Any] = getattr(marker, "__rustest_marks__", [])
|
|
228
|
+
for mark in marks:
|
|
229
|
+
if append:
|
|
230
|
+
self._markers.append(mark)
|
|
231
|
+
else:
|
|
232
|
+
self._markers.insert(0, mark)
|
|
233
|
+
# Add to keywords
|
|
234
|
+
if "name" in mark and isinstance(mark.get("name"), str):
|
|
235
|
+
name_str: str = mark["name"]
|
|
236
|
+
self.keywords[name_str] = True
|
|
237
|
+
return
|
|
238
|
+
# Handle mark objects with name/args/kwargs
|
|
239
|
+
elif hasattr(marker, "name"):
|
|
240
|
+
marker_dict = {
|
|
241
|
+
"name": str(marker.name),
|
|
242
|
+
"args": getattr(marker, "args", ()),
|
|
243
|
+
"kwargs": getattr(marker, "kwargs", {}),
|
|
244
|
+
}
|
|
245
|
+
# Handle dict markers directly
|
|
246
|
+
elif isinstance(marker, dict):
|
|
247
|
+
# Validate and normalize the dict
|
|
248
|
+
# Type ignores needed for untyped dict from external sources
|
|
249
|
+
marker_dict = {
|
|
250
|
+
"name": str(marker.get("name", "")), # type: ignore[arg-type]
|
|
251
|
+
"args": cast(tuple[Any, ...], marker.get("args", ())), # type: ignore[reportUnknownMemberType]
|
|
252
|
+
"kwargs": cast(dict[str, Any], marker.get("kwargs", {})), # type: ignore[reportUnknownMemberType]
|
|
253
|
+
}
|
|
254
|
+
else:
|
|
255
|
+
# Unknown marker type - try to extract what we can
|
|
256
|
+
marker_dict = {"name": str(marker), "args": (), "kwargs": {}}
|
|
257
|
+
|
|
258
|
+
if append:
|
|
259
|
+
self._markers.append(marker_dict)
|
|
260
|
+
else:
|
|
261
|
+
self._markers.insert(0, marker_dict)
|
|
262
|
+
|
|
263
|
+
# Add to keywords
|
|
264
|
+
name = marker_dict["name"]
|
|
265
|
+
if name: # name is now guaranteed to be str
|
|
266
|
+
self.keywords[name] = True
|
|
267
|
+
|
|
268
|
+
def listextrakeywords(self) -> set[str]:
|
|
269
|
+
"""Return a set of extra keywords/markers for this node.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Set of marker/keyword names
|
|
273
|
+
"""
|
|
274
|
+
return set(self.keywords.keys())
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class _MarkerInfo:
|
|
278
|
+
"""Simple marker info object returned by get_closest_marker()."""
|
|
279
|
+
|
|
280
|
+
def __init__(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> None:
|
|
281
|
+
super().__init__()
|
|
282
|
+
self.name = name
|
|
283
|
+
self.args = args
|
|
284
|
+
self.kwargs = kwargs
|
|
285
|
+
|
|
286
|
+
def __repr__(self) -> str:
|
|
287
|
+
return f"Mark(name={self.name!r}, args={self.args!r}, kwargs={self.kwargs!r})"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class Config:
|
|
291
|
+
"""
|
|
292
|
+
Pytest-compatible Config object for accessing test configuration.
|
|
293
|
+
|
|
294
|
+
This provides basic configuration access for compatibility with pytest
|
|
295
|
+
fixtures that access request.config.
|
|
296
|
+
|
|
297
|
+
**Supported:**
|
|
298
|
+
- config.getoption(name, default=None): Get command-line option value
|
|
299
|
+
- config.getini(name): Get configuration value from pytest.ini/setup.cfg/tox.ini
|
|
300
|
+
- config.rootpath: Root directory path (always returns current directory)
|
|
301
|
+
- config.inipath: Path to config file (always None in rustest)
|
|
302
|
+
|
|
303
|
+
**Limited Support:**
|
|
304
|
+
- config.pluginmanager: Stub PluginManager (minimal functionality)
|
|
305
|
+
- config.option: Namespace with option values
|
|
306
|
+
|
|
307
|
+
**Not Supported:**
|
|
308
|
+
- Advanced plugin configuration
|
|
309
|
+
- Hook specifications
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
def test_example(request):
|
|
313
|
+
verbose = request.config.getoption("verbose", default=0)
|
|
314
|
+
if verbose > 1:
|
|
315
|
+
print("Running in verbose mode")
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
def __init__(
|
|
319
|
+
self, options: dict[str, Any] | None = None, ini_values: dict[str, Any] | None = None
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Initialize a Config.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
options: Dictionary of command-line options
|
|
325
|
+
ini_values: Dictionary of ini configuration values
|
|
326
|
+
"""
|
|
327
|
+
super().__init__()
|
|
328
|
+
self._options: dict[str, Any] = options or {}
|
|
329
|
+
self._ini_values: dict[str, Any] = ini_values or {}
|
|
330
|
+
|
|
331
|
+
# Create option namespace for compatibility
|
|
332
|
+
self.option = _OptionNamespace(self._options)
|
|
333
|
+
|
|
334
|
+
# Stub pluginmanager
|
|
335
|
+
self.pluginmanager = _PluginManagerStub()
|
|
336
|
+
|
|
337
|
+
# Paths
|
|
338
|
+
from pathlib import Path
|
|
339
|
+
|
|
340
|
+
self.rootpath: Path = Path.cwd()
|
|
341
|
+
self.inipath: Path | None = None
|
|
342
|
+
|
|
343
|
+
def getoption(self, name: str, default: Any = None, skip: bool = False) -> Any:
|
|
344
|
+
"""Get command-line option value.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
name: Option name (e.g., "verbose", "capture", "tb")
|
|
348
|
+
default: Default value if option not found
|
|
349
|
+
skip: If True and option not found, skip the test
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Option value or default
|
|
353
|
+
|
|
354
|
+
Example:
|
|
355
|
+
verbose = request.config.getoption("verbose", default=0)
|
|
356
|
+
"""
|
|
357
|
+
# Remove leading dashes from option name
|
|
358
|
+
clean_name = name.lstrip("-")
|
|
359
|
+
|
|
360
|
+
value = self._options.get(clean_name, default)
|
|
361
|
+
|
|
362
|
+
if skip and value == default and clean_name not in self._options:
|
|
363
|
+
# Import skip function from rustest
|
|
364
|
+
from rustest.decorators import skip as skip_test
|
|
365
|
+
|
|
366
|
+
skip_test(f"Option '{name}' not found")
|
|
367
|
+
|
|
368
|
+
return value
|
|
369
|
+
|
|
370
|
+
def getini(self, name: str) -> Any:
|
|
371
|
+
"""Get configuration value from pytest.ini/setup.cfg/tox.ini.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
name: Configuration option name
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Configuration value (default empty string/list if not found)
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
testpaths = request.config.getini("testpaths")
|
|
381
|
+
"""
|
|
382
|
+
value = self._ini_values.get(name)
|
|
383
|
+
|
|
384
|
+
# Return appropriate default based on common ini values
|
|
385
|
+
if value is None:
|
|
386
|
+
# Common list-type ini values
|
|
387
|
+
if name in {
|
|
388
|
+
"testpaths",
|
|
389
|
+
"python_files",
|
|
390
|
+
"python_classes",
|
|
391
|
+
"python_functions",
|
|
392
|
+
"markers",
|
|
393
|
+
"filterwarnings",
|
|
394
|
+
}:
|
|
395
|
+
return []
|
|
396
|
+
# Common string-type ini values
|
|
397
|
+
return ""
|
|
398
|
+
|
|
399
|
+
return value
|
|
400
|
+
|
|
401
|
+
def addinivalue_line(self, name: str, line: str) -> None:
|
|
402
|
+
"""Add a line to an ini-file option.
|
|
403
|
+
|
|
404
|
+
This is a no-op in rustest for compatibility.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
name: Option name
|
|
408
|
+
line: Line to add
|
|
409
|
+
"""
|
|
410
|
+
# No-op for compatibility
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class _OptionNamespace:
|
|
415
|
+
"""Namespace object for accessing options as attributes."""
|
|
416
|
+
|
|
417
|
+
def __init__(self, options: dict[str, Any]) -> None:
|
|
418
|
+
super().__init__()
|
|
419
|
+
self._options = options
|
|
420
|
+
|
|
421
|
+
def __getattr__(self, name: str) -> Any:
|
|
422
|
+
return self._options.get(name)
|
|
423
|
+
|
|
424
|
+
def __repr__(self) -> str:
|
|
425
|
+
return f"Namespace({self._options})"
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class _PluginManagerStub:
|
|
429
|
+
"""Stub PluginManager for basic compatibility."""
|
|
430
|
+
|
|
431
|
+
def __init__(self) -> None:
|
|
432
|
+
super().__init__()
|
|
433
|
+
self._plugins: list[Any] = []
|
|
434
|
+
|
|
435
|
+
def get_plugin(self, name: str) -> Any:
|
|
436
|
+
"""Get plugin by name (always returns None)."""
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
def hasplugin(self, name: str) -> bool:
|
|
440
|
+
"""Check if plugin is registered (always returns False)."""
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
def register(self, plugin: Any, name: str | None = None) -> None:
|
|
444
|
+
"""Register a plugin (no-op for compatibility)."""
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
def __repr__(self) -> str:
|
|
448
|
+
return "<PluginManager (stub)>"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class FixtureRequest:
|
|
452
|
+
"""
|
|
453
|
+
Pytest-compatible FixtureRequest for fixture parametrization.
|
|
454
|
+
|
|
455
|
+
This implementation provides access to fixture parameter values via
|
|
456
|
+
request.param for parametrized fixtures.
|
|
457
|
+
|
|
458
|
+
**Supported:**
|
|
459
|
+
- Type annotations: request: pytest.FixtureRequest
|
|
460
|
+
- request.param: Current parameter value for parametrized fixtures
|
|
461
|
+
- request.scope: Fixture scope (default: "function")
|
|
462
|
+
- request.node: Test node object with marker access
|
|
463
|
+
- request.config: Configuration object with option access
|
|
464
|
+
|
|
465
|
+
**Limited Support:**
|
|
466
|
+
- request.node.get_closest_marker(name): Get marker by name
|
|
467
|
+
- request.node.add_marker(marker): Add marker to node
|
|
468
|
+
- request.config.getoption(name): Get command-line option
|
|
469
|
+
- request.config.getini(name): Get ini configuration value
|
|
470
|
+
|
|
471
|
+
**NOT Supported (returns None or raises NotImplementedError):**
|
|
472
|
+
- request.function, cls, module: Always None
|
|
473
|
+
- request.fixturename: Always None
|
|
474
|
+
- request.addfinalizer(): Raises NotImplementedError
|
|
475
|
+
- request.getfixturevalue(): Raises NotImplementedError
|
|
476
|
+
|
|
477
|
+
Common pytest.FixtureRequest attributes:
|
|
478
|
+
- param: Parameter value (for parametrized fixtures) - SUPPORTED
|
|
479
|
+
- node: Test node object - SUPPORTED (basic functionality)
|
|
480
|
+
- config: Pytest config - SUPPORTED (basic functionality)
|
|
481
|
+
- function: Test function - Always None
|
|
482
|
+
- cls: Test class - Always None
|
|
483
|
+
- module: Test module - Always None
|
|
484
|
+
- fixturename: Name of the fixture - Always None
|
|
485
|
+
- scope: Scope of the fixture - Returns "function"
|
|
486
|
+
|
|
487
|
+
Example:
|
|
488
|
+
@pytest.fixture(params=[1, 2, 3])
|
|
489
|
+
def number(request: pytest.FixtureRequest):
|
|
490
|
+
# Access parameter value
|
|
491
|
+
return request.param
|
|
492
|
+
|
|
493
|
+
@pytest.fixture
|
|
494
|
+
def conditional_fixture(request):
|
|
495
|
+
# Check for markers
|
|
496
|
+
marker = request.node.get_closest_marker("slow")
|
|
497
|
+
if marker:
|
|
498
|
+
pytest.skip("Skipping slow test")
|
|
499
|
+
|
|
500
|
+
# Access configuration
|
|
501
|
+
verbose = request.config.getoption("verbose", default=0)
|
|
502
|
+
if verbose > 1:
|
|
503
|
+
print(f"Test: {request.node.name}")
|
|
504
|
+
|
|
505
|
+
return "fixture_value"
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
def __init__(
|
|
509
|
+
self,
|
|
510
|
+
param: Any = None,
|
|
511
|
+
node_name: str = "",
|
|
512
|
+
node_markers: list[MarkerDict] | None = None,
|
|
513
|
+
config_options: dict[str, Any] | None = None,
|
|
514
|
+
) -> None:
|
|
515
|
+
"""Initialize a FixtureRequest.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
param: The parameter value for parametrized fixtures
|
|
519
|
+
node_name: Name of the test node
|
|
520
|
+
node_markers: List of markers applied to the node
|
|
521
|
+
config_options: Dictionary of configuration options
|
|
522
|
+
"""
|
|
523
|
+
super().__init__()
|
|
524
|
+
self.param: Any = param
|
|
525
|
+
self.fixturename: str | None = None
|
|
526
|
+
self.scope: str = "function"
|
|
527
|
+
|
|
528
|
+
# Create Config and Node objects
|
|
529
|
+
self.config: Config = Config(options=config_options)
|
|
530
|
+
self.node: Node = Node(
|
|
531
|
+
name=node_name,
|
|
532
|
+
nodeid=node_name, # Use name as nodeid for now
|
|
533
|
+
markers=node_markers,
|
|
534
|
+
config=self.config,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# These remain unsupported
|
|
538
|
+
self.function: Any = None
|
|
539
|
+
self.cls: Any = None
|
|
540
|
+
self.module: Any = None
|
|
541
|
+
|
|
542
|
+
# Cache for executed fixtures (per-test)
|
|
543
|
+
self._executed_fixtures: dict[str, Any] = {}
|
|
544
|
+
|
|
545
|
+
def addfinalizer(self, finalizer: Callable[[], None]) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Add a finalizer to be called after the test.
|
|
548
|
+
|
|
549
|
+
NOT SUPPORTED in rustest pytest-compat mode.
|
|
550
|
+
|
|
551
|
+
In pytest, this would register a function to be called during teardown.
|
|
552
|
+
Rustest does not support this functionality in compat mode.
|
|
553
|
+
|
|
554
|
+
Raises:
|
|
555
|
+
NotImplementedError: Always raised with helpful message
|
|
556
|
+
|
|
557
|
+
Workaround:
|
|
558
|
+
Use fixture teardown with yield instead:
|
|
559
|
+
|
|
560
|
+
@pytest.fixture
|
|
561
|
+
def my_fixture():
|
|
562
|
+
resource = setup()
|
|
563
|
+
yield resource
|
|
564
|
+
teardown(resource) # This runs after the test
|
|
565
|
+
"""
|
|
566
|
+
msg = (
|
|
567
|
+
"request.addfinalizer() is not supported in rustest pytest-compat mode.\n"
|
|
568
|
+
"\n"
|
|
569
|
+
"Workaround: Use fixture teardown with yield:\n"
|
|
570
|
+
" @pytest.fixture\n"
|
|
571
|
+
" def my_fixture():\n"
|
|
572
|
+
" resource = setup()\n"
|
|
573
|
+
" yield resource\n"
|
|
574
|
+
" teardown(resource) # Runs after test\n"
|
|
575
|
+
"\n"
|
|
576
|
+
"For full pytest features, use pytest directly or migrate to native rustest."
|
|
577
|
+
)
|
|
578
|
+
raise NotImplementedError(msg)
|
|
579
|
+
|
|
580
|
+
def getfixturevalue(self, name: str) -> Any:
|
|
581
|
+
"""
|
|
582
|
+
Get the value of another fixture by name.
|
|
583
|
+
|
|
584
|
+
This method dynamically loads and executes fixtures at runtime by name.
|
|
585
|
+
Fixture dependencies are resolved recursively, and results are cached
|
|
586
|
+
per test execution.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
name: Name of the fixture to retrieve
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
The fixture value
|
|
593
|
+
|
|
594
|
+
Raises:
|
|
595
|
+
ValueError: If the fixture is not found
|
|
596
|
+
NotImplementedError: If the fixture is async (not yet supported)
|
|
597
|
+
|
|
598
|
+
Example:
|
|
599
|
+
@pytest.fixture
|
|
600
|
+
def user():
|
|
601
|
+
return {"name": "Alice"}
|
|
602
|
+
|
|
603
|
+
def test_dynamic(request):
|
|
604
|
+
user = request.getfixturevalue("user")
|
|
605
|
+
assert user["name"] == "Alice"
|
|
606
|
+
"""
|
|
607
|
+
# Check cache first
|
|
608
|
+
if name in self._executed_fixtures:
|
|
609
|
+
return self._executed_fixtures[name]
|
|
610
|
+
|
|
611
|
+
# Import and use the fixture registry
|
|
612
|
+
from rustest.fixture_registry import resolve_fixture
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
# Resolve the fixture (handles dependencies and caching)
|
|
616
|
+
result = resolve_fixture(name, self._executed_fixtures)
|
|
617
|
+
return result
|
|
618
|
+
except ValueError as e:
|
|
619
|
+
# Fixture not found
|
|
620
|
+
raise ValueError(f"fixture '{name}' not found") from e
|
|
621
|
+
except NotImplementedError:
|
|
622
|
+
# Async fixture
|
|
623
|
+
raise
|
|
624
|
+
|
|
625
|
+
def applymarker(self, marker: Any) -> None:
|
|
626
|
+
"""
|
|
627
|
+
Apply a marker to the test.
|
|
628
|
+
|
|
629
|
+
Supports skip, skipif, and xfail markers. Other markers are stored but ignored.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
marker: Marker to apply (can be string name or marker object)
|
|
633
|
+
|
|
634
|
+
Raises:
|
|
635
|
+
Skipped: If skip or skipif marker is applied and condition is met
|
|
636
|
+
|
|
637
|
+
Example:
|
|
638
|
+
def test_dynamic_skip(request):
|
|
639
|
+
if not has_required_library():
|
|
640
|
+
request.applymarker(pytest.mark.skip(reason="Library not available"))
|
|
641
|
+
"""
|
|
642
|
+
# First, check if this is a skip decorator function (from pytest.mark.skip)
|
|
643
|
+
# These are created by skip_decorator() and have __rustest_skip__ attribute
|
|
644
|
+
if callable(marker) and hasattr(marker, "__name__") and marker.__name__ == "decorator":
|
|
645
|
+
# This might be a skip decorator - try to apply it to a dummy function
|
|
646
|
+
# to extract the skip reason
|
|
647
|
+
def dummy():
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
decorated = marker(dummy)
|
|
652
|
+
if hasattr(decorated, "__rustest_skip__"):
|
|
653
|
+
# This is a skip decorator - extract the reason and skip
|
|
654
|
+
reason = getattr(decorated, "__rustest_skip__", "")
|
|
655
|
+
_rustest_skip_function(reason=reason)
|
|
656
|
+
return
|
|
657
|
+
except (_rustest_Skipped, _rustest_XFailed, _rustest_Failed):
|
|
658
|
+
# Re-raise test control exceptions
|
|
659
|
+
raise
|
|
660
|
+
except Exception:
|
|
661
|
+
# Swallow other exceptions (e.g., if marker() fails)
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
# Add the marker to the node
|
|
665
|
+
self.node.add_marker(marker)
|
|
666
|
+
|
|
667
|
+
# Handle MarkDecorator objects (have name, args, kwargs attributes)
|
|
668
|
+
if hasattr(marker, "name"):
|
|
669
|
+
marker_name = str(getattr(marker, "name"))
|
|
670
|
+
|
|
671
|
+
if marker_name == "skip":
|
|
672
|
+
# Extract reason from marker
|
|
673
|
+
reason = getattr(marker, "kwargs", {}).get("reason", "")
|
|
674
|
+
_rustest_skip_function(reason=reason)
|
|
675
|
+
|
|
676
|
+
elif marker_name == "skipif":
|
|
677
|
+
# Extract condition from args
|
|
678
|
+
args = getattr(marker, "args", ())
|
|
679
|
+
if args and len(args) > 0:
|
|
680
|
+
condition = args[0]
|
|
681
|
+
if condition:
|
|
682
|
+
# Condition is met, skip the test
|
|
683
|
+
reason = getattr(marker, "kwargs", {}).get("reason", "")
|
|
684
|
+
_rustest_skip_function(reason=reason)
|
|
685
|
+
|
|
686
|
+
elif marker_name == "xfail":
|
|
687
|
+
# Store xfail marker for potential later handling
|
|
688
|
+
# For now, just add it to the node - the test will run normally
|
|
689
|
+
pass
|
|
690
|
+
|
|
691
|
+
# Other markers (slow, integration, etc.) are just stored on the node
|
|
692
|
+
# No action needed - they're for pytest plugins which rustest doesn't support
|
|
693
|
+
|
|
694
|
+
def raiseerror(self, msg: str | None) -> None:
|
|
695
|
+
"""
|
|
696
|
+
Raise an error with the given message.
|
|
697
|
+
|
|
698
|
+
NOT SUPPORTED in rustest pytest-compat mode.
|
|
699
|
+
|
|
700
|
+
Raises:
|
|
701
|
+
NotImplementedError: Always raised with helpful message
|
|
702
|
+
"""
|
|
703
|
+
error_msg = (
|
|
704
|
+
"request.raiseerror() is not supported in rustest pytest-compat mode.\n"
|
|
705
|
+
"\n"
|
|
706
|
+
"For full pytest features, use pytest directly or migrate to native rustest."
|
|
707
|
+
)
|
|
708
|
+
raise NotImplementedError(error_msg)
|
|
709
|
+
|
|
710
|
+
def __repr__(self) -> str:
|
|
711
|
+
return "<FixtureRequest (rustest compat stub - limited functionality)>"
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def hookimpl(*args: Any, **kwargs: Any) -> Any:
|
|
715
|
+
"""
|
|
716
|
+
Stub for pytest.hookimpl decorator - used by pytest plugins.
|
|
717
|
+
|
|
718
|
+
NOT FUNCTIONAL in rustest pytest-compat mode. Returns a no-op decorator
|
|
719
|
+
that simply returns the function unchanged.
|
|
720
|
+
"""
|
|
721
|
+
|
|
722
|
+
def decorator(func: Any) -> Any:
|
|
723
|
+
return func
|
|
724
|
+
|
|
725
|
+
if len(args) == 1 and callable(args[0]) and not kwargs:
|
|
726
|
+
# Called as @hookimpl without parentheses
|
|
727
|
+
return args[0]
|
|
728
|
+
else:
|
|
729
|
+
# Called as @hookimpl(...) with arguments
|
|
730
|
+
return decorator
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def fixture(
|
|
734
|
+
func: F | None = None,
|
|
735
|
+
*,
|
|
736
|
+
scope: str = "function",
|
|
737
|
+
params: Any = None,
|
|
738
|
+
autouse: bool = False,
|
|
739
|
+
ids: Any = None,
|
|
740
|
+
name: str | None = None,
|
|
741
|
+
) -> F | Callable[[F], F]:
|
|
742
|
+
"""
|
|
743
|
+
Pytest-compatible fixture decorator.
|
|
744
|
+
|
|
745
|
+
Maps to rustest.fixture with full support for fixture parametrization.
|
|
746
|
+
|
|
747
|
+
Supported:
|
|
748
|
+
- scope: function/class/module/session
|
|
749
|
+
- autouse: True/False
|
|
750
|
+
- name: Override fixture name
|
|
751
|
+
- params: List of parameter values for fixture parametrization
|
|
752
|
+
- ids: Custom IDs for each parameter value
|
|
753
|
+
|
|
754
|
+
Examples:
|
|
755
|
+
@pytest.fixture
|
|
756
|
+
def simple_fixture():
|
|
757
|
+
return 42
|
|
758
|
+
|
|
759
|
+
@pytest.fixture(scope="module")
|
|
760
|
+
def database():
|
|
761
|
+
db = Database()
|
|
762
|
+
yield db
|
|
763
|
+
db.close()
|
|
764
|
+
|
|
765
|
+
@pytest.fixture(autouse=True)
|
|
766
|
+
def setup():
|
|
767
|
+
setup_environment()
|
|
768
|
+
|
|
769
|
+
@pytest.fixture(name="db")
|
|
770
|
+
def _database_fixture():
|
|
771
|
+
return Database()
|
|
772
|
+
|
|
773
|
+
@pytest.fixture(params=[1, 2, 3])
|
|
774
|
+
def number(request):
|
|
775
|
+
return request.param
|
|
776
|
+
|
|
777
|
+
@pytest.fixture(params=["mysql", "postgres"], ids=["MySQL", "PostgreSQL"])
|
|
778
|
+
def database_type(request):
|
|
779
|
+
return request.param
|
|
780
|
+
"""
|
|
781
|
+
# Map to rustest fixture - handle both @pytest.fixture and @pytest.fixture()
|
|
782
|
+
if func is not None:
|
|
783
|
+
# Called as @pytest.fixture (without parentheses)
|
|
784
|
+
return _rustest_fixture(
|
|
785
|
+
func, scope=scope, autouse=autouse, name=name, params=params, ids=ids
|
|
786
|
+
)
|
|
787
|
+
else:
|
|
788
|
+
# Called as @pytest.fixture(...) (with parentheses)
|
|
789
|
+
return _rustest_fixture(scope=scope, autouse=autouse, name=name, params=params, ids=ids) # type: ignore[return-value]
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# Direct mappings - these already have identical signatures
|
|
793
|
+
parametrize = _rustest_parametrize
|
|
794
|
+
raises = _rustest_raises
|
|
795
|
+
approx = _rustest_approx
|
|
796
|
+
skip = _rustest_skip_function # pytest.skip() function (raises Skipped)
|
|
797
|
+
fail = _rustest_fail
|
|
798
|
+
Failed = _rustest_Failed
|
|
799
|
+
Skipped = _rustest_Skipped
|
|
800
|
+
XFailed = _rustest_XFailed
|
|
801
|
+
xfail = _rustest_xfail
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class _PytestMarkCompat:
|
|
805
|
+
"""
|
|
806
|
+
Compatibility wrapper for pytest.mark.
|
|
807
|
+
|
|
808
|
+
Provides the same interface as pytest.mark by delegating to rustest.mark.
|
|
809
|
+
|
|
810
|
+
Examples:
|
|
811
|
+
@pytest.mark.slow
|
|
812
|
+
@pytest.mark.integration
|
|
813
|
+
def test_expensive():
|
|
814
|
+
pass
|
|
815
|
+
|
|
816
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
|
|
817
|
+
def test_unix():
|
|
818
|
+
pass
|
|
819
|
+
"""
|
|
820
|
+
|
|
821
|
+
def __getattr__(self, name: str) -> Any:
|
|
822
|
+
"""Delegate all mark.* access to rustest.mark.*"""
|
|
823
|
+
return getattr(_rustest_mark, name)
|
|
824
|
+
|
|
825
|
+
# Explicitly expose common marks for better IDE support
|
|
826
|
+
@property
|
|
827
|
+
def parametrize(self) -> Any:
|
|
828
|
+
"""Alias for @pytest.mark.parametrize (same as top-level parametrize)."""
|
|
829
|
+
return _rustest_mark.parametrize
|
|
830
|
+
|
|
831
|
+
def skip(self, reason: str | None = None) -> Callable[[F], F]:
|
|
832
|
+
"""Mark test as skipped.
|
|
833
|
+
|
|
834
|
+
This is the @pytest.mark.skip() decorator which should skip the test.
|
|
835
|
+
Maps to rustest's skip_decorator().
|
|
836
|
+
"""
|
|
837
|
+
return _rustest_skip_decorator(reason=reason) # type: ignore[return-value]
|
|
838
|
+
|
|
839
|
+
@property
|
|
840
|
+
def skipif(self) -> Any:
|
|
841
|
+
"""Conditional skip decorator."""
|
|
842
|
+
return _rustest_mark.skipif
|
|
843
|
+
|
|
844
|
+
@property
|
|
845
|
+
def xfail(self) -> Any:
|
|
846
|
+
"""Mark test as expected to fail."""
|
|
847
|
+
return _rustest_mark.xfail
|
|
848
|
+
|
|
849
|
+
@property
|
|
850
|
+
def asyncio(self) -> Any:
|
|
851
|
+
"""Mark async test to run with asyncio."""
|
|
852
|
+
return _rustest_mark.asyncio
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
# Create the mark instance
|
|
856
|
+
mark = _PytestMarkCompat()
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def param(*values: Any, id: str | None = None, marks: Any = None, **kwargs: Any) -> ParameterSet:
|
|
860
|
+
"""
|
|
861
|
+
Create a parameter set for use in @pytest.mark.parametrize.
|
|
862
|
+
|
|
863
|
+
This function allows you to specify custom test IDs for individual
|
|
864
|
+
parameter sets:
|
|
865
|
+
|
|
866
|
+
@pytest.mark.parametrize("x,y", [
|
|
867
|
+
pytest.param(1, 2, id="small"),
|
|
868
|
+
pytest.param(100, 200, id="large"),
|
|
869
|
+
])
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
*values: The parameter values for this test case
|
|
873
|
+
id: Optional custom test ID for this parameter set
|
|
874
|
+
marks: Optional marks to apply (currently ignored with a warning)
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
A ParameterSet object that will be handled by parametrize
|
|
878
|
+
|
|
879
|
+
Note:
|
|
880
|
+
The 'marks' parameter is accepted but not yet functional.
|
|
881
|
+
Tests with marks will run normally but marks won't be applied.
|
|
882
|
+
"""
|
|
883
|
+
if marks is not None:
|
|
884
|
+
import warnings
|
|
885
|
+
|
|
886
|
+
warnings.warn(
|
|
887
|
+
"pytest.param() marks are not yet supported in rustest pytest-compat mode. The test will run but marks will be ignored.",
|
|
888
|
+
UserWarning,
|
|
889
|
+
stacklevel=2,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
return ParameterSet(values=values, id=id, marks=marks)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class WarningsChecker:
|
|
896
|
+
"""Context manager for capturing and checking warnings.
|
|
897
|
+
|
|
898
|
+
This implements pytest.warns() functionality for rustest.
|
|
899
|
+
"""
|
|
900
|
+
|
|
901
|
+
def __init__(
|
|
902
|
+
self,
|
|
903
|
+
expected_warning: type[Warning] | tuple[type[Warning], ...] | None = None,
|
|
904
|
+
match: str | None = None,
|
|
905
|
+
):
|
|
906
|
+
super().__init__()
|
|
907
|
+
self.expected_warning = expected_warning
|
|
908
|
+
self.match = match
|
|
909
|
+
self._records: list[Any] = []
|
|
910
|
+
self._catch_warnings: Any = None
|
|
911
|
+
|
|
912
|
+
def __enter__(self) -> list[Any]:
|
|
913
|
+
import warnings
|
|
914
|
+
|
|
915
|
+
self._catch_warnings = warnings.catch_warnings(record=True)
|
|
916
|
+
self._records = self._catch_warnings.__enter__()
|
|
917
|
+
# Cause all warnings to always be triggered
|
|
918
|
+
warnings.simplefilter("always")
|
|
919
|
+
return self._records
|
|
920
|
+
|
|
921
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
922
|
+
if self._catch_warnings is not None:
|
|
923
|
+
self._catch_warnings.__exit__(exc_type, exc_val, exc_tb)
|
|
924
|
+
|
|
925
|
+
# If there was an exception, don't check warnings
|
|
926
|
+
if exc_type is not None:
|
|
927
|
+
return
|
|
928
|
+
|
|
929
|
+
# If no expected warning specified, just return the records
|
|
930
|
+
if self.expected_warning is None:
|
|
931
|
+
return
|
|
932
|
+
|
|
933
|
+
# Check that at least one matching warning was raised
|
|
934
|
+
matching_warnings: list[Any] = []
|
|
935
|
+
for record in self._records:
|
|
936
|
+
# Check warning type
|
|
937
|
+
if isinstance(self.expected_warning, tuple):
|
|
938
|
+
type_matches = issubclass(record.category, self.expected_warning)
|
|
939
|
+
else:
|
|
940
|
+
type_matches = issubclass(record.category, self.expected_warning)
|
|
941
|
+
|
|
942
|
+
if not type_matches:
|
|
943
|
+
continue
|
|
944
|
+
|
|
945
|
+
# Check message match if specified
|
|
946
|
+
if self.match is not None:
|
|
947
|
+
import re
|
|
948
|
+
|
|
949
|
+
message_str = str(record.message)
|
|
950
|
+
if not re.search(self.match, message_str):
|
|
951
|
+
continue
|
|
952
|
+
|
|
953
|
+
matching_warnings.append(record)
|
|
954
|
+
|
|
955
|
+
if not matching_warnings:
|
|
956
|
+
# Build error message
|
|
957
|
+
if isinstance(self.expected_warning, tuple):
|
|
958
|
+
expected_str = " or ".join(w.__name__ for w in self.expected_warning)
|
|
959
|
+
else:
|
|
960
|
+
expected_str = self.expected_warning.__name__
|
|
961
|
+
|
|
962
|
+
if self.match:
|
|
963
|
+
expected_str += f" matching {self.match!r}"
|
|
964
|
+
|
|
965
|
+
if self._records:
|
|
966
|
+
actual = ", ".join(f"{r.category.__name__}({r.message!s})" for r in self._records)
|
|
967
|
+
msg = f"Expected {expected_str} but got: {actual}"
|
|
968
|
+
else:
|
|
969
|
+
msg = f"Expected {expected_str} but no warnings were raised"
|
|
970
|
+
|
|
971
|
+
raise AssertionError(msg)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def warns(
|
|
975
|
+
expected_warning: type[Warning] | tuple[type[Warning], ...] | None = None,
|
|
976
|
+
*,
|
|
977
|
+
match: str | None = None,
|
|
978
|
+
) -> WarningsChecker:
|
|
979
|
+
"""
|
|
980
|
+
Context manager to capture and assert warnings.
|
|
981
|
+
|
|
982
|
+
This function can be used as a context manager to check that certain
|
|
983
|
+
warnings are raised during execution.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
expected_warning: The expected warning class(es), or None to capture all
|
|
987
|
+
match: Optional regex pattern to match against the warning message
|
|
988
|
+
|
|
989
|
+
Returns:
|
|
990
|
+
A context manager that yields a list of captured warnings
|
|
991
|
+
|
|
992
|
+
Examples:
|
|
993
|
+
# Check that a DeprecationWarning is raised
|
|
994
|
+
with pytest.warns(DeprecationWarning):
|
|
995
|
+
some_deprecated_function()
|
|
996
|
+
|
|
997
|
+
# Check warning message matches pattern
|
|
998
|
+
with pytest.warns(UserWarning, match="must be positive"):
|
|
999
|
+
function_with_warning(-1)
|
|
1000
|
+
|
|
1001
|
+
# Capture all warnings without asserting
|
|
1002
|
+
with pytest.warns() as record:
|
|
1003
|
+
some_code()
|
|
1004
|
+
assert len(record) == 2
|
|
1005
|
+
"""
|
|
1006
|
+
return WarningsChecker(expected_warning, match)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def deprecated_call(*, match: str | None = None) -> WarningsChecker:
|
|
1010
|
+
"""
|
|
1011
|
+
Context manager to check that a deprecation warning is raised.
|
|
1012
|
+
|
|
1013
|
+
This is a convenience wrapper around warns(DeprecationWarning).
|
|
1014
|
+
|
|
1015
|
+
Args:
|
|
1016
|
+
match: Optional regex pattern to match against the warning message
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
A context manager that yields a list of captured warnings
|
|
1020
|
+
|
|
1021
|
+
Example:
|
|
1022
|
+
with pytest.deprecated_call():
|
|
1023
|
+
some_deprecated_function()
|
|
1024
|
+
"""
|
|
1025
|
+
return WarningsChecker((DeprecationWarning, PendingDeprecationWarning), match)
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def importorskip(
|
|
1029
|
+
modname: str,
|
|
1030
|
+
minversion: str | None = None,
|
|
1031
|
+
reason: str | None = None,
|
|
1032
|
+
*,
|
|
1033
|
+
exc_type: type[ImportError] = ImportError,
|
|
1034
|
+
) -> Any:
|
|
1035
|
+
"""
|
|
1036
|
+
Import and return the requested module, or skip the test if unavailable.
|
|
1037
|
+
|
|
1038
|
+
This function attempts to import a module and returns it if successful.
|
|
1039
|
+
If the import fails or the version is too old, the current test is skipped.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
modname: The name of the module to import
|
|
1043
|
+
minversion: Minimum required version string (compared with pkg.__version__)
|
|
1044
|
+
reason: Custom reason message to display when skipping
|
|
1045
|
+
exc_type: The exception type to catch (default: ImportError)
|
|
1046
|
+
|
|
1047
|
+
Returns:
|
|
1048
|
+
The imported module
|
|
1049
|
+
|
|
1050
|
+
Example:
|
|
1051
|
+
numpy = pytest.importorskip("numpy")
|
|
1052
|
+
pandas = pytest.importorskip("pandas", minversion="1.0")
|
|
1053
|
+
"""
|
|
1054
|
+
import importlib
|
|
1055
|
+
|
|
1056
|
+
__tracebackhide__ = True
|
|
1057
|
+
|
|
1058
|
+
compile(modname, "", "eval") # Validate module name syntax
|
|
1059
|
+
|
|
1060
|
+
try:
|
|
1061
|
+
mod = importlib.import_module(modname)
|
|
1062
|
+
except exc_type as exc:
|
|
1063
|
+
if reason is None:
|
|
1064
|
+
reason = f"could not import {modname!r}: {exc}"
|
|
1065
|
+
_rustest_skip_function(reason=reason)
|
|
1066
|
+
raise # This line won't be reached due to skip, but satisfies type checker
|
|
1067
|
+
|
|
1068
|
+
if minversion is not None:
|
|
1069
|
+
mod_version = getattr(mod, "__version__", None)
|
|
1070
|
+
if mod_version is None:
|
|
1071
|
+
if reason is None:
|
|
1072
|
+
reason = f"module {modname!r} has no __version__ attribute"
|
|
1073
|
+
_rustest_skip_function(reason=reason)
|
|
1074
|
+
else:
|
|
1075
|
+
# Simple version comparison (works for most common cases)
|
|
1076
|
+
from packaging.version import Version
|
|
1077
|
+
|
|
1078
|
+
try:
|
|
1079
|
+
if Version(mod_version) < Version(minversion):
|
|
1080
|
+
if reason is None:
|
|
1081
|
+
reason = f"module {modname!r} has version {mod_version}, required is {minversion}"
|
|
1082
|
+
_rustest_skip_function(reason=reason)
|
|
1083
|
+
except Exception:
|
|
1084
|
+
# Fallback to string comparison if packaging fails
|
|
1085
|
+
if mod_version < minversion:
|
|
1086
|
+
if reason is None:
|
|
1087
|
+
reason = f"module {modname!r} has version {mod_version}, required is {minversion}"
|
|
1088
|
+
_rustest_skip_function(reason=reason)
|
|
1089
|
+
|
|
1090
|
+
return mod
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
# Module-level version to match pytest
|
|
1094
|
+
__version__ = "rustest-compat"
|
|
1095
|
+
|
|
1096
|
+
# Cache for dynamically generated stub classes
|
|
1097
|
+
_dynamic_stubs: dict[str, type] = {}
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def __getattr__(name: str) -> Any:
|
|
1101
|
+
"""
|
|
1102
|
+
Dynamically provide stub classes for any pytest attribute not explicitly defined.
|
|
1103
|
+
|
|
1104
|
+
This allows pytest plugins (like pytest_asyncio) to import any pytest internal
|
|
1105
|
+
without errors, while these remain non-functional stubs.
|
|
1106
|
+
|
|
1107
|
+
This is the recommended Python 3.7+ way to handle "catch-all" module imports.
|
|
1108
|
+
"""
|
|
1109
|
+
# Check if we've already created this stub
|
|
1110
|
+
if name in _dynamic_stubs:
|
|
1111
|
+
return _dynamic_stubs[name]
|
|
1112
|
+
|
|
1113
|
+
# Don't intercept private attributes or special methods
|
|
1114
|
+
if name.startswith("_"):
|
|
1115
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
1116
|
+
|
|
1117
|
+
# Create a stub class dynamically
|
|
1118
|
+
def stub_init(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
1119
|
+
pass
|
|
1120
|
+
|
|
1121
|
+
def stub_repr(self: Any) -> str:
|
|
1122
|
+
return f"<{name} (rustest compat stub)>"
|
|
1123
|
+
|
|
1124
|
+
stub_class = type(
|
|
1125
|
+
name,
|
|
1126
|
+
(),
|
|
1127
|
+
{
|
|
1128
|
+
"__doc__": (
|
|
1129
|
+
f"Dynamically generated stub for pytest.{name}.\n\n"
|
|
1130
|
+
f"NOT FUNCTIONAL in rustest pytest-compat mode. This stub exists\n"
|
|
1131
|
+
f"to allow pytest plugins to import without errors."
|
|
1132
|
+
),
|
|
1133
|
+
"__init__": stub_init,
|
|
1134
|
+
"__repr__": stub_repr,
|
|
1135
|
+
"__module__": __name__,
|
|
1136
|
+
},
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
# Cache it so subsequent imports get the same class
|
|
1140
|
+
_dynamic_stubs[name] = stub_class
|
|
1141
|
+
return stub_class
|