Почему я не могу получить полную трассировку из сохраненного исключения - и как получить и сохранить полную трассировку?

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

Интерфейс валидации повторно использует код загрузки, что хорошо для повторного использования кода. Он имеет "режим валидации", который не изменяет базу данных. Все находится в атомарном блоке транзакций, и он откатывается в любом случае, когда работает в режиме валидации.

Я нахожусь в середине рефактора, чтобы облегчить проблему. Проблема заключается в том, что пользователю приходится отправлять файлы несколько раз, каждый раз получая очередную ошибку. Поэтому я дорабатываю код, чтобы он мог "буферизировать" исключения в массиве и останавливаться, только если какая-либо ошибка делает дальнейшую обработку невозможной. Пока что это работает отлично.

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

Проблема в том, что когда я добавляю новые функции и сталкиваюсь с новой ошибкой, обратная связь в буферизованных исключениях не полностью сохраняется, что делает отладку раздражающей - даже когда я изменяю код, чтобы поднять и немедленно поймать исключение, чтобы я мог добавить его в буфер с обратной связью. Например, при отладке я могу получить исключение из большого блока кода, и я не могу определить, из какой строки оно исходит.

Я обошел эту проблему, сохраняя обратную трассировку как строку внутри буферизованного объекта исключения, что кажется неправильным. Мне пришлось повозиться с оболочкой, чтобы заставить ее работать. Вот мой простой тестовый пример для демонстрации происходящего. Это воспроизводимо для меня, но, очевидно, не для других, кто пробует этот игрушечный пример - и я не знаю почему:

import traceback

class teste(Exception):
    """This is an exception class I'm going to raise to represent some unanticipated exception - for which I will want a traceback."""
    pass


def buf(exc, args):
    """This represents my method I call to buffer an exception, but for this example, I just return the exception and keep it in main in a variable.  The actual method in my code appends to a data member array in the loader object."""
    try:
        raise exc(*args)
    except Exception as e:
        # This is a sanity check that prints the trace that I will want to get from the buffered exception object later
        print("STACK:")
        traceback.print_stack()
        # This is my workaround where I save the trace as a string in the exception object
        e.past_tb = "".join(traceback.format_stack())
        return e

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

Вот что я вижу, когда использую приведенный выше код в оболочке. Этот первый вызов показывает мою проверку на вменяемость - весь стек, к которому я хочу иметь доступ позже:

Но вот что я вижу, когда хочу посмотреть оригинальный traceback объекта es (т.е. буферизованное исключение) позже. В нем есть только последний элемент из трассировки. Это именно то, что я вижу в исходном коде - единственный элемент для строки кода внутри метода buffer:

In [8]: traceback.print_exception(type(es), es, es.__traceback__)
Traceback (most recent call last):
  File "<ipython-input-2-86e515dc1ec1>", line 3, in buf
    raise exc(*args)
teste: This is a test

Моего обходного пути пока достаточно, но я хотел бы иметь соответствующий объект трассировки.

Я отладил проблему, повторно клонировав наш репозиторий во второй каталог, чтобы убедиться, что я не испортил свою песочницу. Думаю, мне стоит попробовать это и на другом компьютере - моем офисном mac. Но может ли кто-нибудь направить меня в правильном направлении, чтобы отладить эту проблему? Что может быть причиной потери полной трассировки?

Python имеет очень странный способ построения обратных ссылок на исключения. Вы можете ожидать, что он будет строить обратную трассировку, когда исключение создано или когда оно поднято, но это не так.

Python строит обратную трассировку по мере распространения исключения. Каждый раз, когда исключение распространяется до нового стекового кадра, запись об отслеживании для этого стекового кадра добавляется к отслеживанию исключения.

Это означает, что обратная трассировка исключения идет только до того места, куда распространяется само исключение. Если вы поймали исключение (и не повторили его), обратная трассировка идет только до того места, где оно было поймано.

К сожалению, ваш обходной путь не так хорош, как кажется. На самом деле вы не потеряли полный отслеживание, потому что полное отслеживание никогда не создавалось. Если вам нужна полная информация о стеке, вам нужно записать ее самостоятельно, с помощью чего-то вроде функции traceback.format_stack(), которую вы сейчас используете.

Вернуться на верх