Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ Type "help", "copyright", "credits" or "license" for more information.

in order to drop into a `better_exceptions`-enabled Python interactive shell.

### IPython

IPython handles tracebacks without using `sys.excepthook`, so the normal
`BETTER_EXCEPTIONS=1` hook is not enough once the IPython shell is already
running. Load the bundled extension to use `better_exceptions` for IPython
tracebacks:

```ipython
In [1]: %load_ext better_exceptions
```

You can also call `better_exceptions.hook()` from an IPython startup file; when
it detects an active IPython shell, it will install the same traceback hook.

### Advanced Usage

If you want to allow the entirety of values to be outputted instead of being truncated to a certain amount of characters:
Expand Down
13 changes: 13 additions & 0 deletions better_exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,20 @@ def hook():
logging.setLoggerClass(BetExcLogger)
patch_logging()

from .integrations.ipython import install as install_ipython
install_ipython()

if hasattr(sys, 'ps1'):
print('WARNING: better_exceptions will only inspect code from the command line\n'
' when using: `python -m better_exceptions\'. Otherwise, only code\n'
' loaded from files will be inspected!', file=sys.stderr)


def load_ipython_extension(ipython):
from .integrations.ipython import load_ipython_extension as load
load(ipython)


def unload_ipython_extension(ipython):
from .integrations.ipython import unload_ipython_extension as unload
unload(ipython)
111 changes: 111 additions & 0 deletions better_exceptions/integrations/ipython.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import absolute_import

import sys
import types

from .. import excepthook


ORIGINAL_SHOWTRACEBACK_ATTR = '_better_exceptions_showtraceback'


def _get_running_ipython():
try:
from IPython import get_ipython
except ImportError:
try:
import builtins
except ImportError:
import __builtin__ as builtins

get_ipython = getattr(builtins, 'get_ipython', None)

return get_ipython() if get_ipython is not None else None


def _bind(method, instance):
try:
return types.MethodType(method, instance)
except TypeError:
return types.MethodType(method, instance, type(instance))


def _delegate(old_showtraceback, exc_tuple, filename, tb_offset,
exception_only, kwargs):
try:
return old_showtraceback(exc_tuple, filename, tb_offset,
exception_only, **kwargs)
except TypeError:
if kwargs:
return old_showtraceback(exc_tuple, filename, tb_offset,
exception_only)
raise


def _normalize_exc_info(ipython, exc_tuple):
if exc_tuple is None:
exc_tuple = sys.exc_info()

if hasattr(ipython, '_get_exc_info'):
return ipython._get_exc_info(exc_tuple)

return exc_tuple


def _can_format_with_better_exceptions(exc_type, filename, tb_offset,
exception_only):
if filename is not None or tb_offset is not None or exception_only:
return False

try:
if issubclass(exc_type, SyntaxError):
return False
except TypeError:
return False

return True


def _showtraceback(self, exc_tuple=None, filename=None, tb_offset=None,
exception_only=False, **kwargs):
original_exc_tuple = exc_tuple
exc_type, value, traceback = _normalize_exc_info(self, exc_tuple)
old_showtraceback = getattr(self, ORIGINAL_SHOWTRACEBACK_ATTR)

if _can_format_with_better_exceptions(exc_type, filename, tb_offset,
exception_only):
return excepthook(exc_type, value, traceback)

return _delegate(old_showtraceback, original_exc_tuple, filename,
tb_offset, exception_only, kwargs)


def install(ipython=None):
ipython = ipython or _get_running_ipython()
if ipython is None:
return False

if hasattr(ipython, ORIGINAL_SHOWTRACEBACK_ATTR):
return True

setattr(ipython, ORIGINAL_SHOWTRACEBACK_ATTR, ipython.showtraceback)
ipython.showtraceback = _bind(_showtraceback, ipython)
return True


def uninstall(ipython=None):
ipython = ipython or _get_running_ipython()
if ipython is None or not hasattr(ipython, ORIGINAL_SHOWTRACEBACK_ATTR):
return False

ipython.showtraceback = getattr(ipython, ORIGINAL_SHOWTRACEBACK_ATTR)
delattr(ipython, ORIGINAL_SHOWTRACEBACK_ATTR)
return True


def load_ipython_extension(ipython):
install(ipython)


def unload_ipython_extension(ipython):
uninstall(ipython)
79 changes: 79 additions & 0 deletions test/test_ipython.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import io
import sys

import better_exceptions
from better_exceptions.integrations import ipython


class FakeIPython(object):
def __init__(self):
self.delegated = []

def _get_exc_info(self, exc_tuple):
return exc_tuple

def showtraceback(self, exc_tuple=None, filename=None, tb_offset=None,
exception_only=False, **kwargs):
self.delegated.append((exc_tuple, filename, tb_offset,
exception_only, kwargs))
return 'delegated'


def raise_runtime_error():
value = 42
raise RuntimeError('boom {}'.format(value))


def test_better_exceptions_traceback():
stream = io.StringIO()

old_stream = better_exceptions.STREAM
old_should_encode = better_exceptions.SHOULD_ENCODE
old_supports_color = better_exceptions.SUPPORTS_COLOR

better_exceptions.STREAM = stream
better_exceptions.SHOULD_ENCODE = False
better_exceptions.SUPPORTS_COLOR = False

try:
shell = FakeIPython()
assert ipython.install(shell) is True
assert ipython.install(shell) is True

try:
raise_runtime_error()
except RuntimeError:
shell.showtraceback(sys.exc_info(), running_compiled_code=False)

output = stream.getvalue()
assert 'RuntimeError: boom 42' in output
assert 'raise RuntimeError' in output
assert not shell.delegated

assert ipython.uninstall(shell) is True
assert shell.showtraceback() == 'delegated'
assert len(shell.delegated) == 1

shell = FakeIPython()
better_exceptions.load_ipython_extension(shell)
assert hasattr(shell, ipython.ORIGINAL_SHOWTRACEBACK_ATTR)
better_exceptions.unload_ipython_extension(shell)
assert not hasattr(shell, ipython.ORIGINAL_SHOWTRACEBACK_ATTR)
finally:
better_exceptions.STREAM = old_stream
better_exceptions.SHOULD_ENCODE = old_should_encode
better_exceptions.SUPPORTS_COLOR = old_supports_color


def test_delegates_syntax_errors():
shell = FakeIPython()
assert ipython.install(shell) is True

exc_tuple = (SyntaxError, SyntaxError('bad'), None)
assert shell.showtraceback(exc_tuple) == 'delegated'
assert shell.delegated[0][0] == exc_tuple


if __name__ == '__main__':
test_better_exceptions_traceback()
test_delegates_syntax_errors()