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
4 changes: 2 additions & 2 deletions better_exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def write_stream(data, stream=STREAM):
stream.write(data)


def format_exception(exc, value, tb):
def format_exception(exc, value, tb, colored=SUPPORTS_COLOR):
# Rebuild each time to take into account any changes made by the user to the global parameters
formatter = ExceptionFormatter(colored=SUPPORTS_COLOR, theme=THEME, max_length=MAX_LENGTH,
formatter = ExceptionFormatter(colored=colored, theme=THEME, max_length=MAX_LENGTH,
pipe_char=PIPE_CHAR, cap_char=CAP_CHAR)
return list(formatter.format_exception(exc, value, tb))

Expand Down
85 changes: 75 additions & 10 deletions better_exceptions/log.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,89 @@
from __future__ import absolute_import

from copy import copy
import sys

from logging import Logger, StreamHandler
from logging import FileHandler, Logger, StreamHandler


FORMATTER_PATCHED = '_betexc_patched'
FORMATTER_COLORED = '_betexc_colored'


def _make_logging_format_exception(colored):
def logging_format_exception(exc_info):
from . import format_exception

exc, value, tb = exc_info
restore_args = exc is AssertionError and value is not None
args = value.args if restore_args else None

try:
return u''.join(format_exception(exc, value, tb, colored=colored))
finally:
if restore_args:
value.args = args

return logging_format_exception


def _clone_formatter(formatter):
formatter = copy(formatter)

if getattr(formatter, FORMATTER_PATCHED, False):
for name in ('format', 'formatException', FORMATTER_PATCHED, FORMATTER_COLORED):
if name in formatter.__dict__:
delattr(formatter, name)

return formatter


def _patch_formatter(formatter, colored):
if (getattr(formatter, FORMATTER_PATCHED, False) and
getattr(formatter, FORMATTER_COLORED, None) == colored):
return formatter

formatter = _clone_formatter(formatter)
format_record = formatter.format

def format(record):
exc_text = record.exc_text
record.exc_text = None
try:
return format_record(record)
finally:
record.exc_text = exc_text

formatter.formatException = _make_logging_format_exception(colored)
formatter.format = format
setattr(formatter, FORMATTER_PATCHED, True)
setattr(formatter, FORMATTER_COLORED, colored)
return formatter


def _handler_supports_color(handler):
return isinstance(handler, StreamHandler) and handler.stream == sys.stderr


def _should_patch_handler(handler):
return isinstance(handler, FileHandler) or _handler_supports_color(handler)


def patch():
import logging
from . import format_exception

logging_format_exception = lambda exc_info: u''.join(format_exception(*exc_info))
from . import SUPPORTS_COLOR

if hasattr(logging, '_defaultFormatter'):
logging._defaultFormatter.format_exception = logging_format_exception
logging._defaultFormatter = _patch_formatter(logging._defaultFormatter, SUPPORTS_COLOR)

for handler_ref in logging._handlerList:
handler = handler_ref()
if not _should_patch_handler(handler):
continue

patchables = [handler() for handler in logging._handlerList if isinstance(handler(), StreamHandler)]
patchables = [handler for handler in patchables if handler.stream == sys.stderr]
patchables = [handler for handler in patchables if handler.formatter is not None]
for handler in patchables:
handler.formatter.formatException = logging_format_exception
colored = SUPPORTS_COLOR and _handler_supports_color(handler)
formatter = handler.formatter or logging._defaultFormatter
handler.setFormatter(_patch_formatter(formatter, colored))


class BetExcLogger(Logger):
Expand Down
81 changes: 81 additions & 0 deletions test/test_file_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from __future__ import absolute_import

import logging
import os
import re
import sys
import tempfile

try:
from io import StringIO
except ImportError:
from StringIO import StringIO

import better_exceptions


ANSI_ESCAPE = re.compile(r'\x1b\[[0-9;]*m')


def fail():
value = 52
assert value == 90


def check_handler_order(stream_first):
better_exceptions.SUPPORTS_COLOR = True

logger = logging.getLogger('better_exceptions_file_logging_test_{0}'.format(stream_first))
logger.setLevel(logging.ERROR)
logger.propagate = False

old_stderr = sys.stderr
stream = StringIO()
sys.stderr = stream
stream_handler = logging.StreamHandler()

fd, path = tempfile.mkstemp()
os.close(fd)
file_handler = logging.FileHandler(path)

shared_formatter = logging.Formatter()
stream_handler.setFormatter(shared_formatter)
file_handler.setFormatter(shared_formatter)

handlers = [stream_handler, file_handler] if stream_first else [file_handler, stream_handler]
for handler in handlers:
logger.addHandler(handler)

try:
better_exceptions.hook()
try:
fail()
except AssertionError:
logger.exception('callback failed')

for handler in handlers:
handler.flush()

stream_output = stream.getvalue()
with open(path, 'r') as log:
file_output = log.read()

assert ANSI_ESCAPE.search(stream_output), stream_output
assert not ANSI_ESCAPE.search(file_output), file_output
assert 'assert value == 90' in file_output
finally:
sys.stderr = old_stderr
logger.removeHandler(stream_handler)
logger.removeHandler(file_handler)
stream_handler.close()
file_handler.close()
os.remove(path)


def main():
check_handler_order(stream_first=True)
check_handler_order(stream_first=False)


if __name__ == '__main__':
main()
7 changes: 7 additions & 0 deletions test_all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ function test_case {
return $?
}

function assert_case {
echo -e "\x1b[36;1m " "$@" "\x1b[m" 1>&2

"$@"
}

function test_all {
test_case "$PYTHON" "test/test.py"
test_case "$PYTHON" "test/test_color.py"
Expand All @@ -45,6 +51,7 @@ function test_all {
# test_case "./test/test_interactive_raw.sh"
test_case "./test/test_string.sh"
test_case "$PYTHON" "test/test_logging.py"
assert_case "$PYTHON" "test/test_file_logging.py"
test_case "$PYTHON" "test/test_truncating.py"
test_case "$PYTHON" "test/test_truncating_disabled.py"
test_case "$PYTHON" "test/test_indentation_error.py"
Expand Down