Skip to content

Commit

Permalink
Monkeypatch Apport excepthook (#88)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakkdl committed Oct 31, 2023
1 parent f77f5b0 commit fae9d9a
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Version history

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

**UNRELEASED**

- Added special monkeypatching if `Apport <https://github.com/canonical/apport>`_ has
overridden ``sys.excepthook`` so it will format exception groups correctly
(PR by John Litborn)

**1.1.3**

- ``catch()`` now raises a ``TypeError`` if passed an async exception handler instead of
Expand Down
40 changes: 40 additions & 0 deletions src/exceptiongroup/_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,46 @@ def format_exception_only(self):
)
sys.excepthook = exceptiongroup_excepthook

# Ubuntu's system Python has a sitecustomize.py file that imports
# 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 getattr(sys.excepthook, "__name__", None) in (
"apport_excepthook",
# on ubuntu 22.10 the hook was renamed to partial_apport_excepthook
"partial_apport_excepthook",
):
# patch traceback like above
traceback.TracebackException.__init__ = ( # type: ignore[assignment]
PatchedTracebackException.__init__
)
traceback.TracebackException.format = ( # type: ignore[assignment]
PatchedTracebackException.format
)
traceback.TracebackException.format_exception_only = ( # type: ignore[assignment]
PatchedTracebackException.format_exception_only
)

from types import ModuleType

import apport_python_hook

assert sys.excepthook is apport_python_hook.apport_excepthook

# monkeypatch the sys module that apport has imported
fake_sys = ModuleType("exceptiongroup_fake_sys")
fake_sys.__dict__.update(sys.__dict__)
fake_sys.__excepthook__ = exceptiongroup_excepthook
apport_python_hook.sys = fake_sys


@singledispatch
def format_exception_only(__exc: BaseException) -> List[str]:
Expand Down
13 changes: 13 additions & 0 deletions tests/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 # noqa: E402 # unsorted import

apport_python_hook.install()

from exceptiongroup import ExceptionGroup # noqa: E402 # unsorted import

raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])
63 changes: 63 additions & 0 deletions tests/test_apport_monkeypatching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

import pytest

import exceptiongroup


def run_script(name: str) -> subprocess.CompletedProcess[bytes]:
exceptiongroup_path = Path(exceptiongroup.__file__).parent.parent
script_path = Path(__file__).parent / name

env = dict(os.environ)
print("parent PYTHONPATH:", env.get("PYTHONPATH"))
if "PYTHONPATH" in env: # pragma: no cover
pp = env["PYTHONPATH"].split(os.pathsep)
else:
pp = []

pp.insert(0, str(exceptiongroup_path))
pp.insert(0, str(script_path.parent))
env["PYTHONPATH"] = os.pathsep.join(pp)
print("subprocess PYTHONPATH:", env.get("PYTHONPATH"))

cmd = [sys.executable, "-u", str(script_path)]
print("running:", cmd)
completed = subprocess.run(
cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
print("process output:")
print(completed.stdout.decode("utf-8"))
return completed


@pytest.mark.skipif(
sys.version_info > (3, 11),
reason="No patching is done on Python >= 3.11",
)
@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")
file = Path(__file__).parent / "apport_excepthook.py"
assert stdout == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{file}", line 13, in <module>
| raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])
| exceptiongroup.ExceptionGroup: msg1 (2 sub-exceptions)
+-+---------------- 1 ----------------
| KeyError: 'msg2'
+---------------- 2 ----------------
| ValueError: msg3
+------------------------------------
"""
)

0 comments on commit fae9d9a

Please sign in to comment.