Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to testing.RaisesGroup for catching unwrapped exceptions #2989

Merged
merged 30 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4497b3d
Add support to testing.RaisesGroup for catching unwrapped exceptions …
jakkdl Apr 15, 2024
62c0ea0
Merge branch 'master' into looser_excgroups
jakkdl Apr 15, 2024
f50f808
add test case to get full coverage
jakkdl Apr 15, 2024
f5c755f
fix type error by adding covariance to typevar
jakkdl Apr 16, 2024
cf64533
rewrite RaisesGroup docstring
jakkdl Apr 16, 2024
38c950b
Merge branch 'master' into looser_excgroups
jakkdl Apr 16, 2024
c506a89
Work around +E typevar issue in docs for _raises_group
TeamSpen210 Apr 17, 2024
0bff4e6
Fix docs issue with type property in _ExceptionInfo
TeamSpen210 Apr 17, 2024
62246f1
Apply suggestions from code review
jakkdl Apr 18, 2024
3a56911
split 'strict' into 'flatten_subgroups' and 'allow_unwrapped', fix bu…
jakkdl Apr 18, 2024
cc5d980
add deprecation of strict, add newsfragments
jakkdl Apr 18, 2024
1a89cd7
the great flattening ...
jakkdl Apr 18, 2024
5af79ac
kinda weird that verifytypes thought this was ambiguous
jakkdl Apr 18, 2024
8f72967
update newsfragments after review
jakkdl Apr 18, 2024
ff3e5fc
update docstring to match new parameter names
jakkdl Apr 18, 2024
2df4c1c
sphinx does not like `...`s
jakkdl Apr 18, 2024
c4dbb78
moar newsfragment improvements
jakkdl Apr 18, 2024
b0d3408
bump exceptiongroup to 1.2.1
jakkdl Apr 22, 2024
36e757a
minor test changes after review
jakkdl Apr 23, 2024
5179221
fix ^$ matching on exceptiongroups
jakkdl Apr 24, 2024
ff5d4eb
Merge branch 'master' into looser_excgroups
jakkdl Apr 24, 2024
8b7aefc
use warn_deprecated instead of DeprecationWarning, disallow allow_unw…
jakkdl May 1, 2024
2183e70
Merge remote-tracking branch 'origin/master' into looser_excgroups
jakkdl May 1, 2024
8c3f1f6
mention $ in bugfix newsfragment, fix test
jakkdl May 1, 2024
f736120
fix coverage
jakkdl May 3, 2024
0c7591a
add test case for nested exceptiongroup + allow_unwrapped
jakkdl May 14, 2024
553df3d
add signature overloads for RaisesGroup to raise type errors when doi…
jakkdl May 14, 2024
04636ac
add pytest.deprecated_call() test
jakkdl May 16, 2024
6f9a8a1
add type tests for narrowing of check argument
jakkdl May 16, 2024
6ef442b
Merge branch 'master' into looser_excgroups
jakkdl May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ def open_nursery(
version="0.24.1",
issue=2929,
instead="the default value of True and rewrite exception handlers to handle ExceptionGroups",
use_triodeprecationwarning=True,
)

if strict_exception_groups is None:
Expand Down Expand Up @@ -2265,6 +2266,7 @@ def run(
version="0.24.1",
issue=2929,
instead="the default value of True and rewrite exception handlers to handle ExceptionGroups",
use_triodeprecationwarning=True,
)

__tracebackhide__ = True
Expand Down Expand Up @@ -2378,6 +2380,7 @@ def my_done_callback(run_outcome):
version="0.24.1",
issue=2929,
instead="the default value of True and rewrite exception handlers to handle ExceptionGroups",
use_triodeprecationwarning=True,
)

runner = setup_runner(
Expand Down
1 change: 1 addition & 0 deletions src/trio/_core/_unbounded_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class UnboundedQueue(Generic[T]):
issue=497,
thing="trio.lowlevel.UnboundedQueue",
instead="trio.open_memory_channel(math.inf)",
use_triodeprecationwarning=True,
)
def __init__(self) -> None:
self._lot = _core.ParkingLot()
Expand Down
22 changes: 19 additions & 3 deletions src/trio/_deprecate.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def warn_deprecated(
issue: int | None,
instead: object,
stacklevel: int = 2,
use_triodeprecationwarning: bool = False,
) -> None:
stacklevel += 1
msg = f"{_stringify(thing)} is deprecated since Trio {version}"
Expand All @@ -67,20 +68,35 @@ def warn_deprecated(
msg += f"; use {_stringify(instead)} instead"
if issue is not None:
msg += f" ({_url_for_issue(issue)})"
warnings.warn(TrioDeprecationWarning(msg), stacklevel=stacklevel)
if use_triodeprecationwarning:
warning_class: type[Warning] = TrioDeprecationWarning
else:
warning_class = DeprecationWarning
warnings.warn(warning_class(msg), stacklevel=stacklevel)


# @deprecated("0.2.0", issue=..., instead=...)
# def ...
def deprecated(
version: str, *, thing: object = None, issue: int | None, instead: object
version: str,
*,
thing: object = None,
issue: int | None,
instead: object,
use_triodeprecationwarning: bool = False,
) -> Callable[[Callable[ArgsT, RetT]], Callable[ArgsT, RetT]]:
def do_wrap(fn: Callable[ArgsT, RetT]) -> Callable[ArgsT, RetT]:
nonlocal thing

@wraps(fn)
def wrapper(*args: ArgsT.args, **kwargs: ArgsT.kwargs) -> RetT:
warn_deprecated(thing, version, instead=instead, issue=issue)
warn_deprecated(
thing,
version,
instead=instead,
issue=issue,
use_triodeprecationwarning=use_triodeprecationwarning,
)
return fn(*args, **kwargs)

# If our __module__ or __qualname__ get modified, we want to pick up
Expand Down
1 change: 1 addition & 0 deletions src/trio/_highlevel_open_tcp_listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def _compute_backlog(backlog: int | None) -> int:
version="0.23.0",
instead="None",
issue=2842,
use_triodeprecationwarning=True,
)
if not isinstance(backlog, int) and backlog is not None:
raise TypeError(f"backlog must be an int or None, not {backlog!r}")
Expand Down
32 changes: 21 additions & 11 deletions src/trio/_tests/test_deprecate.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def deprecated_thing() -> None:
deprecated_thing()
filename, lineno = _here()
assert len(recwarn_always) == 1
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "ice is deprecated" in got.message.args[0]
assert "Trio 1.2" in got.message.args[0]
Expand All @@ -54,7 +54,7 @@ def test_warn_deprecated_no_instead_or_issue(
# Explicitly no instead or issue
warn_deprecated("water", "1.3", issue=None, instead=None)
assert len(recwarn_always) == 1
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "water is deprecated" in got.message.args[0]
assert "no replacement" in got.message.args[0]
Expand All @@ -70,7 +70,7 @@ def nested2() -> None:

filename, lineno = _here()
nested1()
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert got.filename == filename
assert got.lineno == lineno + 1

Expand All @@ -85,7 +85,7 @@ def new() -> None: # pragma: no cover

def test_warn_deprecated_formatting(recwarn_always: pytest.WarningsRecorder) -> None:
warn_deprecated(old, "1.0", issue=1, instead=new)
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "test_deprecate.old is deprecated" in got.message.args[0]
assert "test_deprecate.new instead" in got.message.args[0]
Expand All @@ -98,7 +98,7 @@ def deprecated_old() -> int:

def test_deprecated_decorator(recwarn_always: pytest.WarningsRecorder) -> None:
assert deprecated_old() == 3
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "test_deprecate.deprecated_old is deprecated" in got.message.args[0]
assert "1.5" in got.message.args[0]
Expand All @@ -115,7 +115,7 @@ def method(self) -> int:
def test_deprecated_decorator_method(recwarn_always: pytest.WarningsRecorder) -> None:
f = Foo()
assert f.method() == 7
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "test_deprecate.Foo.method is deprecated" in got.message.args[0]

Expand All @@ -129,7 +129,7 @@ def test_deprecated_decorator_with_explicit_thing(
recwarn_always: pytest.WarningsRecorder,
) -> None:
assert deprecated_with_thing() == 72
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "the thing is deprecated" in got.message.args[0]

Expand All @@ -143,7 +143,7 @@ def new_hotness() -> str:

def test_deprecated_alias(recwarn_always: pytest.WarningsRecorder) -> None:
assert old_hotness() == "new hotness"
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "test_deprecate.old_hotness is deprecated" in got.message.args[0]
assert "1.23" in got.message.args[0]
Expand All @@ -168,7 +168,7 @@ def new_hotness_method(self) -> str:
def test_deprecated_alias_method(recwarn_always: pytest.WarningsRecorder) -> None:
obj = Alias()
assert obj.old_hotness_method() == "new hotness method"
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
msg = got.message.args[0]
assert "test_deprecate.Alias.old_hotness_method is deprecated" in msg
Expand Down Expand Up @@ -243,7 +243,7 @@ def test_module_with_deprecations(recwarn_always: pytest.WarningsRecorder) -> No

filename, lineno = _here()
assert module_with_deprecations.dep1 == "value1" # type: ignore[attr-defined]
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert got.filename == filename
assert got.lineno == lineno + 1
Expand All @@ -254,9 +254,19 @@ def test_module_with_deprecations(recwarn_always: pytest.WarningsRecorder) -> No
assert "value1 instead" in got.message.args[0]

assert module_with_deprecations.dep2 == "value2" # type: ignore[attr-defined]
got = recwarn_always.pop(TrioDeprecationWarning)
got = recwarn_always.pop(DeprecationWarning)
assert isinstance(got.message, Warning)
assert "instead-string instead" in got.message.args[0]

with pytest.raises(AttributeError):
module_with_deprecations.asdf # type: ignore[attr-defined] # noqa: B018 # "useless expression"


def test_warning_class() -> None:
with pytest.warns(DeprecationWarning):
warn_deprecated("foo", "bar", issue=None, instead=None)
A5rocks marked this conversation as resolved.
Show resolved Hide resolved

with pytest.warns(TrioDeprecationWarning):
warn_deprecated(
"foo", "bar", issue=None, instead=None, use_triodeprecationwarning=True
)
35 changes: 31 additions & 4 deletions src/trio/_tests/test_testing_raisesgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ def test_catch_unwrapped_exceptions() -> None:
with pytest.raises(
ValueError, match="^You cannot specify multiple exceptions with"
):
with RaisesGroup(SyntaxError, ValueError, allow_unwrapped=True):
...
RaisesGroup(SyntaxError, ValueError, allow_unwrapped=True)
CoolCat467 marked this conversation as resolved.
Show resolved Hide resolved
# if users want one of several exception types they need to use a Matcher
# (which the error message suggests)
with RaisesGroup(
Expand All @@ -130,8 +129,8 @@ def test_catch_unwrapped_exceptions() -> None:

# Unwrapped nested `RaisesGroup` is likely a user error, so we raise an error.
with pytest.raises(ValueError, match="has no effect when expecting"):
with RaisesGroup(RaisesGroup(ValueError), allow_unwrapped=True):
...
RaisesGroup(RaisesGroup(ValueError), allow_unwrapped=True)

# But it *can* be used to check for nesting level +- 1 if they move it to
# the nested RaisesGroup. Users should probably use `Matcher`s instead though.
with RaisesGroup(RaisesGroup(ValueError, allow_unwrapped=True)):
Expand Down Expand Up @@ -186,6 +185,34 @@ def test_check() -> None:
raise ExceptionGroup("", (ValueError(),))


def test_unwrapped_match_check() -> None:
msg = (
"`allow_unwrapped=True` bypasses the `match` and `check` parameters"
" if the exception is unwrapped. If you intended to match/check the"
" exception you should use a `Matcher` object. If you want to match/check"
" the exceptiongroup when the exception *is* wrapped you need to"
" do e.g. `if isinstance(exc.value, ExceptionGroup):"
" assert RaisesGroup(...).matches(exc.value)` afterwards."
)
A5rocks marked this conversation as resolved.
Show resolved Hide resolved
with pytest.raises(ValueError, match=re.escape(msg)):
RaisesGroup(ValueError, allow_unwrapped=True, match="foo")
with pytest.raises(ValueError, match=re.escape(msg)):
RaisesGroup(ValueError, allow_unwrapped=True, check=lambda x: True)

# Users should instead use a Matcher
rg = RaisesGroup(Matcher(ValueError, match="^foo$"), allow_unwrapped=True)
with rg:
raise ValueError("foo")
with rg:
raise ExceptionGroup("", [ValueError("foo")])

# or if they wanted to match/check the group, do a conditional `.matches()`
with RaisesGroup(ValueError, allow_unwrapped=True) as exc:
raise ExceptionGroup("bar", [ValueError("foo")])
if isinstance(exc.value, ExceptionGroup):
assert RaisesGroup(ValueError, match="bar").matches(exc.value)


def test_RaisesGroup_matches() -> None:
rg = RaisesGroup(ValueError)
assert not rg.matches(None)
Expand Down
1 change: 1 addition & 0 deletions src/trio/_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ async def to_thread_run_sync( # type: ignore[misc]
"0.23.0",
issue=2841,
instead="`abandon_on_cancel=`",
use_triodeprecationwarning=True,
)
abandon_on_cancel = cancellable
# raise early if abandon_on_cancel.__bool__ raises
Expand Down
22 changes: 15 additions & 7 deletions src/trio/testing/_raises_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import re
import sys
import warnings
from typing import (
TYPE_CHECKING,
Callable,
Expand All @@ -15,6 +14,7 @@
overload,
)

from trio._deprecate import warn_deprecated
from trio._util import final

if TYPE_CHECKING:
Expand Down Expand Up @@ -342,12 +342,11 @@ def __init__(
self.is_baseexceptiongroup = False

if strict is not None:
warnings.warn(
DeprecationWarning(
"`strict=False` has been replaced with `flatten_subgroups=True`"
" with the introduction of `allow_unwrapped` as a parameter."
),
stacklevel=2,
warn_deprecated(
"The `strict` parameter",
"0.25.1",
issue=2989,
instead="flatten_subgroups=True (for strict=False}",
)
self.flatten_subgroups = not strict

Expand All @@ -364,6 +363,15 @@ def __init__(
" You might want it in the expected `RaisesGroup`, or"
" `flatten_subgroups=True` if you don't care about the structure."
)
A5rocks marked this conversation as resolved.
Show resolved Hide resolved
if allow_unwrapped and (match is not None or check is not None):
raise ValueError(
"`allow_unwrapped=True` bypasses the `match` and `check` parameters"
" if the exception is unwrapped. If you intended to match/check the"
" exception you should use a `Matcher` object. If you want to match/check"
" the exceptiongroup when the exception *is* wrapped you need to"
" do e.g. `if isinstance(exc, ExceptionGroup):"
" assert RaisesGroup(...).matches(exc)` afterwards."
)
A5rocks marked this conversation as resolved.
Show resolved Hide resolved

# verify `expected_exceptions` and set `self.is_baseexceptiongroup`
for exc in self.expected_exceptions:
Expand Down