Sharp edges
This approach will crash your program. One way or another. I don’t know how. Just be careful!
You can monkey-patch the Exception type itself, kind of — you can’t patch the object, since it’s immutable. Instead, you need to create a new class that mirrors it with your PDB hook inside, then patch that class into the place of Exception in the builtin module.
Then, you’ll also need to do the same for the entire exception tree, doing it module-by-module, in order to catch all types of exceptions which may already be imported and live objects in the process.
There’s a few things important to note when using this approach:
- You are NOT in a proper
pdb.pm()/pdb.post_mortem()frame here! You are in apdb.set_trace()that just happened to land withinException.__new__.- But you absolutely should act like you are!
- Typically, the first thing you should do is
upto go up one frame to where the exception is actually being thrown. - Don’t try to continue execution — you’ll just throw the exception, unwind the stack, and everything dies.
- I don’t know how to safely undo this. Once you’re done debugging, just let the program die and relaunch the whole process.
#!python
# -*- coding: utf-8 -*-
"""
Super exception hook 9000.
"""
import builtins
import pdb
import re
import sys
import traceback
import warnings
import testmod
_ORIGINAL_EXCEPTION = Exception
_DONT_PATCH = [ "StopIteration", "MemoryError", "PatchedExc", "PatchedSub",
"BdbQuit", "Warning" ]
def stdlib_antiexc_guards():
indent = " "
for i in range(10):
re.compile(f"(?m)^{indent*i}")
sys.ps1 = ">>> "
sys.ps2 = "... "
warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning))
traceback._extract_caret_anchors_from_line_segment = lambda s: None
class PatchedExc(_ORIGINAL_EXCEPTION):
def __new__(cls, *args):
inst = _ORIGINAL_EXCEPTION.__new__(cls, *args)
print("In patched __new__")
pdb.set_trace()
return inst
def install_hook_subclasses(cls, level):
indent = " " * level
indentm1 = " " * (level - 1)
subs = cls.__subclasses__()
if len(subs):
print(f"{indentm1}recurse {cls}")
for sub in subs:
if sub.__name__ in _DONT_PATCH:
print(f"{indent}skip {sub.__name__}; blacklist")
continue
if sub.__module__ not in sys.modules:
print(f"{indent}skip {sub.__name__}; not in sys.modules")
continue
orig_bases = sub.__bases__
new_bases = []
for orig in orig_bases:
mod = sys.modules[orig.__module__]
new = getattr(mod, orig.__name__)
new_bases.append(new)
class PatchedSub(*new_bases[:-1], PatchedExc):
__name__ = sub.__name__
__qualname__ = sub.__qualname__
__module__ = sub.__module__
pass
namespace = sys.modules[sub.__module__]
namespace.__setattr__(sub.__name__, PatchedSub)
print(f"{indent}patch {sub.__module__}.{sub.__name__}")
install_hook_subclasses(sub, level + 1)
def install_hook():
stdlib_antiexc_guards()
builtins.Exception = PatchedExc
install_hook_subclasses(_ORIGINAL_EXCEPTION, 1)
def main():
install_hook()
print('\nInstalled patched exceptions\n')
testmod.main()
if __name__ == '__main__':
main()Technical notes (“wtf was that??“):
_DONT_PATCH:- Exceptions you don’t want to patch. Add names to this list if you want to skip custom types.
- At minimum, keep
StopIteration,PatchedExc,PatchedSub,BdbQuit, andWarning.
install_hook()- Main entry point to install hooks. This is what you should call in your “I would like to set debugger hooks, please” code.
install_hook_subclasses- Recurses the exception hierarchy, patching subclasses of
Exceptionand overwriting them in their respective modules. This is the most important part.
- Recurses the exception hierarchy, patching subclasses of
stdlib_antiexc_guards- This function monkey-patches various standard library pieces to prevent low-level internal try/catch loops from tripping an exception. Because we’re patching all exceptions, even the caught ones, any try/catch loop that occurs needs to not meet the error conditions.
- This function has to be called before any exception patching takes place — hence the first thing in the
install_hook()function. - If your module uses a lot of regular expressions, you may need to increase the
re._MAXCACHE(default 512) andre._MAXCACHE2(default 256) sizes.
You can test this out with the accompanying testmod.py:
#!python
# -*- coding: utf-8 -*-
"""
Test module for catching exceptions.
"""
import random
import sys
import traceback
class CustomException(ValueError):
pass
def foo():
guessme = random.randint(1, 10)
try:
raise CustomException("Didn't catch at all!")
except Exception:
print("Hit normal exception handler in try-catch")
def bar():
raise CustomException("Unhandled exception")
def ovrd_excepthook(typ, value, tb):
traceback.print_tb(tb)
import pdb; pdb.set_trace()
print("Hit sys.excepthook")
def install_excepthook():
sys.excepthook = ovrd_excepthook
def uninstall_excepthook():
sys.excepthook = sys.__excepthook__
def main():
print("Installing excepthook")
install_excepthook()
print("Caught exc with excepthook set")
foo()
print("Uninstalling excepthook")
uninstall_excepthook()
print("Caught exc with excepthook unset")
foo()
print("Uncaught exc with excepthook unset")
bar()
print("Done")
if __name__ == '__main__':
main()See also
I got this idea from this SO answer: https://stackoverflow.com/a/1029619 . Thanks, Cosmin Stejerean!