Skip to content

Commit

Permalink
#11638 manhole vs excepthook (#11639)
Browse files Browse the repository at this point in the history
Always install our own excepthook that sends tracebacks reports
to the right place.
  • Loading branch information
exarkun committed Sep 6, 2022
2 parents d415545 + 9d2a558 commit 3f52190
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 23 deletions.
32 changes: 23 additions & 9 deletions src/twisted/conch/manhole.py
Expand Up @@ -17,11 +17,15 @@
import sys
import tokenize
from io import BytesIO
from traceback import format_exception
from types import TracebackType
from typing import Type

from twisted.conch import recvline
from twisted.internet import defer
from twisted.python.compat import _get_async_param
from twisted.python.htmlizer import TokenPrinter
from twisted.python.monkey import MonkeyPatcher


class FileWrapper:
Expand Down Expand Up @@ -71,6 +75,11 @@ def __init__(self, handler, locals=None, filename="<console>"):
self.filename = filename
self.resetBuffer()

self.monkeyPatcher = MonkeyPatcher()
self.monkeyPatcher.addPatch(sys, "displayhook", self.displayhook)
self.monkeyPatcher.addPatch(sys, "excepthook", self.excepthook)
self.monkeyPatcher.addPatch(sys, "stdout", FileWrapper(self.handler))

def resetBuffer(self):
"""
Reset the input buffer.
Expand Down Expand Up @@ -104,15 +113,20 @@ def push(self, line):
return more

def runcode(self, *a, **kw):
orighook, sys.displayhook = sys.displayhook, self.displayhook
try:
origout, sys.stdout = sys.stdout, FileWrapper(self.handler)
try:
code.InteractiveInterpreter.runcode(self, *a, **kw)
finally:
sys.stdout = origout
finally:
sys.displayhook = orighook
with self.monkeyPatcher:
code.InteractiveInterpreter.runcode(self, *a, **kw)

def excepthook(
self,
excType: Type[BaseException],
excValue: BaseException,
excTraceback: TracebackType,
) -> None:
"""
Format exception tracebacks and write them to the output handler.
"""
lines = format_exception(excType, excValue, excTraceback.tb_next)
self.write("".join(lines))

def displayhook(self, obj):
self.locals["_"] = obj
Expand Down
1 change: 1 addition & 0 deletions src/twisted/conch/newsfragments/11638.bugfix
@@ -0,0 +1 @@
twisted.conch.manhole.ManholeInterpreter now captures tracebacks even if sys.excepthook has been modified.
19 changes: 14 additions & 5 deletions src/twisted/conch/test/test_manhole.py
Expand Up @@ -8,6 +8,7 @@
Tests for L{twisted.conch.manhole}.
"""

import sys
import traceback
from typing import Optional

Expand Down Expand Up @@ -148,9 +149,6 @@ def test_identicalOutput(self):
class ManholeLoopbackMixin:
serverProtocol = manhole.ColoredManhole

def wfd(self, d):
return defer.waitForDeferred(d)

def test_SimpleExpression(self):
"""
Evaluate simple expression.
Expand Down Expand Up @@ -244,10 +242,21 @@ def finished(ign):
+ defaultFunctionName.encode("utf-8"),
b"Exception: foo bar baz",
b">>> done",
]
],
)

return done.addCallback(finished)
done.addCallback(finished)
return done

def test_ExceptionWithCustomExcepthook(
self,
):
"""
Raised exceptions are handled the same way even if L{sys.excepthook}
has been modified from its original value.
"""
self.patch(sys, "excepthook", lambda *args: None)
return self.test_Exception()

def test_ControlC(self):
"""
Expand Down
10 changes: 1 addition & 9 deletions src/twisted/conch/test/test_recvline.py
Expand Up @@ -473,15 +473,7 @@ class _BaseMixin:
def _assertBuffer(self, lines):
receivedLines = self.recvlineClient.__bytes__().splitlines()
expectedLines = lines + ([b""] * (self.HEIGHT - len(lines) - 1))
self.assertEqual(len(receivedLines), len(expectedLines))
for i in range(len(receivedLines)):
self.assertEqual(
receivedLines[i],
expectedLines[i],
b"".join(receivedLines[max(0, i - 1) : i + 1])
+ b" != "
+ b"".join(expectedLines[max(0, i - 1) : i + 1]),
)
self.assertEqual(receivedLines, expectedLines)

def _trivialTest(self, inputLine, output):
done = self.recvlineClient.expect(b"done")
Expand Down
5 changes: 5 additions & 0 deletions src/twisted/python/monkey.py
Expand Up @@ -48,6 +48,8 @@ def patch(self):
self._originals.append((obj, name, getattr(obj, name)))
setattr(obj, name, value)

__enter__ = patch

def restore(self):
"""
Restore all original values to any patched objects.
Expand All @@ -56,6 +58,9 @@ def restore(self):
obj, name, value = self._originals.pop()
setattr(obj, name, value)

def __exit__(self, excType=None, excValue=None, excTraceback=None):
self.restore()

def runWithPatches(self, f, *args, **kw):
"""
Apply each patch already specified. Then run the function f with the
Expand Down
19 changes: 19 additions & 0 deletions src/twisted/test/test_monkey.py
Expand Up @@ -152,3 +152,22 @@ def _():
self.assertRaises(RuntimeError, self.monkeyPatcher.runWithPatches, _)
self.assertEqual(self.testObject.foo, self.originalObject.foo)
self.assertEqual(self.testObject.bar, self.originalObject.bar)

def test_contextManager(self):
"""
L{MonkeyPatcher} is a context manager that applies its patches on
entry and restore original values on exit.
"""
self.monkeyPatcher.addPatch(self.testObject, "foo", "patched value")
with self.monkeyPatcher:
self.assertEqual(self.testObject.foo, "patched value")
self.assertEqual(self.testObject.foo, self.originalObject.foo)

def test_contextManagerPropagatesExceptions(self):
"""
Exceptions propagate through the L{MonkeyPatcher} context-manager
exit method.
"""
with self.assertRaises(RuntimeError):
with self.monkeyPatcher:
raise RuntimeError("something")

0 comments on commit 3f52190

Please sign in to comment.