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 25 commits
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import collections.abc
import os
import sys
import types
from typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
Expand Down Expand Up @@ -98,14 +99,22 @@
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#event-autodoc-process-signature
def autodoc_process_signature(
app: Sphinx,
what: object,
what: str,
name: str,
obj: object,
options: object,
signature: str,
return_annotation: str,
) -> tuple[str, str]:
"""Modify found signatures to fix various issues."""
if name == "trio.testing._raises_group._ExceptionInfo.type":
# This has the type "type[E]", which gets resolved into the property itself.
# That means Sphinx can't resolve it. Fix the issue by overwriting with a fully-qualified
# name.
assert isinstance(obj, property), obj
assert isinstance(obj.fget, types.FunctionType), obj.fget
assert obj.fget.__annotations__["return"] == "type[E]", obj.fget.__annotations__
obj.fget.__annotations__["return"] = "type[~trio.testing._raises_group.E]"
if signature is not None:
signature = signature.replace("~_contextvars.Context", "~contextvars.Context")
if name == "trio.lowlevel.RunVar": # Typevar is not useful here.
Expand All @@ -114,6 +123,13 @@ def autodoc_process_signature(
# Strip the type from the union, make it look like = ...
signature = signature.replace(" | type[trio._core._local._NoValue]", "")
signature = signature.replace("<class 'trio._core._local._NoValue'>", "...")
if (
name in ("trio.testing.RaisesGroup", "trio.testing.Matcher")
and "+E" in signature
):
# This typevar being covariant isn't handled correctly in some cases, strip the +
# and insert the fully-qualified name.
signature = signature.replace("+E", "~trio.testing._raises_group.E")
if "DTLS" in name:
signature = signature.replace("SSL.Context", "OpenSSL.SSL.Context")
# Don't specify PathLike[str] | PathLike[bytes], this is just for humans.
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/2989.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed a bug where :class:`trio.testing.RaisesGroup(..., strict=False) <trio.testing.RaisesGroup>` would check the number of exceptions in the raised `ExceptionGroup` before flattening subgroups, leading to incorrectly failed matches.
It now properly supports end (``$``) regex markers in the ``match`` message, by no longer including " (x sub-exceptions)" in the string it matches against.
1 change: 1 addition & 0 deletions newsfragments/2989.deprecated.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecated ``strict`` parameter from :class:`trio.testing.RaisesGroup`, previous functionality of ``strict=False`` is now in ``flatten_subgroups=True``.
1 change: 1 addition & 0 deletions newsfragments/2989.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:class:`trio.testing.RaisesGroup` can now catch an unwrapped exception with ``unwrapped=True``. This means that the behaviour of :ref:`except* <except_star>` can be fully replicated in combination with ``flatten_subgroups=True`` (formerly ``strict=False``).
3 changes: 3 additions & 0 deletions src/trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,7 @@ def open_nursery(
"the default value of True and rewrite exception handlers to handle ExceptionGroups. "
"See https://trio.readthedocs.io/en/stable/reference-core.html#designing-for-multiple-errors"
),
use_triodeprecationwarning=True,
)

if strict_exception_groups is None:
Expand Down Expand Up @@ -2271,6 +2272,7 @@ def run(
"the default value of True and rewrite exception handlers to handle ExceptionGroups. "
"See https://trio.readthedocs.io/en/stable/reference-core.html#designing-for-multiple-errors"
),
use_triodeprecationwarning=True,
)

__tracebackhide__ = True
Expand Down Expand Up @@ -2387,6 +2389,7 @@ def my_done_callback(run_outcome):
"the default value of True and rewrite exception handlers to handle ExceptionGroups. "
"See https://trio.readthedocs.io/en/stable/reference-core.html#designing-for-multiple-errors"
),
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
)