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 19 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
1 change: 1 addition & 0 deletions newsfragments/2989.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
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``)
69 changes: 61 additions & 8 deletions src/trio/_tests/test_testing_raisesgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,33 +78,77 @@
with RaisesGroup(ValueError, SyntaxError):
raise ExceptionGroup("", (ValueError(),))


def test_flatten_subgroups() -> None:
# loose semantics, as with expect*
with RaisesGroup(ValueError, strict=False):
with RaisesGroup(ValueError, flatten_subgroups=True):
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))

with RaisesGroup(ValueError, TypeError, flatten_subgroups=True):
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(), TypeError())),))
with RaisesGroup(ValueError, TypeError, flatten_subgroups=True):
raise ExceptionGroup("", [ExceptionGroup("", [ValueError()]), TypeError()])

# mixed loose is possible if you want it to be at least N deep
with RaisesGroup(RaisesGroup(ValueError, strict=False)):
with RaisesGroup(RaisesGroup(ValueError, flatten_subgroups=True)):
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
with RaisesGroup(RaisesGroup(ValueError, strict=False)):
with RaisesGroup(RaisesGroup(ValueError, flatten_subgroups=True)):
raise ExceptionGroup(
"", (ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)),)
)
with pytest.raises(ExceptionGroup):
with RaisesGroup(RaisesGroup(ValueError, strict=False)):
with RaisesGroup(RaisesGroup(ValueError, flatten_subgroups=True)):
raise ExceptionGroup("", (ValueError(),))

# but not the other way around
with pytest.raises(
ValueError,
match="^You cannot specify a nested structure inside a RaisesGroup with strict=False$",
match="^You cannot specify a nested structure inside a RaisesGroup with",
):
RaisesGroup(RaisesGroup(ValueError), strict=False)
RaisesGroup(RaisesGroup(ValueError), flatten_subgroups=True)


# currently not fully identical in behaviour to expect*, which would also catch an unwrapped exception
def test_catch_unwrapped_exceptions() -> None:
# Catches lone exceptions with strict=False
# just as except* would
with RaisesGroup(ValueError, allow_unwrapped=True):
raise ValueError

# expecting multiple unwrapped exceptions is not possible
with pytest.raises(
ValueError, match="^You cannot specify multiple exceptions with"
):
with RaisesGroup(SyntaxError, ValueError, allow_unwrapped=True):
...

Check warning on line 122 in src/trio/_tests/test_testing_raisesgroup.py

View check run for this annotation

Codecov / codecov/patch

src/trio/_tests/test_testing_raisesgroup.py#L122

Added line #L122 was not covered by tests
# if users want one of several exception types they need to use a Matcher
# (which the error message suggests)
with RaisesGroup(
Matcher(check=lambda e: isinstance(e, (SyntaxError, ValueError))),
allow_unwrapped=True,
):
raise ValueError

# 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):
...

Check warning on line 134 in src/trio/_tests/test_testing_raisesgroup.py

View check run for this annotation

Codecov / codecov/patch

src/trio/_tests/test_testing_raisesgroup.py#L134

Added line #L134 was not covered by tests
jakkdl marked this conversation as resolved.
Show resolved Hide resolved
# 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)):
raise ExceptionGroup("", [ExceptionGroup("", [ValueError()])])
with RaisesGroup(RaisesGroup(ValueError, allow_unwrapped=True)):
raise ExceptionGroup("", [ValueError()])

# with allow_unwrapped=False (default) it will not be caught
with pytest.raises(ValueError, match="^value error text$"):
with RaisesGroup(ValueError, strict=False):
with RaisesGroup(ValueError):
raise ValueError("value error text")

# code coverage
with pytest.raises(TypeError):
with RaisesGroup(ValueError, allow_unwrapped=True):
raise TypeError

A5rocks marked this conversation as resolved.
Show resolved Hide resolved

def test_match() -> None:
# supports match string
Expand Down Expand Up @@ -260,3 +304,12 @@
assert excinfo.type is ExceptionGroup
assert excinfo.value.exceptions[0].args == ("hello",)
assert isinstance(excinfo.tb, TracebackType)


def test_deprecated_strict() -> None:
"""`strict` has been replaced with `flatten_subgroups`"""
# parameter is typed as `None` to also emit a type error if passing anything
with pytest.deprecated_call():
RaisesGroup(ValueError, strict=False) # type: ignore[arg-type]
with pytest.deprecated_call():
RaisesGroup(ValueError, strict=True) # type: ignore[arg-type]
53 changes: 36 additions & 17 deletions src/trio/_tests/type_tests/raisesgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,7 @@ def check_inheritance_and_assignments() -> None:

a: BaseExceptionGroup[BaseExceptionGroup[ValueError]]
a = RaisesGroup(RaisesGroup(ValueError))
# pyright-ignore due to bug in exceptiongroup
# https://github.com/agronholm/exceptiongroup/pull/101
# once fixed we'll get errors for unnecessary-pyright-ignore and can clean up
a = BaseExceptionGroup(
"", (BaseExceptionGroup("", (ValueError(),)),) # pyright: ignore
)
a = BaseExceptionGroup("", (BaseExceptionGroup("", (ValueError(),)),))
assert a


Expand Down Expand Up @@ -71,23 +66,25 @@ def check_matcher_init() -> None:
def check_exc(exc: BaseException) -> bool:
return isinstance(exc, ValueError)

def check_filenotfound(exc: FileNotFoundError) -> bool:
return not exc.filename.endswith(".tmp")

# Check various combinations of constructor signatures.
# At least 1 arg must be provided. If exception_type is provided, that narrows
# check's argument.
# 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(check=check_filenotfound) # type: ignore
Matcher(ValueError, match="regex")
Matcher(FileNotFoundError, check=check_filenotfound)
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)


Expand Down Expand Up @@ -125,12 +122,34 @@ def check_nested_raisesgroups_contextmanager() -> None:

def check_nested_raisesgroups_matches() -> None:
"""Check nested RaisesGroups with .matches"""
# pyright-ignore due to bug in exceptiongroup
# https://github.com/agronholm/exceptiongroup/pull/101
# once fixed we'll get errors for unnecessary-pyright-ignore and can clean up
exc: ExceptionGroup[ExceptionGroup[ValueError]] = ExceptionGroup(
"", (ExceptionGroup("", (ValueError(),)),) # pyright: ignore
"", (ExceptionGroup("", (ValueError(),)),)
)
# has the same problems as check_nested_raisesgroups_contextmanager
if RaisesGroup(RaisesGroup(ValueError)).matches(exc):
assert_type(exc, BaseExceptionGroup[RaisesGroup[ValueError]])


def check_multiple_exceptions_1() -> None:
a = RaisesGroup(ValueError, ValueError)
b = RaisesGroup(Matcher(ValueError), Matcher(ValueError))
c = RaisesGroup(ValueError, Matcher(ValueError))

d: BaseExceptionGroup[ValueError]
d = a
d = b
d = c
assert d


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: BaseExceptionGroup[Exception]
d = a
d = b
d = c
assert d