четверг, 4 августа 2011 г.

Python: делаем информативный вывод исключений

Обновление исправлен ошибка в случае если появляется исключение UnicodeDecode/Encode, в этом случае возвращается repr представление проблемной строки.

Нередко, получая исключительную ситуацию при работе с программой, мы не в силах сходу определить что же произошло не так как задумывалось, особенно когда отображаемая информация содержит в себе лишь немного полезного. Увидев как подается исключительная ситуация в фрейворке Django, мне захотелось так же организовать вывод и в своем коде. Оттуда был взять фрагмент кода, находящегося в модуле views/debug.py, и переделан под собственные нужды.

Итак, ниже представлен тестовый фрагмент кода, в котором произойдет ошибка.

# -*- encoding: utf-8 -*-

import traceback, datetime, sys, re

def error_func(x,y):
    return (x+y)/(x-y)

class A(object):
    def a(self):
        a1=1
        a2=1
        return error_func(a1, a2)
    
try:
    a=A()
    a.a()
except Exception:
    # здесь наш обработчик

Если бы данный код не был облечен в инструкции обработки исключений, то интерпретатор быдал бы следующее:

Traceback (most recent call last):
  File "C:\Documents and Settings\mer\workspace\test\src\a.py", line 125, in
    a.a()
  File "C:\Documents and Settings\mer\workspace\test\src\a.py", line 121, in a
    return error_func(a1, a2)
  File "C:\Documents and Settings\mer\workspace\test\src\a.py", line 115, in error_func
    return (x+y)/(x-y)
ZeroDivisionError: integer division or modulo by zero

Если такая информация выглядит скудной, то исправит положение определяемая ниже функция get_tb_full()

Функция get_tb() возвращает строку с информацией об исключении как это делает по умолчанию интерпретатор (пример выше). Такая функция может пригодиться если необходимо далее каким либо образом обработать полученную строку.

def get_tb():
    tmp=traceback.format_exception(sys.exc_info()[0],
                                sys.exc_info()[1],
                                sys.exc_info()[2],)
    return ''.join(tmp)

Функция get_tb_full() так же возвращает строку в которой и привидена более полная информация об исключении. Параметр message - сообщение которое будет прикрепленно к выводу, only_last - будет выведен только самый нижний уровень, тот на котором непосредственно возникла исключительная ситуация.

def get_tb_full(message='', only_last=False):
    def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None):
        """
        Returns context_lines before and after lineno from file.
        Returns (pre_context_lineno, pre_context, context_line, post_context).
        """
        source=None
        if loader is not None and hasattr(loader, "get_source"):
            source=loader.get_source(module_name)
            if source is not None:
                source=source.splitlines()
        if source is None:
            try:
                f=open(filename)
                try:
                    source=f.readlines()
                finally:
                    f.close()
            except (OSError, IOError):
                pass
        if source is None:
            return None, [], None, []
    
        encoding='ascii'
        for line in source[:2]:
            # File coding may be specified. Match pattern from PEP-263
            # (http://www.python.org/dev/peps/pep-0263/)
            match=re.search(r'coding[:=]\s*([-\w.]+)', line)
            if match:
                encoding=match.group(1)
                break
        source=[unicode(sline, encoding, 'replace') for sline in source]
    
        lower_bound=max(0, lineno - context_lines)
        upper_bound=lineno + context_lines
    
        pre_context=[line.strip('\n') for line in source[lower_bound:lineno]]
        context_line=source[lineno].strip('\n')
        post_context=[line.strip('\n') for line in source[lineno+1:upper_bound]]

        return lower_bound, pre_context, context_line, post_context
    
    date_and_time=datetime.datetime.now()
    frames=[]
    tb=sys.exc_info()[-1]
    
    # перебираем уровни которые приняли сигнал об исключительной ситуации
    # заполняя список frames словарями с необходимми аттрибутами
    while tb is not None:
        # support for __traceback_hide__ which is used by a few libraries
        # to hide internal frames.
        if tb.tb_frame.f_locals.get('__traceback_hide__'):
            tb=tb.tb_next
            continue
        filename=tb.tb_frame.f_code.co_filename
        function=tb.tb_frame.f_code.co_name
        lineno=tb.tb_lineno - 1
        loader=tb.tb_frame.f_globals.get('__loader__')
        module_name=tb.tb_frame.f_globals.get('__name__')
        pre_context_lineno, pre_context, context_line, post_context=_get_lines_from_file(filename, lineno, 7, loader, module_name)
        if pre_context_lineno is not None:
            frames.append({
                'tb': tb,
                'filename': filename,
                'function': function,
                'lineno': lineno + 1,
                'vars': tb.tb_frame.f_locals.items(),
                'id': id(tb),
                'pre_context': pre_context,
                'context_line': context_line,
                'post_context': post_context,
                'pre_context_lineno': pre_context_lineno + 1,
                'module':module_name
            })
        tb=tb.tb_next
    
    
    frames_len_orig=range(len(frames)-1, -1, -1)
    if only_last:
        frames=list(frames[-1:])
    
    # заносим в результирующий список строки с выводом
    result=[]
    result.append('='*100)
    result.append('%s %s'%(date_and_time, message))
    result.append('Exception raised: %s %s'%sys.exc_info()[:2])
    for i,item in enumerate(frames):
        result.append('-'*100)
        result.append('depth level=%i. %s in module "%s"'%(frames_len_orig[i-len(frames)], item['tb'], item['module']))
        result.append('filename "%s" function "%s" in string %s:'%(item['filename'], item['function'], item['lineno']))
        code=item['pre_context']+[item['context_line']]+item['post_context']
        for y,value in enumerate(code):
            result.append('\t%i| %s'%(y+item['pre_context_lineno'],value))
        result.append('local variables:')
        for y in item['vars']:
            result.append('\t\t%s=%s'%y)
    result.append('='*100)
    
    out=[]
    for i in result:
        try:
            t=unicode(i)
        except Exception:
            t=repr(i)
        out.append(t)
    # преобразуем список строк в строку с разделитем переноса строки
    return '\r\n'.join(out)+\r\n

Если в качестве обработчика в тестовом коде мы используем:

get_tb_full('This is happen:(', False)
то получим исчерпывающий вывод по всем уровням через которое прошло исключение:
====================================================================================================
2011-08-04 12:20:12.211000 This is happen:(
Exception raised:  integer division or modulo by zero
----------------------------------------------------------------------------------------------------
depth level=2.  in module "__main__"
filename "C:\Documents and Settings\mer\workspace\test\src\a.py" function "" in string 125:
 118|     def a(self):
 119|         a1=1
 120|         a2=1
 121|         return error_func(a1, a2)
 122|     
 123| try:
 124|     a=A()
 125|     a.a()
 126| except Exception:
 127|     print get_tb2('This is happen:(', False)
 128|     print
 129|     print get_tb2(only_last=True)  
 130|     print
 131|     print get_tb()
local variables:
  A=
  a=<__main__.A object at 0x00B64770>
  get_tb=
  error_func=
  __builtins__=
  __file__=C:\Documents and Settings\mer\workspace\test\src\a.py
  traceback=
  __package__=None
  sys=
  re=
  __name__=__main__
  get_tb2=
  datetime=
  __doc__=None
----------------------------------------------------------------------------------------------------
depth level=1.  in module "__main__"
filename "C:\Documents and Settings\mer\workspace\test\src\a.py" function "a" in string 121:
 114| def error_func(x,y):
 115|     return (x+y)/(x-y)
 116| 
 117| class A(object):
 118|     def a(self):
 119|         a1=1
 120|         a2=1
 121|         return error_func(a1, a2)
 122|     
 123| try:
 124|     a=A()
 125|     a.a()
 126| except Exception:
 127|     print get_tb2('This is happen:(', False)
local variables:
  a1=1
  self=<__main__.A object at 0x00B64770>
  a2=1
----------------------------------------------------------------------------------------------------
depth level=0.  in module "__main__"
filename "C:\Documents and Settings\mer\workspace\test\src\a.py" function "error_func" in string 115:
 108|     result.append('='*100)
 109|     
 110|     # преобразуем список строк в строку с разделитем переноса строки
 111|     return '\n'.join(result)
 112| 
 113| 
 114| def error_func(x,y):
 115|     return (x+y)/(x-y)
 116| 
 117| class A(object):
 118|     def a(self):
 119|         a1=1
 120|         a2=1
 121|         return error_func(a1, a2)
local variables:
  y=1
  x=1
====================================================================================================

Модифицируя последнюю часть кода функции get_tb_full() можно настроить вывод по своему вкусу.

1 комментарий:

  1. Вы представляете как это будет выглядеть со стеком в 20-30 вызовов )) А вообще действительно удобно. Иногда жалею, что не везде есть страницы ошибки, как в Django ))

    ОтветитьУдалить