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.
@@ -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