Skip to content

Commit fae9d9a

Browse files
authored
Monkeypatch Apport excepthook (#88)
1 parent f77f5b0 commit fae9d9a

File tree

4 files changed

+122
-0
lines changed

4 files changed

+122
-0
lines changed

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ Version history
33

44
This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
55

6+
**UNRELEASED**
7+
8+
- Added special monkeypatching if `Apport <https://github.com/canonical/apport>`_ has
9+
overridden ``sys.excepthook`` so it will format exception groups correctly
10+
(PR by John Litborn)
11+
612
**1.1.3**
713

814
- ``catch()`` now raises a ``TypeError`` if passed an async exception handler instead of

src/exceptiongroup/_formatting.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,46 @@ def format_exception_only(self):
359359
)
360360
sys.excepthook = exceptiongroup_excepthook
361361

362+
# Ubuntu's system Python has a sitecustomize.py file that imports
363+
# apport_python_hook and replaces sys.excepthook.
364+
#
365+
# The custom hook captures the error for crash reporting, and then calls
366+
# sys.__excepthook__ to actually print the error.
367+
#
368+
# We don't mind it capturing the error for crash reporting, but we want to
369+
# take over printing the error. So we monkeypatch the apport_python_hook
370+
# module so that instead of calling sys.__excepthook__, it calls our custom
371+
# hook.
372+
#
373+
# More details: https://github.com/python-trio/trio/issues/1065
374+
if getattr(sys.excepthook, "__name__", None) in (
375+
"apport_excepthook",
376+
# on ubuntu 22.10 the hook was renamed to partial_apport_excepthook
377+
"partial_apport_excepthook",
378+
):
379+
# patch traceback like above
380+
traceback.TracebackException.__init__ = ( # type: ignore[assignment]
381+
PatchedTracebackException.__init__
382+
)
383+
traceback.TracebackException.format = ( # type: ignore[assignment]
384+
PatchedTracebackException.format
385+
)
386+
traceback.TracebackException.format_exception_only = ( # type: ignore[assignment]
387+
PatchedTracebackException.format_exception_only
388+
)
389+
390+
from types import ModuleType
391+
392+
import apport_python_hook
393+
394+
assert sys.excepthook is apport_python_hook.apport_excepthook
395+
396+
# monkeypatch the sys module that apport has imported
397+
fake_sys = ModuleType("exceptiongroup_fake_sys")
398+
fake_sys.__dict__.update(sys.__dict__)
399+
fake_sys.__excepthook__ = exceptiongroup_excepthook
400+
apport_python_hook.sys = fake_sys
401+
362402

363403
@singledispatch
364404
def format_exception_only(__exc: BaseException) -> List[str]:

tests/apport_excepthook.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# The apport_python_hook package is only installed as part of Ubuntu's system
2+
# python, and not available in venvs. So before we can import it we have to
3+
# make sure it's on sys.path.
4+
import sys
5+
6+
sys.path.append("/usr/lib/python3/dist-packages")
7+
import apport_python_hook # noqa: E402 # unsorted import
8+
9+
apport_python_hook.install()
10+
11+
from exceptiongroup import ExceptionGroup # noqa: E402 # unsorted import
12+
13+
raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])

tests/test_apport_monkeypatching.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
import pytest
9+
10+
import exceptiongroup
11+
12+
13+
def run_script(name: str) -> subprocess.CompletedProcess[bytes]:
14+
exceptiongroup_path = Path(exceptiongroup.__file__).parent.parent
15+
script_path = Path(__file__).parent / name
16+
17+
env = dict(os.environ)
18+
print("parent PYTHONPATH:", env.get("PYTHONPATH"))
19+
if "PYTHONPATH" in env: # pragma: no cover
20+
pp = env["PYTHONPATH"].split(os.pathsep)
21+
else:
22+
pp = []
23+
24+
pp.insert(0, str(exceptiongroup_path))
25+
pp.insert(0, str(script_path.parent))
26+
env["PYTHONPATH"] = os.pathsep.join(pp)
27+
print("subprocess PYTHONPATH:", env.get("PYTHONPATH"))
28+
29+
cmd = [sys.executable, "-u", str(script_path)]
30+
print("running:", cmd)
31+
completed = subprocess.run(
32+
cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
33+
)
34+
print("process output:")
35+
print(completed.stdout.decode("utf-8"))
36+
return completed
37+
38+
39+
@pytest.mark.skipif(
40+
sys.version_info > (3, 11),
41+
reason="No patching is done on Python >= 3.11",
42+
)
43+
@pytest.mark.skipif(
44+
not Path("/usr/lib/python3/dist-packages/apport_python_hook.py").exists(),
45+
reason="need Ubuntu with python3-apport installed",
46+
)
47+
def test_apport_excepthook_monkeypatch_interaction():
48+
completed = run_script("apport_excepthook.py")
49+
stdout = completed.stdout.decode("utf-8")
50+
file = Path(__file__).parent / "apport_excepthook.py"
51+
assert stdout == (
52+
f"""\
53+
+ Exception Group Traceback (most recent call last):
54+
| File "{file}", line 13, in <module>
55+
| raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])
56+
| exceptiongroup.ExceptionGroup: msg1 (2 sub-exceptions)
57+
+-+---------------- 1 ----------------
58+
| KeyError: 'msg2'
59+
+---------------- 2 ----------------
60+
| ValueError: msg3
61+
+------------------------------------
62+
"""
63+
)

0 commit comments

Comments
 (0)