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

Replace MultiError with BaseExceptionGroup #2213

Merged
merged 104 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
31706e8
Add news fragment for #2210
agronholm Jan 25, 2022
743354b
Replace MultiError with (Base)ExceptionGroup
agronholm Jan 15, 2022
6fdd0fe
Collapse exception groups containing a single exception
agronholm Jan 16, 2022
13de11d
Require exceptiongroup unconditionally for tests
agronholm Jan 16, 2022
9ecca9b
Added exceptiongroup to docs requirements
agronholm Jan 25, 2022
21757c8
Fixed dangling references in the documentation
agronholm Jan 25, 2022
61a1646
Removed obsolete check
agronholm Jan 25, 2022
ca05efb
Removed one last MultiError reference in docs
agronholm Jan 25, 2022
0b159e8
Found one more MultiError, in a news fragment
agronholm Jan 25, 2022
e9aaabc
Added a test for exception group collapsing
agronholm Jan 25, 2022
83e054c
Merge branch 'master' into exceptiongroup
pquentin Jan 27, 2022
370e40f
Merge branch 'master' into exceptiongroup
agronholm Jan 30, 2022
e2fbec0
Updated an example to refer to BaseExceptionGroup
agronholm Jan 30, 2022
95a7b9e
Improved the docs regarding exception groups
agronholm Jan 30, 2022
745f3cf
Added Python 3.11 to the CI matrix
agronholm Jan 30, 2022
bc043aa
Attempt at fixing Python 3.11 test runs
agronholm Jan 30, 2022
797c234
Removed Windows and macOS py3.11 test jobs
agronholm Jan 30, 2022
8a09778
Skip test_coroutine_or_error() on Python 3.11
agronholm Jan 30, 2022
c1200de
Merge branch 'master' into exceptiongroup
agronholm Jan 31, 2022
30358d1
Changed case to be consistent in BEG messages
agronholm Jan 31, 2022
54eb0ce
Restored (and deprecated) MultiError
agronholm Feb 3, 2022
f1cbcf7
Removed unwarranted type annotations
agronholm Feb 3, 2022
0f5dc79
Removed useless failing test
agronholm Feb 3, 2022
36e0eba
Removed unused function
agronholm Feb 3, 2022
20df5d8
Update docs/source/reference-core.rst
agronholm Feb 8, 2022
6eeb137
Merge branch 'master' into exceptiongroup
agronholm Feb 8, 2022
2dc1c64
Update newsfragments/2211.deprecated.rst
agronholm Feb 8, 2022
e7faa46
Update docs/source/tutorial/echo-server.py
agronholm Feb 8, 2022
c41517a
Update trio/_highlevel_open_tcp_stream.py
agronholm Feb 8, 2022
50963c6
Don't skip the entire test_coroutine_or_error test on py3.11
agronholm Feb 8, 2022
ba17fbb
Removed weird test code
agronholm Feb 8, 2022
8911e0a
Update newsfragments/2211.deprecated.rst
agronholm Feb 8, 2022
dfd084c
Bump prompt-toolkit from 3.0.26 to 3.0.27
dependabot[bot] Feb 8, 2022
b3c79c8
Bump tomli from 2.0.0 to 2.0.1
dependabot[bot] Feb 9, 2022
81feff2
Bump platformdirs from 2.4.1 to 2.5.0
dependabot[bot] Feb 10, 2022
bcb442b
Bump towncrier from 21.3.0 to 21.9.0
dependabot[bot] Feb 11, 2022
ab78e02
Bump prompt-toolkit from 3.0.27 to 3.0.28
dependabot[bot] Feb 11, 2022
9abddfa
Bump markupsafe from 2.0.1 to 2.1.0
dependabot[bot] Feb 18, 2022
efc8b01
Bump click from 8.0.3 to 8.0.4
dependabot[bot] Feb 21, 2022
9a4f512
Bump platformdirs from 2.5.0 to 2.5.1
dependabot[bot] Feb 21, 2022
6cb5ad9
Merged test requirements
agronholm Mar 9, 2022
ee245a0
Bump pytest from 7.0.0 to 7.0.1
dependabot[bot] Feb 14, 2022
1e36f8c
Tickle sr.ht CI
pquentin Feb 21, 2022
034bd32
Merge two competing newsfragments
pquentin Feb 21, 2022
80a751f
Bump charset-normalizer from 2.0.11 to 2.0.12
dependabot[bot] Feb 21, 2022
9eb2d1c
Tickle sr.ht CI
pquentin Feb 21, 2022
fd56492
Bump version to 0.20.0
pquentin Feb 21, 2022
af6233b
Bump version to 0.20.0+dev
pquentin Feb 21, 2022
a4876bc
Renamed the news fragment
agronholm Mar 9, 2022
5c09f44
Updated wrapt in test requirements
agronholm Mar 9, 2022
c5af0a4
Changed wrapt target git hash so it's compatible with astroid
agronholm Mar 9, 2022
93c81f0
Merge branch 'master' into exceptiongroup
agronholm May 14, 2022
b4791f8
Fixed error in test_coroutine_or_error() on py3.11
agronholm May 14, 2022
70bb109
Updated dependencies
agronholm May 14, 2022
1c403b1
Move Python 3.11 to its separate test job
agronholm May 14, 2022
a28bfac
Fixed bad pytest switch
agronholm May 14, 2022
7694592
Implemented the strict_exception_groups setting
agronholm May 14, 2022
161c0ab
Implemented NonBaseMultiError which inherits from both MultiError and…
agronholm May 14, 2022
226184f
Fixed MultiError reference in version history
agronholm May 14, 2022
748ceb8
Removed Python 3.11 from test matrix
agronholm May 14, 2022
dfdda5b
Documented the MultiError compatibility issue
agronholm May 14, 2022
d49abbb
Merge branch 'master' into exceptiongroup
agronholm May 16, 2022
a877191
Merge branch 'master' into exceptiongroup
agronholm May 16, 2022
e8a1b60
Documentation updates
agronholm May 17, 2022
4bd09bc
Changed open_tcp_listeners() and open_tcp_stream() to use ExceptionGr…
agronholm May 17, 2022
1101ead
Expose NonBaseMultiError in trio (as a deprecated attribute)
agronholm May 17, 2022
cd11213
Changed wording in MultiError deprecation messages
agronholm May 20, 2022
7088be4
Added missing install dependency on exceptiongroup
agronholm May 20, 2022
5bc2bd9
Merge branch 'master' into exceptiongroup
agronholm Jun 7, 2022
e87de6c
Updated trio version in deprecation notes
agronholm Jun 7, 2022
6b5e4f6
Fixed formatting error
agronholm Jun 7, 2022
9ff7a45
Use trio's own deprecation mechanism
agronholm Jul 4, 2022
2140b70
Added comment on _collapse=False in derive()
agronholm Jul 4, 2022
1da1af4
Only modify MultiErrors in collapse_exception_group()
agronholm Jul 4, 2022
f9e11fc
Retain traceback frames from the MultiError itself
agronholm Jul 4, 2022
1d99b23
Improved prevention of MultiError double initialization
agronholm Jul 9, 2022
2591808
Reworded the comments in the exception group handling examples
agronholm Jul 9, 2022
afd798f
Merge branch 'master' into exceptiongroup
agronholm Jul 9, 2022
8c77459
Made all references to (Base)ExceptionGroup into links
agronholm Jul 15, 2022
b621e70
Improved the documentation for exception group handling
agronholm Jul 17, 2022
656fad4
Fixed test collection error on Python 3.11
agronholm Jul 20, 2022
6e86da3
Readded the exceptiongroups label
agronholm Jul 20, 2022
e29c00f
Merge branch 'master' into exceptiongroup
agronholm Sep 4, 2022
16c19b6
Spelled out MultiError.filter() and MultiError.catch()
agronholm Sep 18, 2022
c35fccc
Fixed the instead= part in the deprecation warning
agronholm Sep 18, 2022
7b7acb9
Replaced warn() with warn_deprecated()
agronholm Sep 18, 2022
3b97ce1
Merge branch 'master' into exceptiongroup
agronholm Sep 18, 2022
1075ff0
Fixed the check for StopAsyncIteration
agronholm Sep 18, 2022
458f5e2
Fixed comment in _nested_child_finished()
agronholm Sep 18, 2022
0f39b3f
Fixed deprecation checking in MultiError.catch() tests
agronholm Sep 18, 2022
dc35213
Reversed the order of tracebacks when concatenating
agronholm Sep 18, 2022
f9eff8e
Restored the IPython custom error handler
agronholm Sep 18, 2022
30b8f3b
Added link to specific IPython issue
agronholm Sep 20, 2022
232688b
Updated wording about extracting submodules to independent packages
agronholm Sep 20, 2022
0b8e908
If present, patch Apport to support exception groups
agronholm Sep 25, 2022
878af56
Added test coverage for collapse_exception_group()
agronholm Sep 25, 2022
cc7d76a
Changed cancel scopes to not collapse exceptions by default
agronholm Sep 25, 2022
a20e2d8
Extended test coverage
agronholm Sep 26, 2022
1aba33e
Fixed the apport script test
agronholm Sep 26, 2022
0c283b7
Fixed MultiError.collapse attribute missing on Python 3.11
agronholm Sep 26, 2022
0364b48
Added test coverage for cancel scope encountering an excgroup w/o Can…
agronholm Sep 26, 2022
bb31ed6
Removed an except block that was never entered
agronholm Sep 26, 2022
2fd8309
Fixed excepthook wrapper name
agronholm Sep 27, 2022
80889b2
Removed unused parameter
agronholm Sep 27, 2022
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
38 changes: 37 additions & 1 deletion trio/_core/_multierror.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ class MultiError(BaseExceptionGroup):
"""

def __init__(self, exceptions, *, _collapse=True):
self.collapse = _collapse

# Avoid double initialization when _collapse is True and exceptions[0] returned
# by __new__() happens to be a MultiError and subsequently __init__() is called.
if _collapse and getattr(self, "exceptions", None) is not None:
Expand Down Expand Up @@ -220,7 +222,9 @@ def __repr__(self):
def derive(self, __excs):
# We use _collapse=False here to get ExceptionGroup semantics, since derive()
# is part of the PEP 654 API
return MultiError(__excs, _collapse=False)
exc = MultiError(__excs, _collapse=False)
exc.collapse = self.collapse
return exc

@classmethod
def filter(cls, handler, root_exc):
Expand Down Expand Up @@ -414,3 +418,35 @@ def trio_show_traceback(self, etype, value, tb, tb_offset=None):
print_exception(value)

ip.set_custom_exc((BaseExceptionGroup,), trio_show_traceback)


# Ubuntu's system Python has a sitecustomize.py file that import
# apport_python_hook and replaces sys.excepthook.
#
# The custom hook captures the error for crash reporting, and then calls
# sys.__excepthook__ to actually print the error.
#
# We don't mind it capturing the error for crash reporting, but we want to
# take over printing the error. So we monkeypatch the apport_python_hook
# module so that instead of calling sys.__excepthook__, it calls our custom
# hook.
#
# More details: https://github.com/python-trio/trio/issues/1065
if (
sys.version_info < (3, 11)
and getattr(sys.excepthook, "__name__", None) == "apport_excepthook"
):
from types import ModuleType

import apport_python_hook
from exceptiongroup import format_exception

assert sys.excepthook is apport_python_hook.apport_excepthook

def replacement_webhook(etype, value, tb):
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
sys.stderr.write("".join(format_exception(etype, value, tb)))
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved

fake_sys = ModuleType("trio_fake_sys")
fake_sys.__dict__.update(sys.__dict__)
fake_sys.__excepthook__ = replacement_webhook # type: ignore
apport_python_hook.sys = fake_sys
10 changes: 5 additions & 5 deletions trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ def collapse_exception_group(excgroup):
exceptions = list(excgroup.exceptions)
modified = False
for i, exc in enumerate(exceptions):
if isinstance(exc, MultiError):
if isinstance(exc, BaseExceptionGroup):
oremanj marked this conversation as resolved.
Show resolved Hide resolved
new_exc = collapse_exception_group(exc)
if new_exc is not exc:
modified = True
exceptions[i] = new_exc

if len(exceptions) == 1:
if len(exceptions) == 1 and isinstance(excgroup, MultiError) and excgroup.collapse:
exceptions[0].__traceback__ = concat_tb(
excgroup.__traceback__, exceptions[0].__traceback__
)
Expand Down Expand Up @@ -475,7 +475,7 @@ def __enter__(self):
task._activate_cancel_status(self._cancel_status)
return self

def _close(self, exc, collapse=True):
def _close(self, exc, collapse=False):
oremanj marked this conversation as resolved.
Show resolved Hide resolved
if self._cancel_status is None:
new_exc = RuntimeError(
"Cancel scope stack corrupted: attempted to exit {!r} "
Expand Down Expand Up @@ -540,8 +540,8 @@ def _close(self, exc, collapse=True):
if matched:
self.cancelled_caught = True

if collapse and isinstance(exc, MultiError):
exc = collapse_exception_group(exc)
if exc:
exc = collapse_exception_group(exc)

self._cancel_status.close()
with self._might_change_registered_deadline():
Expand Down
19 changes: 19 additions & 0 deletions trio/_core/tests/test_multierror.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,22 @@ def test_ipython_custom_exc_handler():
)
# Make sure our other warning doesn't show up
assert "custom sys.excepthook" not in completed.stdout.decode("utf-8")


@slow
@pytest.mark.skipif(
not Path("/usr/lib/python3/dist-packages/apport_python_hook.py").exists(),
reason="need Ubuntu with python3-apport installed",
)
def test_apport_excepthook_monkeypatch_interaction():
completed = run_script("apport_excepthook.py")
stdout = completed.stdout.decode("utf-8")

# No warning
assert "custom sys.excepthook" not in stdout

# Proper traceback
assert_match_in_seq(
["--- 1 ---", "KeyError", "--- 2 ---", "ValueError"],
stdout,
)
13 changes: 13 additions & 0 deletions trio/_core/tests/test_multierror_scripts/apport_excepthook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# The apport_python_hook package is only installed as part of Ubuntu's system
# python, and not available in venvs. So before we can import it we have to
# make sure it's on sys.path.
import sys

sys.path.append("/usr/lib/python3/dist-packages")
import apport_python_hook

apport_python_hook.install()

import trio

raise trio.MultiError([KeyError("key_error"), ValueError("value_error")])
76 changes: 67 additions & 9 deletions trio/_core/tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1855,17 +1855,11 @@ def __aiter__(self):
async def __anext__(self):
nexts = self.nexts
items = [None] * len(nexts)
got_stop = False

try:
async with _core.open_nursery() as nursery:
for i, f in enumerate(nexts):
nursery.start_soon(self._accumulate, f, items, i)
except ExceptionGroup as excgroup:
got_stop = bool(excgroup.split(StopAsyncIteration)[0])
async with _core.open_nursery() as nursery:
for i, f in enumerate(nexts):
nursery.start_soon(self._accumulate, f, items, i)

if got_stop:
raise StopAsyncIteration
return items

result = []
Expand Down Expand Up @@ -2371,3 +2365,67 @@ async def test_nursery_strict_exception_groups():
assert len(exc.value.exceptions) == 1
assert type(exc.value.exceptions[0]) is Exception
assert exc.value.exceptions[0].args == ("foo",)


async def test_nursery_collapse_strict():
"""
Test that a single exception from a nested nursery with strict semantics doesn't get
collapsed when CancelledErrors are stripped from it.
"""

async def raise_error():
raise RuntimeError("test error")

with pytest.raises(MultiError) as exc:
async with _core.open_nursery() as nursery:
nursery.start_soon(sleep_forever)
nursery.start_soon(raise_error)
async with _core.open_nursery(strict_exception_groups=True) as nursery2:
nursery2.start_soon(sleep_forever)
nursery2.start_soon(raise_error)
nursery.cancel_scope.cancel()

exceptions = exc.value.exceptions
assert len(exceptions) == 2
assert isinstance(exceptions[0], RuntimeError)
assert isinstance(exceptions[1], MultiError)
assert len(exceptions[1].exceptions) == 1
assert isinstance(exceptions[1].exceptions[0], RuntimeError)


async def test_nursery_collapse_loose():
"""
Test that a single exception from a nested nursery with loose semantics gets
collapsed when CancelledErrors are stripped from it.
"""

async def raise_error():
raise RuntimeError("test error")

with pytest.raises(MultiError) as exc:
async with _core.open_nursery() as nursery:
nursery.start_soon(sleep_forever)
nursery.start_soon(raise_error)
async with _core.open_nursery() as nursery2:
nursery2.start_soon(sleep_forever)
nursery2.start_soon(raise_error)
nursery.cancel_scope.cancel()

exceptions = exc.value.exceptions
assert len(exceptions) == 2
assert isinstance(exceptions[0], RuntimeError)
assert isinstance(exceptions[1], RuntimeError)


async def test_cancel_scope_no_cancellederror():
"""
Test that when a cancel scope encounters an exception group that does NOT contain
a Cancelled exception, it will NOT set the ``cancelled_caught`` flag.
"""

with pytest.raises(ExceptionGroup):
with _core.CancelScope() as scope:
scope.cancel()
raise ExceptionGroup("test", [RuntimeError(), RuntimeError()])

assert not scope.cancelled_caught