rustest 0.5.0__cp312-cp312-win_amd64.whl → 0.7.0__cp312-cp312-win_amd64.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.

Potentially problematic release.


This version of rustest might be problematic. Click here for more details.

rustest/cli.py CHANGED
@@ -93,7 +93,35 @@ def build_parser() -> argparse.ArgumentParser:
93
93
  action="store_false",
94
94
  help="Disable code block tests from markdown files.",
95
95
  )
96
- _ = parser.set_defaults(capture_output=True, color=True, enable_codeblocks=True)
96
+ _ = parser.add_argument(
97
+ "--lf",
98
+ "--last-failed",
99
+ action="store_true",
100
+ dest="last_failed",
101
+ help="Rerun only the tests that failed in the last run.",
102
+ )
103
+ _ = parser.add_argument(
104
+ "--ff",
105
+ "--failed-first",
106
+ action="store_true",
107
+ dest="failed_first",
108
+ help="Run previously failed tests first, then all other tests.",
109
+ )
110
+ _ = parser.add_argument(
111
+ "-x",
112
+ "--exitfirst",
113
+ action="store_true",
114
+ dest="fail_fast",
115
+ help="Exit instantly on first error or failed test.",
116
+ )
117
+ _ = parser.set_defaults(
118
+ capture_output=True,
119
+ color=True,
120
+ enable_codeblocks=True,
121
+ last_failed=False,
122
+ failed_first=False,
123
+ fail_fast=False,
124
+ )
97
125
  return parser
98
126
 
99
127
 
@@ -105,6 +133,14 @@ def main(argv: Sequence[str] | None = None) -> int:
105
133
  if not args.color:
106
134
  Colors.disable()
107
135
 
136
+ # Determine last_failed_mode
137
+ if args.last_failed:
138
+ last_failed_mode = "only"
139
+ elif args.failed_first:
140
+ last_failed_mode = "first"
141
+ else:
142
+ last_failed_mode = "none"
143
+
108
144
  report = run(
109
145
  paths=list(args.paths),
110
146
  pattern=args.pattern,
@@ -112,6 +148,8 @@ def main(argv: Sequence[str] | None = None) -> int:
112
148
  workers=args.workers,
113
149
  capture_output=args.capture_output,
114
150
  enable_codeblocks=args.enable_codeblocks,
151
+ last_failed_mode=last_failed_mode,
152
+ fail_fast=args.fail_fast,
115
153
  )
116
154
  _print_report(report, verbose=args.verbose, ascii_mode=args.ascii)
117
155
  return 0 if report.failed == 0 else 1
rustest/core.py CHANGED
@@ -16,6 +16,8 @@ def run(
16
16
  workers: int | None = None,
17
17
  capture_output: bool = True,
18
18
  enable_codeblocks: bool = True,
19
+ last_failed_mode: str = "none",
20
+ fail_fast: bool = False,
19
21
  ) -> RunReport:
20
22
  """Execute tests and return a rich report.
21
23
 
@@ -26,6 +28,8 @@ def run(
26
28
  workers: Number of worker slots to use (experimental)
27
29
  capture_output: Whether to capture stdout/stderr during test execution
28
30
  enable_codeblocks: Whether to enable code block tests from markdown files
31
+ last_failed_mode: Last failed mode: "none", "only", or "first"
32
+ fail_fast: Exit instantly on first error or failed test
29
33
  """
30
34
  raw_report = rust.run(
31
35
  paths=list(paths),
@@ -34,5 +38,7 @@ def run(
34
38
  workers=workers,
35
39
  capture_output=capture_output,
36
40
  enable_codeblocks=enable_codeblocks,
41
+ last_failed_mode=last_failed_mode,
42
+ fail_fast=fail_fast,
37
43
  )
38
44
  return RunReport.from_py(raw_report)
rustest/decorators.py CHANGED
@@ -162,8 +162,122 @@ class MarkGenerator:
162
162
  @mark.skipif(condition, *, reason="...")
163
163
  @mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
164
164
  @mark.usefixtures("fixture1", "fixture2")
165
+ @mark.asyncio(loop_scope="function")
165
166
  """
166
167
 
168
+ def asyncio(
169
+ self,
170
+ func: Callable[..., Any] | None = None,
171
+ *,
172
+ loop_scope: str = "function",
173
+ ) -> Callable[..., Any]:
174
+ """Mark an async test function to be executed with asyncio.
175
+
176
+ This decorator allows you to write async test functions that will be
177
+ automatically executed in an asyncio event loop. The loop_scope parameter
178
+ controls the scope of the event loop used for execution.
179
+
180
+ Args:
181
+ func: The function to decorate (when used without parentheses)
182
+ loop_scope: The scope of the event loop. One of:
183
+ - "function": New loop for each test function (default)
184
+ - "class": Shared loop across all test methods in a class
185
+ - "module": Shared loop across all tests in a module
186
+ - "session": Shared loop across all tests in the session
187
+
188
+ Usage:
189
+ @mark.asyncio
190
+ async def test_async_function():
191
+ result = await some_async_operation()
192
+ assert result == expected
193
+
194
+ @mark.asyncio(loop_scope="module")
195
+ async def test_with_module_loop():
196
+ await another_async_operation()
197
+
198
+ Note:
199
+ This decorator should only be applied to async functions (coroutines).
200
+ Applying it to regular functions will raise a TypeError.
201
+ """
202
+ import asyncio
203
+ import inspect
204
+ from functools import wraps
205
+
206
+ valid_scopes = {"function", "class", "module", "session"}
207
+ if loop_scope not in valid_scopes:
208
+ valid = ", ".join(sorted(valid_scopes))
209
+ msg = f"Invalid loop_scope '{loop_scope}'. Must be one of: {valid}"
210
+ raise ValueError(msg)
211
+
212
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
213
+ # Handle class decoration - apply mark to all async methods
214
+ if inspect.isclass(f):
215
+ # Apply the mark to the class itself
216
+ mark_decorator = MarkDecorator("asyncio", (), {"loop_scope": loop_scope})
217
+ marked_class = mark_decorator(f)
218
+
219
+ # Wrap all async methods in the class
220
+ for name, method in inspect.getmembers(
221
+ marked_class, predicate=inspect.iscoroutinefunction
222
+ ):
223
+ wrapped_method = _wrap_async_function(method, loop_scope)
224
+ setattr(marked_class, name, wrapped_method)
225
+ return marked_class
226
+
227
+ # Validate that the function is a coroutine
228
+ if not inspect.iscoroutinefunction(f):
229
+ msg = f"@mark.asyncio can only be applied to async functions or test classes, but '{f.__name__}' is not async"
230
+ raise TypeError(msg)
231
+
232
+ # Store the asyncio mark
233
+ mark_decorator = MarkDecorator("asyncio", (), {"loop_scope": loop_scope})
234
+ marked_f = mark_decorator(f)
235
+
236
+ # Wrap the async function to run it synchronously
237
+ return _wrap_async_function(marked_f, loop_scope)
238
+
239
+ def _wrap_async_function(f: Callable[..., Any], loop_scope: str) -> Callable[..., Any]:
240
+ """Wrap an async function to run it synchronously in an event loop."""
241
+
242
+ @wraps(f)
243
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
244
+ # Get or create event loop based on scope
245
+ # For now, we'll always create a new loop - scope handling will be
246
+ # implemented in a future enhancement via fixtures
247
+ loop = asyncio.new_event_loop()
248
+ asyncio.set_event_loop(loop)
249
+ try:
250
+ # Run the coroutine in the event loop
251
+ # Get the original async function
252
+ original_func = getattr(f, "__wrapped__", f)
253
+ coro = original_func(*args, **kwargs)
254
+ return loop.run_until_complete(coro)
255
+ finally:
256
+ # Clean up the loop
257
+ try:
258
+ # Cancel any pending tasks
259
+ pending = asyncio.all_tasks(loop)
260
+ for task in pending:
261
+ task.cancel()
262
+ # Run the loop one more time to let tasks finish cancellation
263
+ if pending:
264
+ loop.run_until_complete(
265
+ asyncio.gather(*pending, return_exceptions=True)
266
+ )
267
+ except Exception:
268
+ pass
269
+ finally:
270
+ loop.close()
271
+
272
+ # Store reference to original async function
273
+ sync_wrapper.__wrapped__ = f
274
+ return sync_wrapper
275
+
276
+ # Support both @mark.asyncio and @mark.asyncio(loop_scope="...")
277
+ if func is not None:
278
+ return decorator(func)
279
+ return decorator
280
+
167
281
  def skipif(
168
282
  self,
169
283
  condition: bool | str,
Binary file
rustest/rust.pyi CHANGED
@@ -32,6 +32,8 @@ def run(
32
32
  workers: int | None,
33
33
  capture_output: bool,
34
34
  enable_codeblocks: bool,
35
+ last_failed_mode: str,
36
+ fail_fast: bool,
35
37
  ) -> PyRunReport:
36
38
  """Execute tests and return a report."""
37
39
  ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rustest
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -18,6 +18,8 @@ Requires-Dist: maturin>=1.4,<2 ; extra == 'dev'
18
18
  Requires-Dist: poethepoet>=0.22 ; extra == 'dev'
19
19
  Requires-Dist: pre-commit>=3.5 ; extra == 'dev'
20
20
  Requires-Dist: pytest>=7.0 ; extra == 'dev'
21
+ Requires-Dist: pytest-asyncio>=1.2.0 ; extra == 'dev'
22
+ Requires-Dist: pytest-codeblocks>=0.17.0 ; extra == 'dev'
21
23
  Requires-Dist: ruff>=0.1.9 ; extra == 'dev'
22
24
  Requires-Dist: mkdocs>=1.5.0 ; extra == 'docs'
23
25
  Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'docs'
@@ -44,6 +46,7 @@ Rustest (pronounced like Russ-Test) is a Rust-powered test runner that aims to p
44
46
 
45
47
  - 🚀 **About 2x faster** than pytest on the rustest integration test suite
46
48
  - ✅ Familiar `@fixture`, `@parametrize`, `@skip`, and `@mark` decorators
49
+ - 🔄 **Built-in async support** with `@mark.asyncio` (like pytest-asyncio)
47
50
  - 🔍 Automatic test discovery (`test_*.py` and `*_test.py` files)
48
51
  - 📝 **Built-in markdown code block testing** (like pytest-codeblocks, but faster)
49
52
  - 🎯 Simple, clean API—if you know pytest, you already know rustest
@@ -76,6 +79,7 @@ With **10,000 parametrized invocations**:
76
79
 
77
80
  Rustest supports Python **3.10 through 3.14**.
78
81
 
82
+ <!--pytest.mark.skip-->
79
83
  ```bash
80
84
  # Using pip
81
85
  pip install rustest
@@ -94,6 +98,7 @@ Create a file `test_math.py`:
94
98
 
95
99
  ```python
96
100
  from rustest import fixture, parametrize, mark, approx, raises
101
+ import asyncio
97
102
 
98
103
  @fixture
99
104
  def numbers() -> list[int]:
@@ -111,6 +116,13 @@ def test_expensive_operation() -> None:
111
116
  result = sum(range(1000000))
112
117
  assert result > 0
113
118
 
119
+ @mark.asyncio
120
+ async def test_async_operation() -> None:
121
+ # Example async operation
122
+ await asyncio.sleep(0.001)
123
+ result = 42
124
+ assert result == 42
125
+
114
126
  def test_division_by_zero() -> None:
115
127
  with raises(ZeroDivisionError, match="division by zero"):
116
128
  1 / 0
@@ -118,6 +130,7 @@ def test_division_by_zero() -> None:
118
130
 
119
131
  ### 2. Run Your Tests
120
132
 
133
+ <!--pytest.mark.skip-->
121
134
  ```bash
122
135
  # Run all tests
123
136
  rustest
@@ -133,6 +146,16 @@ rustest -m "slow" # Run only slow tests
133
146
  rustest -m "not slow" # Skip slow tests
134
147
  rustest -m "slow and integration" # Run tests with both marks
135
148
 
149
+ # Rerun only failed tests
150
+ rustest --lf # Last failed only
151
+ rustest --ff # Failed first, then all others
152
+
153
+ # Exit on first failure
154
+ rustest -x # Fail fast
155
+
156
+ # Combine options
157
+ rustest --ff -x # Run failed tests first, stop on first failure
158
+
136
159
  # Show output during execution
137
160
  rustest --no-capture
138
161
  ```
@@ -186,6 +209,7 @@ We welcome contributions! See the [Development Guide](https://apex-engineers-inc
186
209
 
187
210
  Quick reference:
188
211
 
212
+ <!--pytest.mark.skip-->
189
213
  ```bash
190
214
  # Setup
191
215
  git clone https://github.com/Apex-Engineers-Inc/rustest.git
@@ -0,0 +1,16 @@
1
+ rustest-0.7.0.dist-info/METADATA,sha256=t6zLLc3Y-0qY2KKh3xCdVK50GM1B1ZNugDVGt4AChzI,9011
2
+ rustest-0.7.0.dist-info/WHEEL,sha256=M1DN_cdEL9MiMVOnA_mgpqWn-huMwVTFfEx_RmZww1E,97
3
+ rustest-0.7.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
4
+ rustest-0.7.0.dist-info/licenses/LICENSE,sha256=Ci0bB0T1ZGkqIV237Zp_Bv8LIJJ0Vxwc-AhLhgDgAoQ,1096
5
+ rustest/__init__.py,sha256=LL9UloOClzeNO6A-iMkEFtHDrBTAhRLko3sXy55H0QA,542
6
+ rustest/__main__.py,sha256=yMhaWvxGAV46BYY8fB6GoRy9oh8Z8YrS9wlZI3LmoyY,188
7
+ rustest/approx.py,sha256=sGaH15n3vSSv84dmR_QAIDV-xwaUrX-MqwpWIp5ihjk,5904
8
+ rustest/cli.py,sha256=6etivUqjdnHgYShXobtrEFFhjW2PuetWuj8hwM4gRuI,10496
9
+ rustest/core.py,sha256=ORTAlkeuu6nC2Q43hg-VJY6PN1HUiaMoP1vu_WH-8cs,1505
10
+ rustest/decorators.py,sha256=mUrnY09mn7x0--3OAB47WQRR2f8NtexVSUWAUGcHvyc,19240
11
+ rustest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ rustest/reporting.py,sha256=g4jdlwjFoKF_fWA_sKfeDwmhDeHxPJQUqypa_E2XQlc,1686
13
+ rustest/rust.cp312-win_amd64.pyd,sha256=PUQBX9EY49xlHJ6zyxKz6yDayGOLDA5Gc8OBdeA2xow,1411584
14
+ rustest/rust.py,sha256=N_1C-uXRiC2qkV7ecKVcb51-XXyfhYNepd5zs-RIYOo,682
15
+ rustest/rust.pyi,sha256=ltvSC9_ZUpuRgq9cShN93YLxaxt9c1B2AqBswpk1nfY,849
16
+ rustest-0.7.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: maturin (1.9.6)
2
+ Generator: maturin (1.10.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-win_amd64
@@ -1,16 +0,0 @@
1
- rustest-0.5.0.dist-info/METADATA,sha256=SkRHcV4Pv7s5Hfo9ywd77-b3wuJtgbPvtrYY6LQoaYk,8225
2
- rustest-0.5.0.dist-info/WHEEL,sha256=n8ZdhGDUio-M1d1jdBhS3kD6MKEN1SRxo8BOjWwZdsg,96
3
- rustest-0.5.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
4
- rustest-0.5.0.dist-info/licenses/LICENSE,sha256=Ci0bB0T1ZGkqIV237Zp_Bv8LIJJ0Vxwc-AhLhgDgAoQ,1096
5
- rustest/__init__.py,sha256=LL9UloOClzeNO6A-iMkEFtHDrBTAhRLko3sXy55H0QA,542
6
- rustest/__main__.py,sha256=yMhaWvxGAV46BYY8fB6GoRy9oh8Z8YrS9wlZI3LmoyY,188
7
- rustest/approx.py,sha256=sGaH15n3vSSv84dmR_QAIDV-xwaUrX-MqwpWIp5ihjk,5904
8
- rustest/cli.py,sha256=Y7I3sLMA7icsHFLdcGJMy0LElEcoqzrVrTkedE_vGTs,9474
9
- rustest/core.py,sha256=2_kav-3XPhrtXnayQBDSc_tCaaaEj07rFHq5ln4Em8Q,1227
10
- rustest/decorators.py,sha256=mCiqNJXYpbQMCMaVEdeiebN9KWLvgtRmqZ6nbUXpm6E,14138
11
- rustest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- rustest/reporting.py,sha256=g4jdlwjFoKF_fWA_sKfeDwmhDeHxPJQUqypa_E2XQlc,1686
13
- rustest/rust.cp312-win_amd64.pyd,sha256=RFiwEJBXWgLvajST9qNaP7RV5QbL7L6BWIMPAPgOAYQ,1341440
14
- rustest/rust.py,sha256=N_1C-uXRiC2qkV7ecKVcb51-XXyfhYNepd5zs-RIYOo,682
15
- rustest/rust.pyi,sha256=D2PL2GamLpcwdBDZxFeWC1ZgJ01CkvWycU62LnVTMD0,799
16
- rustest-0.5.0.dist-info/RECORD,,