diff --git a/newsfragments/3326.deprecated.rst b/newsfragments/3326.deprecated.rst new file mode 100644 index 000000000..74916bf4e --- /dev/null +++ b/newsfragments/3326.deprecated.rst @@ -0,0 +1 @@ +Both :class:`trio.testing.RaisesGroup` and :class:`trio.testing.Matcher` have been deprecated. Pytest alternatives ``pytest.RaisesGroup`` and ``pytest.RaisesExc`` (respectively) are considered correct replacement. diff --git a/src/trio/_core/_tests/test_cancelled.py b/src/trio/_core/_tests/test_cancelled.py index 0c144c37f..0280b6068 100644 --- a/src/trio/_core/_tests/test_cancelled.py +++ b/src/trio/_core/_tests/test_cancelled.py @@ -7,7 +7,7 @@ import trio from trio import Cancelled from trio.lowlevel import current_task -from trio.testing import RaisesGroup, wait_all_tasks_blocked +from trio.testing import wait_all_tasks_blocked from .test_ki import ki_self @@ -108,7 +108,7 @@ async def failing_task(task_status: trio.TaskStatus[trio.lowlevel.Task]) -> None task_status.started(current_task()) raise ValueError - with RaisesGroup(ValueError, TypeError): + with pytest.RaisesGroup(ValueError, TypeError): async with trio.open_nursery() as nursery: fail_task = await nursery.start(failing_task) with pytest.raises(Cancelled, match=match_str.format(fail_task)): @@ -123,7 +123,7 @@ async def failing_task(task_status: trio.TaskStatus[trio.lowlevel.Task]) -> None await wait_all_tasks_blocked() raise ValueError - with RaisesGroup(ValueError, TypeError): + with pytest.RaisesGroup(ValueError, TypeError): async with trio.open_nursery() as nursery: fail_task = await nursery.start(failing_task) await nursery.start(cancelled_task, fail_task) @@ -147,7 +147,7 @@ async def cancelled_task() -> None: ): await trio.sleep_forever() - with RaisesGroup(ValueError): + with pytest.RaisesGroup(ValueError): async with trio.open_nursery() as nursery: nursery.start_soon(cancelled_task) await wait_all_tasks_blocked() @@ -192,7 +192,7 @@ async def child() -> None: ): await trio.sleep_forever() - with RaisesGroup(ValueError): + with pytest.RaisesGroup(ValueError): async with trio.open_nursery() as nursery: nursery.start_soon(child) await ev.wait() @@ -214,7 +214,7 @@ async def sleeper(name: str) -> None: async def raiser(name: str) -> None: ki_self() - with RaisesGroup(KeyboardInterrupt): + with pytest.RaisesGroup(KeyboardInterrupt): async with trio.open_nursery() as nursery: nursery.start_soon(sleeper, "s1") nursery.start_soon(sleeper, "s2") diff --git a/src/trio/_core/_tests/test_ki.py b/src/trio/_core/_tests/test_ki.py index cf37aae6d..ea45edaef 100644 --- a/src/trio/_core/_tests/test_ki.py +++ b/src/trio/_core/_tests/test_ki.py @@ -12,8 +12,6 @@ import outcome import pytest -from trio.testing import RaisesGroup - from .tutil import gc_collect_harder try: @@ -309,7 +307,7 @@ async def check_unprotected_kill() -> None: nursery.start_soon(raiser, "r1", record_set) # raises inside a nursery, so the KeyboardInterrupt is wrapped in an ExceptionGroup - with RaisesGroup(KeyboardInterrupt): + with pytest.RaisesGroup(KeyboardInterrupt): _core.run(check_unprotected_kill) assert record_set == {"s1 ok", "s2 ok", "r1 raise ok"} @@ -326,7 +324,7 @@ async def check_protected_kill() -> None: # __aexit__ blocks, and then receives the KI # raises inside a nursery, so the KeyboardInterrupt is wrapped in an ExceptionGroup - with RaisesGroup(KeyboardInterrupt): + with pytest.RaisesGroup(KeyboardInterrupt): _core.run(check_protected_kill) assert record_set == {"s1 ok", "s2 ok", "r1 cancel ok"} diff --git a/src/trio/_core/_tests/test_parking_lot.py b/src/trio/_core/_tests/test_parking_lot.py index 3348e47cb..55c5144f9 100644 --- a/src/trio/_core/_tests/test_parking_lot.py +++ b/src/trio/_core/_tests/test_parking_lot.py @@ -11,7 +11,6 @@ current_task, remove_parking_lot_breaker, ) -from trio.testing import Matcher, RaisesGroup from ... import _core from ...testing import wait_all_tasks_blocked @@ -267,8 +266,8 @@ async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None: cs = _core.CancelScope() # check that parked task errors - with RaisesGroup( - Matcher(_core.BrokenResourceError, match="^Parking lot broken by"), + with pytest.RaisesGroup( + pytest.RaisesExc(_core.BrokenResourceError, match="^Parking lot broken by"), ): async with _core.open_nursery() as nursery: nursery.start_soon(bad_parker, lot, cs) @@ -382,8 +381,8 @@ async def return_me_and_park( await lot.park() lot = ParkingLot() - with RaisesGroup( - Matcher(_core.BrokenResourceError, match="^Parking lot broken by"), + with pytest.RaisesGroup( + pytest.RaisesExc(_core.BrokenResourceError, match="^Parking lot broken by"), ): async with _core.open_nursery() as nursery: child_task = await nursery.start(return_me_and_park, lot) diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index 45ea21cdd..94e448448 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -27,8 +27,6 @@ from ..._threads import to_thread_run_sync from ..._timeouts import fail_after, sleep from ...testing import ( - Matcher, - RaisesGroup, Sequencer, assert_checkpoints, wait_all_tasks_blocked, @@ -134,7 +132,7 @@ async def test_nursery_warn_use_async_with() -> None: async def test_nursery_main_block_error_basic() -> None: exc = ValueError("whoops") - with RaisesGroup(Matcher(check=lambda e: e is exc)): + with pytest.RaisesGroup(pytest.RaisesExc(check=lambda e: e is exc)): async with _core.open_nursery(): raise exc @@ -145,7 +143,7 @@ async def test_child_crash_basic() -> None: async def erroring() -> NoReturn: raise my_exc - with RaisesGroup(Matcher(check=lambda e: e is my_exc)): + with pytest.RaisesGroup(pytest.RaisesExc(check=lambda e: e is my_exc)): # nursery.__aexit__ propagates exception from child back to parent async with _core.open_nursery() as nursery: nursery.start_soon(erroring) @@ -187,7 +185,7 @@ async def main() -> None: nursery.start_soon(looper) nursery.start_soon(crasher) - with RaisesGroup(Matcher(ValueError, "^argh$")): + with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="^argh$")): _core.run(main) assert looper_record == ["cancelled"] @@ -204,7 +202,7 @@ async def main() -> NoReturn: nursery.start_soon(crasher) raise KeyError - with RaisesGroup(ValueError, KeyError): + with pytest.RaisesGroup(ValueError, KeyError): _core.run(main) @@ -217,7 +215,7 @@ async def main() -> None: nursery.start_soon(crasher, KeyError) nursery.start_soon(crasher, ValueError) - with RaisesGroup(ValueError, KeyError): + with pytest.RaisesGroup(ValueError, KeyError): _core.run(main) @@ -225,7 +223,7 @@ async def test_child_crash_wakes_parent() -> None: async def crasher() -> NoReturn: raise ValueError("this is a crash") - with RaisesGroup(Matcher(ValueError, "^this is a crash$")): + with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="^this is a crash$")): async with _core.open_nursery() as nursery: nursery.start_soon(crasher) await sleep_forever() @@ -465,13 +463,13 @@ async def crasher() -> NoReturn: # This is outside the outer scope, so all the Cancelled # exceptions should have been absorbed, leaving just a regular # KeyError from crasher(), wrapped in an ExceptionGroup - with RaisesGroup(KeyError): + with pytest.RaisesGroup(KeyError): with _core.CancelScope() as outer: # Since the outer scope became cancelled before the # nursery block exited, all cancellations inside the # nursery block continue propagating to reach the # outer scope. - with RaisesGroup( + with pytest.RaisesGroup( _core.Cancelled, _core.Cancelled, _core.Cancelled, @@ -490,7 +488,7 @@ async def crasher() -> NoReturn: # And one that raises a different error nursery.start_soon(crasher) # t4 # and then our __aexit__ also receives an outer Cancelled - # reraise the exception caught by RaisesGroup for the + # reraise the exception caught by pytest.RaisesGroup for the # CancelScope to handle raise excinfo.value @@ -824,11 +822,11 @@ def no_context(exc: RuntimeError) -> bool: return exc.__context__ is None msg = "closed before the task exited" - group = RaisesGroup( - Matcher(RuntimeError, match=msg, check=no_context), - Matcher(RuntimeError, match=msg, check=no_context), + group = pytest.RaisesGroup( + pytest.RaisesExc(RuntimeError, match=msg, check=no_context), + pytest.RaisesExc(RuntimeError, match=msg, check=no_context), # sleep_forever - Matcher( + pytest.RaisesExc( RuntimeError, match=msg, check=lambda x: isinstance(x.__context__, _core.Cancelled), @@ -1126,7 +1124,9 @@ async def main() -> None: # the first exceptiongroup is from the first nursery opened in Runner.init() # the second exceptiongroup is from the second nursery opened in Runner.init() # the third exceptongroup is from the nursery defined in `system_task` above - assert RaisesGroup(RaisesGroup(RaisesGroup(KeyError, ValueError))).matches( + assert pytest.RaisesGroup( + pytest.RaisesGroup(pytest.RaisesGroup(KeyError, ValueError)) + ).matches( excinfo.value.__cause__, ) @@ -1156,7 +1156,9 @@ async def main() -> None: _core.run(main) # See explanation for triple-wrap in test_system_task_crash_ExceptionGroup - assert RaisesGroup(RaisesGroup(RaisesGroup(ValueError))).matches( + assert pytest.RaisesGroup( + pytest.RaisesGroup(pytest.RaisesGroup(ValueError)) + ).matches( excinfo.value.__cause__, ) @@ -1172,7 +1174,9 @@ async def main() -> None: with pytest.raises(_core.TrioInternalError) as excinfo: _core.run(main) # "Only" double-wrapped since ki() doesn't create an exceptiongroup - assert RaisesGroup(RaisesGroup(KeyboardInterrupt)).matches(excinfo.value.__cause__) + assert pytest.RaisesGroup(pytest.RaisesGroup(KeyboardInterrupt)).matches( + excinfo.value.__cause__ + ) # This used to fail because checkpoint was a yield followed by an immediate @@ -1280,7 +1284,9 @@ async def child() -> None: await sleep_forever() raise - with RaisesGroup(Matcher(KeyError, check=lambda e: e.__context__ is None)): + with pytest.RaisesGroup( + pytest.RaisesExc(KeyError, check=lambda e: e.__context__ is None) + ): async with _core.open_nursery() as nursery: nursery.start_soon(child) await wait_all_tasks_blocked() @@ -1306,11 +1312,11 @@ async def child() -> None: except KeyError: await sleep_forever() - with RaisesGroup( - Matcher( + with pytest.RaisesGroup( + pytest.RaisesExc( ValueError, - "error text", - lambda e: isinstance(e.__context__, KeyError), + match="error text", + check=lambda e: isinstance(e.__context__, KeyError), ), ): async with _core.open_nursery() as nursery: @@ -1345,7 +1351,9 @@ async def inner(exc: BaseException) -> None: await sleep_forever() assert not_none(sys.exc_info()[1]) is exc - with RaisesGroup(Matcher(KeyError, check=lambda e: e.__context__ is None)): + with pytest.RaisesGroup( + pytest.RaisesExc(KeyError, check=lambda e: e.__context__ is None) + ): async with _core.open_nursery() as nursery: nursery.start_soon(child) await wait_all_tasks_blocked() @@ -1375,11 +1383,11 @@ async def inner() -> None: except IndexError: await sleep_forever() - with RaisesGroup( - Matcher( + with pytest.RaisesGroup( + pytest.RaisesExc( ValueError, - "^Unique Text$", - lambda e: isinstance(e.__context__, IndexError) + match="^Unique Text$", + check=lambda e: isinstance(e.__context__, IndexError) and isinstance(e.__context__.__context__, KeyError), ), ): @@ -1397,7 +1405,9 @@ async def crasher() -> NoReturn: raise KeyError # the ExceptionGroup should not have the KeyError or ValueError as context - with RaisesGroup(ValueError, KeyError, check=lambda x: x.__context__ is None): + with pytest.RaisesGroup( + ValueError, KeyError, check=lambda x: x.__context__ is None + ): async with _core.open_nursery() as nursery: nursery.start_soon(crasher) raise ValueError @@ -1527,7 +1537,9 @@ async def main() -> None: _core.run(main) # the first exceptiongroup is from the first nursery opened in Runner.init() # the second exceptiongroup is from the second nursery opened in Runner.init() - assert RaisesGroup(RaisesGroup(KeyError)).matches(excinfo.value.__cause__) + assert pytest.RaisesGroup(pytest.RaisesGroup(KeyError)).matches( + excinfo.value.__cause__ + ) assert record == {"2nd run_sync_soon ran", "cancelled!"} @@ -1641,7 +1653,7 @@ async def main() -> None: with pytest.raises(_core.TrioInternalError) as excinfo: _core.run(main) - assert RaisesGroup(KeyError).matches(excinfo.value.__cause__) + assert pytest.RaisesGroup(KeyError).matches(excinfo.value.__cause__) assert record == ["main exiting", "2nd ran"] @@ -1876,11 +1888,15 @@ async def async_gen(arg: T) -> AsyncGenerator[T, None]: # pragma: no cover # bad_call_spawn calls the function inside a nursery, so the exception will be # wrapped in an exceptiongroup - with RaisesGroup(Matcher(TypeError, "expecting an async function")): + with pytest.RaisesGroup( + pytest.RaisesExc(TypeError, match="expecting an async function") + ): bad_call_spawn(f()) # type: ignore[arg-type] - with RaisesGroup( - Matcher(TypeError, "expected an async function but got an async generator"), + with pytest.RaisesGroup( + pytest.RaisesExc( + TypeError, match="expected an async function but got an async generator" + ), ): bad_call_spawn(async_gen, 0) # type: ignore @@ -1903,7 +1919,7 @@ async def misguided() -> None: async def test_asyncio_function_inside_nursery_does_not_explode() -> None: # Regression test for https://github.com/python-trio/trio/issues/552 - with RaisesGroup(Matcher(TypeError, "asyncio")): + with pytest.RaisesGroup(pytest.RaisesExc(TypeError, match="asyncio")): async with _core.open_nursery() as nursery: nursery.start_soon(sleep_forever) await create_asyncio_future_in_new_loop() @@ -1943,7 +1959,7 @@ async def noop_with_no_checkpoint() -> None: with _core.CancelScope() as cancel_scope: cancel_scope.cancel() - with RaisesGroup(KeyError): + with pytest.RaisesGroup(KeyError): async with _core.open_nursery(): raise KeyError @@ -2081,7 +2097,7 @@ async def test_task_nursery_stack() -> None: assert task._child_nurseries == [] async with _core.open_nursery() as nursery1: assert task._child_nurseries == [nursery1] - with RaisesGroup(KeyError): + with pytest.RaisesGroup(KeyError): async with _core.open_nursery() as nursery2: assert task._child_nurseries == [nursery1, nursery2] raise KeyError @@ -2179,7 +2195,7 @@ async def start_sleep_then_crash(nursery: _core.Nursery) -> None: async def test_nursery_explicit_exception() -> None: - with RaisesGroup(KeyError): + with pytest.RaisesGroup(KeyError): async with _core.open_nursery(): raise KeyError() @@ -2188,7 +2204,7 @@ async def test_nursery_stop_iteration() -> None: async def fail() -> NoReturn: raise ValueError - with RaisesGroup(StopIteration, ValueError): + with pytest.RaisesGroup(StopIteration, ValueError): async with _core.open_nursery() as nursery: nursery.start_soon(fail) raise StopIteration @@ -2235,7 +2251,7 @@ async def __anext__(self) -> list[int]: # With strict_exception_groups enabled, users now need to unwrap # StopAsyncIteration and re-raise it. # This would be relatively clean on python3.11+ with except*. - # We could also use RaisesGroup, but that's primarily meant as + # We could also use pytest.RaisesGroup, but that's primarily meant as # test infra, not as a runtime tool. if len(e.exceptions) == 1 and isinstance( e.exceptions[0], @@ -2264,7 +2280,7 @@ def check_traceback(exc: KeyError) -> bool: assert tb is not None return tb.tb_frame.f_code is my_child_task.__code__ - with RaisesGroup(Matcher(KeyError, check=check_traceback)): + with pytest.RaisesGroup(pytest.RaisesExc(KeyError, check=check_traceback)): # For now cancel/nursery scopes still leave a bunch of tb gunk behind. # But if there's an Exceptiongroup, they leave it on the group, # which lets us get a clean look at the KeyError itself. @@ -2443,7 +2459,7 @@ async def detachable_coroutine( # Check the exception paths too task = None pdco_outcome = None - with RaisesGroup(KeyError): + with pytest.RaisesGroup(KeyError): async with _core.open_nursery() as nursery: nursery.start_soon(detachable_coroutine, outcome.Error(KeyError()), "uh oh") throw_in = ValueError() @@ -2619,7 +2635,9 @@ async def crasher() -> NoReturn: # (See https://github.com/python-trio/trio/pull/1864) await do_a_cancel() - with RaisesGroup(Matcher(ValueError, "^this is a crash$")): + with pytest.RaisesGroup( + pytest.RaisesExc(ValueError, match="^this is a crash$") + ): async with _core.open_nursery() as nursery: # cover NurseryManager.__aexit__ nursery.start_soon(crasher) @@ -2645,8 +2663,8 @@ async def crasher() -> NoReturn: old_flags = gc.get_debug() try: with ( - RaisesGroup( - Matcher(ValueError, "^this is a crash$"), + pytest.RaisesGroup( + pytest.RaisesExc(ValueError, match="^this is a crash$"), ), _core.CancelScope() as outer, ): @@ -2769,15 +2787,15 @@ def run_main() -> None: # mypy doesn't like kwarg magic _core.run(main, **_create_kwargs(run_strict)) # type: ignore[arg-type] - matcher = Matcher(RuntimeError, r"^test error$") + matcher = pytest.RaisesExc(RuntimeError, match=r"^test error$") if multiple_exceptions: - with RaisesGroup(matcher, matcher): + with pytest.RaisesGroup(matcher, matcher): run_main() elif open_nursery_strict or ( open_nursery_strict is None and run_strict is not False ): - with RaisesGroup(matcher): + with pytest.RaisesGroup(matcher): run_main() else: with pytest.raises(RuntimeError, match=r"^test error$"): @@ -2798,11 +2816,11 @@ async def raise_error() -> NoReturn: raise RuntimeError("test error") # mypy requires explicit type for conditional expression - maybe_wrapped_runtime_error: type[RuntimeError] | RaisesGroup[RuntimeError] = ( - RuntimeError if strict is False else RaisesGroup(RuntimeError) - ) + maybe_wrapped_runtime_error: ( + type[RuntimeError] | pytest.RaisesGroup[RuntimeError] + ) = (RuntimeError if strict is False else pytest.RaisesGroup(RuntimeError)) - with RaisesGroup(RuntimeError, maybe_wrapped_runtime_error): + with pytest.RaisesGroup(RuntimeError, maybe_wrapped_runtime_error): async with _core.open_nursery() as nursery: nursery.start_soon(sleep_forever) nursery.start_soon(raise_error) @@ -2818,7 +2836,7 @@ async def test_cancel_scope_no_cancellederror() -> None: a Cancelled exception, it will NOT set the ``cancelled_caught`` flag. """ - with RaisesGroup(RuntimeError, RuntimeError, match="test"): + with pytest.RaisesGroup(RuntimeError, RuntimeError, match="test"): with _core.CancelScope() as scope: scope.cancel() raise ExceptionGroup("test", [RuntimeError(), RuntimeError()]) @@ -2915,7 +2933,7 @@ async def spawn_tasks_in_old_nursery(task_status: _core.TaskStatus[None]) -> Non async with _core.open_nursery() as nursery: with pytest.raises(_core.TrioInternalError) as excinfo: await nursery.start(spawn_tasks_in_old_nursery) - assert RaisesGroup(ValueError, ValueError).matches(excinfo.value.__cause__) + assert pytest.RaisesGroup(ValueError, ValueError).matches(excinfo.value.__cause__) if sys.version_info >= (3, 11): diff --git a/src/trio/_deprecate.py b/src/trio/_deprecate.py index 5c827b4fd..2f1f9f43a 100644 --- a/src/trio/_deprecate.py +++ b/src/trio/_deprecate.py @@ -7,6 +7,8 @@ import attrs +from ._util import final + if TYPE_CHECKING: from collections.abc import Callable @@ -139,6 +141,7 @@ def wrapper(*args: ArgsT.args, **kwargs: ArgsT.kwargs) -> RetT: return wrapper +@final @attrs.frozen(slots=False) class DeprecatedAttribute: _not_set: ClassVar[object] = object() diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json index 72d981f89..6e8c54ea8 100644 --- a/src/trio/_tests/_check_type_completeness.json +++ b/src/trio/_tests/_check_type_completeness.json @@ -45,10 +45,6 @@ "No docstring found for class \"trio.socket.herror\"", "No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"", "No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"", - "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"", - "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"", - "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\"" + "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"" ] } diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py index 986207b30..c8b2efddc 100644 --- a/src/trio/_tests/test_channel.py +++ b/src/trio/_tests/test_channel.py @@ -8,7 +8,7 @@ import trio from trio import EndOfChannel, as_safe_channel, open_memory_channel -from ..testing import Matcher, RaisesGroup, assert_checkpoints, wait_all_tasks_blocked +from ..testing import assert_checkpoints, wait_all_tasks_blocked if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup @@ -518,9 +518,9 @@ async def agen(events: list[str]) -> AsyncGenerator[int]: raise ValueError("agen") events: list[str] = [] - with RaisesGroup( - Matcher(ValueError, match="^agen$"), - Matcher(TypeError, match="^iterator$"), + with pytest.RaisesGroup( + pytest.RaisesExc(ValueError, match="^agen$"), + pytest.RaisesExc(TypeError, match="^iterator$"), ) as g: async with agen(events) as recv_chan: async for i in recv_chan: # pragma: no branch @@ -574,7 +574,7 @@ async def agen() -> AsyncGenerator[None]: raise NotImplementedError("not entered") yield # pragma: no cover - with RaisesGroup(Matcher(ValueError, match="bar"), match="foo"): + with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="bar"), match="foo"): async with agen() as _: raise ExceptionGroup("foo", [ValueError("bar")]) diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index abc50210d..0414d4342 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -333,7 +333,7 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit # __init__ is called (and reason they don't use attrs is because they're going # to be reimplemented in pytest). # Not 100% that's the case, and it works locally, so whatever /shrug - if class_ is trio.testing.RaisesGroup or class_ is trio.testing.Matcher: + if module_name == "trio.testing" and class_name in ("_RaisesGroup", "_Matcher"): continue # dir() and inspect.getmembers doesn't display properties from the metaclass diff --git a/src/trio/_tests/test_highlevel_open_tcp_stream.py b/src/trio/_tests/test_highlevel_open_tcp_stream.py index eaec52ad4..193bf77e7 100644 --- a/src/trio/_tests/test_highlevel_open_tcp_stream.py +++ b/src/trio/_tests/test_highlevel_open_tcp_stream.py @@ -16,7 +16,6 @@ reorder_for_rfc_6555_section_5_4, ) from trio.socket import AF_INET, AF_INET6, IPPROTO_TCP, SOCK_STREAM, SocketType -from trio.testing import Matcher, RaisesGroup if TYPE_CHECKING: from collections.abc import Sequence @@ -550,8 +549,8 @@ async def test_all_fail(autojump_clock: MockClock) -> None: ) assert isinstance(exc, OSError) - subexceptions = (Matcher(OSError, match="^sorry$"),) * 4 - assert RaisesGroup( + subexceptions = (pytest.RaisesExc(OSError, match="^sorry$"),) * 4 + assert pytest.RaisesGroup( *subexceptions, match="all attempts to connect to test.example.com:80 failed", ).matches(exc.__cause__) diff --git a/src/trio/_tests/test_highlevel_serve_listeners.py b/src/trio/_tests/test_highlevel_serve_listeners.py index 9268555b3..1a6ac9469 100644 --- a/src/trio/_tests/test_highlevel_serve_listeners.py +++ b/src/trio/_tests/test_highlevel_serve_listeners.py @@ -5,15 +5,14 @@ from typing import TYPE_CHECKING, NoReturn, cast import attrs +import pytest import trio from trio import Nursery, StapledStream, TaskStatus from trio.testing import ( - Matcher, MemoryReceiveStream, MemorySendStream, MockClock, - RaisesGroup, memory_stream_pair, wait_all_tasks_blocked, ) @@ -21,8 +20,6 @@ if TYPE_CHECKING: from collections.abc import Awaitable, Callable - import pytest - from trio._channel import MemoryReceiveChannel, MemorySendChannel from trio.abc import Stream @@ -124,7 +121,7 @@ def check_error(e: BaseException) -> bool: listener.accept_hook = raise_error - with RaisesGroup(Matcher(check=check_error)): + with pytest.RaisesGroup(pytest.RaisesExc(check=check_error)): await trio.serve_listeners(None, [listener]) # type: ignore[arg-type] @@ -172,7 +169,7 @@ async def connection_watcher( raise Done # the exception is wrapped twice because we open two nested nurseries - with RaisesGroup(RaisesGroup(Done)): + with pytest.RaisesGroup(pytest.RaisesGroup(Done)): async with trio.open_nursery() as nursery: value = await nursery.start(connection_watcher) assert isinstance(value, trio.Nursery) diff --git a/src/trio/_tests/test_signals.py b/src/trio/_tests/test_signals.py index 9c8674174..501ae1e80 100644 --- a/src/trio/_tests/test_signals.py +++ b/src/trio/_tests/test_signals.py @@ -6,7 +6,6 @@ import pytest import trio -from trio.testing import RaisesGroup from .. import _core from .._signals import _signal_handler, get_pending_signal_count, open_signal_receiver @@ -75,7 +74,7 @@ async def naughty() -> None: async def test_open_signal_receiver_conflict() -> None: - with RaisesGroup(trio.BusyResourceError): + with pytest.RaisesGroup(trio.BusyResourceError): with open_signal_receiver(signal.SIGILL) as receiver: async with trio.open_nursery() as nursery: nursery.start_soon(receiver.__anext__) diff --git a/src/trio/_tests/test_ssl.py b/src/trio/_tests/test_ssl.py index b365f7dd3..085ce8f1d 100644 --- a/src/trio/_tests/test_ssl.py +++ b/src/trio/_tests/test_ssl.py @@ -15,12 +15,7 @@ from trio import StapledStream from trio._tests.pytest_plugin import skip_if_optional_else_raise from trio.abc import ReceiveStream, SendStream -from trio.testing import ( - Matcher, - MemoryReceiveStream, - MemorySendStream, - RaisesGroup, -) +from trio.testing import MemoryReceiveStream, MemorySendStream try: import trustme @@ -350,7 +345,9 @@ async def do_test( args2: tuple[object, ...], ) -> None: s = PyOpenSSLEchoStream() - with RaisesGroup(Matcher(_core.BusyResourceError, "simultaneous")): + with pytest.RaisesGroup( + pytest.RaisesExc(_core.BusyResourceError, match="simultaneous") + ): async with _core.open_nursery() as nursery: nursery.start_soon(getattr(s, func1), *args1) nursery.start_soon(getattr(s, func2), *args2) @@ -774,7 +771,9 @@ async def do_test( func2: Callable[[S], Awaitable[None]], ) -> None: s, _ = ssl_lockstep_stream_pair(client_ctx) - with RaisesGroup(Matcher(_core.BusyResourceError, "another task")): + with pytest.RaisesGroup( + pytest.RaisesExc(_core.BusyResourceError, match="another task") + ): async with _core.open_nursery() as nursery: nursery.start_soon(func1, s) nursery.start_soon(func2, s) diff --git a/src/trio/_tests/test_subprocess.py b/src/trio/_tests/test_subprocess.py index 71b143c38..05ac69d3f 100644 --- a/src/trio/_tests/test_subprocess.py +++ b/src/trio/_tests/test_subprocess.py @@ -22,7 +22,6 @@ import pytest import trio -from trio.testing import Matcher, RaisesGroup from .. import ( Event, @@ -661,7 +660,9 @@ async def do_stuff() -> None: nursery.cancel_scope.cancel() # double wrap from our nursery + the internal nursery - with RaisesGroup(RaisesGroup(Matcher(ValueError, "^foo$"))): + with pytest.RaisesGroup( + pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="^foo$")) + ): _core.run(do_stuff, strict_exception_groups=True) @@ -698,7 +699,7 @@ async def test_warn_on_cancel_SIGKILL_escalation( # the background_process_param exercises a lot of run_process cases, but it uses # check=False, so lets have a test that uses check=True as well async def test_run_process_background_fail() -> None: - with RaisesGroup(subprocess.CalledProcessError): + with pytest.RaisesGroup(subprocess.CalledProcessError): async with _core.open_nursery() as nursery: value = await nursery.start(run_process, EXIT_FALSE) assert isinstance(value, Process) @@ -733,7 +734,7 @@ async def very_broken_open(*args: object, **kwargs: object) -> str: return "oops" monkeypatch.setattr(trio._subprocess, "_open_process", very_broken_open) - with RaisesGroup(AttributeError, AttributeError): + with pytest.RaisesGroup(AttributeError, AttributeError): await run_process(EXIT_TRUE, capture_stdout=True) diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 064689898..e4177307f 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -7,8 +7,6 @@ import pytest -from trio.testing import Matcher, RaisesGroup - from .. import _core from .._core._parking_lot import GLOBAL_PARKING_LOT_BREAKER from .._sync import * @@ -696,8 +694,8 @@ async def test_lock_multiple_acquire() -> None: see https://github.com/python-trio/trio/issues/3035""" assert not GLOBAL_PARKING_LOT_BREAKER lock = trio.Lock() - with RaisesGroup( - Matcher( + with pytest.RaisesGroup( + pytest.RaisesExc( trio.BrokenResourceError, match="^Owner of this lock exited without releasing: ", ), diff --git a/src/trio/_tests/test_testing.py b/src/trio/_tests/test_testing.py index e7c9a1a1a..ca67b8bef 100644 --- a/src/trio/_tests/test_testing.py +++ b/src/trio/_tests/test_testing.py @@ -6,8 +6,6 @@ import pytest -from trio.testing import RaisesGroup - from .. import _core, sleep, socket as tsocket from .._core._tests.tutil import can_bind_ipv6 from .._highlevel_generic import StapledStream, aclose_forcefully @@ -292,7 +290,7 @@ async def getter(expect: bytes) -> None: nursery.start_soon(putter, b"xyz") # Two gets at the same time -> BusyResourceError - with RaisesGroup(_core.BusyResourceError): + with pytest.RaisesGroup(_core.BusyResourceError): async with _core.open_nursery() as nursery: nursery.start_soon(getter, b"asdf") nursery.start_soon(getter, b"asdf") @@ -428,7 +426,7 @@ async def do_receive_some(max_bytes: int | None) -> bytes: mrs.put_data(b"abc") assert await do_receive_some(None) == b"abc" - with RaisesGroup(_core.BusyResourceError): + with pytest.RaisesGroup(_core.BusyResourceError): async with _core.open_nursery() as nursery: nursery.start_soon(do_receive_some, 10) nursery.start_soon(do_receive_some, 10) diff --git a/src/trio/_tests/test_testing_raisesgroup.py b/src/trio/_tests/test_testing_raisesgroup.py index 9cc929938..ba453edbe 100644 --- a/src/trio/_tests/test_testing_raisesgroup.py +++ b/src/trio/_tests/test_testing_raisesgroup.py @@ -7,7 +7,7 @@ import pytest import trio -from trio.testing import Matcher, RaisesGroup +from trio.testing import _Matcher as Matcher, _RaisesGroup as RaisesGroup from trio.testing._raises_group import repr_callable if sys.version_info < (3, 11): @@ -1107,8 +1107,22 @@ def test__ExceptionInfo(monkeypatch: pytest.MonkeyPatch) -> None: "ExceptionInfo", trio.testing._raises_group._ExceptionInfo, ) - with trio.testing.RaisesGroup(ValueError) as excinfo: + with RaisesGroup(ValueError) as excinfo: raise ExceptionGroup("", (ValueError("hello"),)) assert excinfo.type is ExceptionGroup assert excinfo.value.exceptions[0].args == ("hello",) assert isinstance(excinfo.tb, TracebackType) + + +def test_raisesgroup_matcher_deprecation() -> None: + with pytest.deprecated_call(): + trio.testing.Matcher # type: ignore # noqa: B018 + + with pytest.deprecated_call(): + trio.testing.RaisesGroup # type: ignore # noqa: B018 + + with pytest.deprecated_call(): + from trio.testing import Matcher # type: ignore # noqa: F401 + + with pytest.deprecated_call(): + from trio.testing import RaisesGroup # type: ignore # noqa: F401 diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index 69310d64e..c8beefa1c 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -9,7 +9,7 @@ import pytest import trio -from trio.testing import Matcher, RaisesGroup +from trio.testing import _Matcher as Matcher, _RaisesGroup as RaisesGroup from .. import _core from .._core._tests.tutil import ( diff --git a/src/trio/_tests/type_tests/raisesgroup.py b/src/trio/_tests/type_tests/raisesgroup.py deleted file mode 100644 index 012c42b4d..000000000 --- a/src/trio/_tests/type_tests/raisesgroup.py +++ /dev/null @@ -1,223 +0,0 @@ -from __future__ import annotations - -import sys -from collections.abc import Callable - -from trio.testing import Matcher, RaisesGroup -from typing_extensions import assert_type - -if sys.version_info < (3, 11): - from exceptiongroup import BaseExceptionGroup, ExceptionGroup - -# split into functions to isolate the different scopes - - -def check_matcher_typevar_default(e: Matcher) -> None: - assert e.exception_type is not None - _exc: type[BaseException] = e.exception_type - # this would previously pass, as the type would be `Any` - e.exception_type().blah() # type: ignore - - -def check_basic_contextmanager() -> None: - with RaisesGroup(ValueError) as e: - raise ExceptionGroup("foo", (ValueError(),)) - assert_type(e.value, ExceptionGroup[ValueError]) - - -def check_basic_matches() -> None: - # check that matches gets rid of the naked ValueError in the union - exc: ExceptionGroup[ValueError] | ValueError = ExceptionGroup("", (ValueError(),)) - if RaisesGroup(ValueError).matches(exc): - assert_type(exc, ExceptionGroup[ValueError]) - - # also check that BaseExceptionGroup shows up for BaseExceptions - if RaisesGroup(KeyboardInterrupt).matches(exc): - assert_type(exc, BaseExceptionGroup[KeyboardInterrupt]) - - -def check_matches_with_different_exception_type() -> None: - e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup( - "", - (KeyboardInterrupt(),), - ) - - # note: it might be tempting to have this warn. - # however, that isn't possible with current typing - if RaisesGroup(ValueError).matches(e): - assert_type(e, ExceptionGroup[ValueError]) - - -def check_matcher_init() -> None: - def check_exc(exc: BaseException) -> bool: - return isinstance(exc, ValueError) - - # Check various combinations of constructor signatures. - # At least 1 arg must be provided. - Matcher() # type: ignore - Matcher(ValueError) - Matcher(ValueError, "regex") - Matcher(ValueError, "regex", check_exc) - Matcher(exception_type=ValueError) - Matcher(match="regex") - Matcher(check=check_exc) - Matcher(ValueError, match="regex") - Matcher(match="regex", check=check_exc) - - def check_filenotfound(exc: FileNotFoundError) -> bool: - return not exc.filename.endswith(".tmp") - - # If exception_type is provided, that narrows the `check` method's argument. - Matcher(FileNotFoundError, check=check_filenotfound) - Matcher(ValueError, check=check_filenotfound) # type: ignore - Matcher(check=check_filenotfound) # type: ignore - Matcher(FileNotFoundError, match="regex", check=check_filenotfound) - - -def raisesgroup_check_type_narrowing() -> None: - """Check type narrowing on the `check` argument to `RaisesGroup`. - All `type: ignore`s are correctly pointing out type errors. - """ - - def handle_exc(e: BaseExceptionGroup[BaseException]) -> bool: - return True - - def handle_kbi(e: BaseExceptionGroup[KeyboardInterrupt]) -> bool: - return True - - def handle_value(e: BaseExceptionGroup[ValueError]) -> bool: - return True - - RaisesGroup(BaseException, check=handle_exc) - RaisesGroup(BaseException, check=handle_kbi) # type: ignore - - RaisesGroup(Exception, check=handle_exc) - RaisesGroup(Exception, check=handle_value) # type: ignore - - RaisesGroup(KeyboardInterrupt, check=handle_exc) - RaisesGroup(KeyboardInterrupt, check=handle_kbi) - RaisesGroup(KeyboardInterrupt, check=handle_value) # type: ignore - - RaisesGroup(ValueError, check=handle_exc) - RaisesGroup(ValueError, check=handle_kbi) # type: ignore - RaisesGroup(ValueError, check=handle_value) - - RaisesGroup(ValueError, KeyboardInterrupt, check=handle_exc) - RaisesGroup(ValueError, KeyboardInterrupt, check=handle_kbi) # type: ignore - RaisesGroup(ValueError, KeyboardInterrupt, check=handle_value) # type: ignore - - -def raisesgroup_narrow_baseexceptiongroup() -> None: - """Check type narrowing specifically for the container exceptiongroup.""" - - def handle_group(e: ExceptionGroup[Exception]) -> bool: - return True - - def handle_group_value(e: ExceptionGroup[ValueError]) -> bool: - return True - - RaisesGroup(ValueError, check=handle_group_value) - - RaisesGroup(Exception, check=handle_group) - - -def check_matcher_transparent() -> None: - with RaisesGroup(Matcher(ValueError)) as e: - ... - _: BaseExceptionGroup[ValueError] = e.value - assert_type(e.value, ExceptionGroup[ValueError]) - - -def check_nested_raisesgroups_contextmanager() -> None: - with RaisesGroup(RaisesGroup(ValueError)) as excinfo: - raise ExceptionGroup("foo", (ValueError(),)) - - _: BaseExceptionGroup[BaseExceptionGroup[ValueError]] = excinfo.value - - assert_type( - excinfo.value, - ExceptionGroup[ExceptionGroup[ValueError]], - ) - - assert_type( - excinfo.value.exceptions[0], - # this union is because of how typeshed defines .exceptions - ExceptionGroup[ValueError] | ExceptionGroup[ExceptionGroup[ValueError]], - ) - - -def check_nested_raisesgroups_matches() -> None: - """Check nested RaisesGroups with .matches""" - exc: ExceptionGroup[ExceptionGroup[ValueError]] = ExceptionGroup( - "", - (ExceptionGroup("", (ValueError(),)),), - ) - - if RaisesGroup(RaisesGroup(ValueError)).matches(exc): - assert_type(exc, ExceptionGroup[ExceptionGroup[ValueError]]) - - -def check_multiple_exceptions_1() -> None: - a = RaisesGroup(ValueError, ValueError) - b = RaisesGroup(Matcher(ValueError), Matcher(ValueError)) - c = RaisesGroup(ValueError, Matcher(ValueError)) - - d: RaisesGroup[ValueError] - d = a - d = b - d = c - assert isinstance(d, RaisesGroup) - - -def check_multiple_exceptions_2() -> None: - # This previously failed due to lack of covariance in the TypeVar - a = RaisesGroup(Matcher(ValueError), Matcher(TypeError)) - b = RaisesGroup(Matcher(ValueError), TypeError) - c = RaisesGroup(ValueError, TypeError) - - d: RaisesGroup[Exception] - d = a - d = b - d = c - assert isinstance(d, RaisesGroup) - - -def check_raisesgroup_overloads() -> None: - # allow_unwrapped=True does not allow: - # multiple exceptions - RaisesGroup(ValueError, TypeError, allow_unwrapped=True) # type: ignore - # nested RaisesGroup - RaisesGroup(RaisesGroup(ValueError), allow_unwrapped=True) # type: ignore - # specifying match - RaisesGroup(ValueError, match="foo", allow_unwrapped=True) # type: ignore - # specifying check - RaisesGroup(ValueError, check=bool, allow_unwrapped=True) # type: ignore - # allowed variants - RaisesGroup(ValueError, allow_unwrapped=True) - RaisesGroup(ValueError, allow_unwrapped=True, flatten_subgroups=True) - RaisesGroup(Matcher(ValueError), allow_unwrapped=True) - - # flatten_subgroups=True does not allow nested RaisesGroup - RaisesGroup(RaisesGroup(ValueError), flatten_subgroups=True) # type: ignore - # but rest is plenty fine - RaisesGroup(ValueError, TypeError, flatten_subgroups=True) - RaisesGroup(ValueError, match="foo", flatten_subgroups=True) - RaisesGroup(ValueError, check=bool, flatten_subgroups=True) - RaisesGroup(ValueError, flatten_subgroups=True) - RaisesGroup(Matcher(ValueError), flatten_subgroups=True) - - # if they're both false we can of course specify nested raisesgroup - RaisesGroup(RaisesGroup(ValueError)) - - -def check_triple_nested_raisesgroup() -> None: - with RaisesGroup(RaisesGroup(RaisesGroup(ValueError))) as e: - assert_type(e.value, ExceptionGroup[ExceptionGroup[ExceptionGroup[ValueError]]]) - - -def check_check_typing() -> None: - # `BaseExceptiongroup` should perhaps be `ExceptionGroup`, but close enough - assert_type( - RaisesGroup(ValueError).check, - Callable[[BaseExceptionGroup[ValueError]], bool] | None, - ) diff --git a/src/trio/testing/__init__.py b/src/trio/testing/__init__.py index d93d33aab..a8f323447 100644 --- a/src/trio/testing/__init__.py +++ b/src/trio/testing/__init__.py @@ -1,5 +1,6 @@ # Uses `from x import y as y` for compatibility with `pyright --verifytypes` (#2625) +from .. import _deprecate as _deprecate from .._core import ( MockClock as MockClock, wait_all_tasks_blocked as wait_all_tasks_blocked, @@ -28,12 +29,30 @@ memory_stream_pump as memory_stream_pump, ) from ._network import open_stream_to_socket_listener as open_stream_to_socket_listener -from ._raises_group import Matcher as Matcher, RaisesGroup as RaisesGroup +from ._raises_group import Matcher as _Matcher, RaisesGroup as _RaisesGroup from ._sequencer import Sequencer as Sequencer from ._trio_test import trio_test as trio_test ################################################################ +_deprecate.deprecate_attributes( + __name__, + { + "RaisesGroup": _deprecate.DeprecatedAttribute( + _RaisesGroup, + version="0.33.0", + issue=3326, + instead="See https://docs.pytest.org/en/stable/reference/reference.html#pytest.RaisesGroup", + ), + "Matcher": _deprecate.DeprecatedAttribute( + _Matcher, + version="0.33.0", + issue=3326, + instead="See https://docs.pytest.org/en/stable/reference/reference.html#pytest.RaisesExc", + ), + }, +) + fixup_module_metadata(__name__, globals()) del fixup_module_metadata