Поднимите исключение с более высокого уровня, а-ля предупреждения

В предупреждениях модуля (https://docs.python.org/3.5/library/warnings.html) есть возможность вызвать предупреждение, которое, кажется, исходит откуда-то из более раннего места в стеке:

warnings.warn('This is a test', stacklevel=2)

Есть ли аналог для поднятия ошибок? Я знаю, что могу вызвать ошибку с помощью альтернативной трассировки, но я не могу создать эту трассировку в модуле, поскольку она должна поступать ранее. Я представляю себе что-то вроде:

tb = magic_create_traceback_right_here()
raise ValueError('This is a test').with_traceback(tb.tb_next)

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

Кроме того, я пробовал уловки, такие как создание фиктивного исключения, его перехват и передача трассировки, но каким-то образом tb_next становится None. У меня нет идей.

Изменить:

Я хотел бы получить результат этого минимального примера (называемого tb2.py):

import check_raise

check_raise.raise_if_string_is_true('True')

быть только этим:

Traceback (most recent call last):
  File "tb2.py", line 10, in <module>
    check_raise.raise_if_string_is_true(string)
RuntimeError: An exception was raised.

person Joel    schedule 09.12.2015    source источник
comment
Хм, если вы используете filter, чтобы превратить предупреждение в ошибку, а затем просто позвоните warnings.warn, поможет ли это?   -  person Dimitris Fasarakis Hilliard    schedule 09.12.2015
comment
Судя по source это raise это, я просто не уверен, будет ли он делать это так, как вы хотите (и, глядя на это, я думаю, что это не так).   -  person Dimitris Fasarakis Hilliard    schedule 09.12.2015
comment
@Jim Я пробовал это, возникает ошибка, но выводимая им трассировка такая же, как если бы вы вызвали ошибку в этом месте: аргумент stacklevel = ничего не делает.   -  person Joel    schedule 09.12.2015
comment
Jinja2 (движок шаблонов) использует множество грязных уловок, чтобы добиться чего-то подобного, но для реализации требуется огромное количество кода. См. github.com/mitsuhiko/jinja2/blob/master/jinja2/debug. .py, чтобы узнать о неряшливых подробностях, а затем подумайте, действительно ли вы хотите пойти по этому пути ...   -  person kiwidrew    schedule 10.12.2015
comment
@kiwidrew Да, это выглядит ужасно. Понятия не имею, возможно ли это вообще.   -  person Joel    schedule 11.12.2015
comment
Я не думаю, что есть способ сделать это, AFAIK. Тем не менее, вы можете уклониться от всего, что имеет какое-либо существенное отношение к трассировке. Например, создайте подкласс класса logging.Logger, который изменяет трассировку (или создает измененную копию), прежде чем передавать его своему классу super. Кроме того, вы можете заменить функцию sys.excepthook, которая распечатывает неперехваченные исключения и проделывают те же трюки. Если вы на самом деле не используете трассировку для чего-либо, кроме печати, это может достаточно хорошо соответствовать вашим потребностям.   -  person eestrada    schedule 16.01.2016
comment
@eestrada К сожалению, мне это нужно не только для печати. Я в основном хочу создать свою собственную функцию, которая действует точно так же, как оператор raise, но имеет другую логику. Я думаю, что это, вероятно, бесполезное упражнение из-за того, насколько оно похоже на простое создание оператора, который потребует перекомпиляции python, что определенно НЕ является маршрутом, который я иду с пакетом, который хочу распространить. stackoverflow.com/ questions / 214881 /   -  person Joel    schedule 16.01.2016
comment
@Joel Вот где что-то вроде макросов Lisp прекрасно подошло бы для всех этих требований : \. Вы можете попробовать этот пакет макросов Python, чтобы проверить, соответствует ли он требованиям. Предостережение: я никогда не пробовал его, и на данный момент он кажется довольно старым.   -  person eestrada    schedule 16.01.2016
comment
Возможно ли просто catch исходное исключение и выбросить новое в соответствующий момент? В качестве альтернативы, можете ли вы создать tb в соответствующей точке независимо от того, возникает ли ошибка, и передать его в стек вызовов, чтобы при необходимости выбросить позже?   -  person Kyle Strand    schedule 21.01.2016


Ответы (3)


Если я правильно понимаю, вам нужен вывод этого минимального примера:

def check_raise(function):
    try:
        return function()
    except Exception:
        raise RuntimeError('An exception was raised.')

def function():
    1/0

check_raise(function)

быть только этим:

Traceback (most recent call last):
  File "tb2.py", line 10, in <module>
    check_raise(function)
RuntimeError: An exception was raised.

Фактически, это намного больше; существует цепочка исключений, с которой можно справиться, обработав RuntimeError немедленно, удалив его __context__ и повторно подняв его, и есть еще одна строка трассировки для самого RuntimeError:

  File "tb2.py", line 5, in check_raise
    raise RuntimeError('An exception was raised.')

Насколько я могу судить, чистый код Python не может заменить трассировку исключения после того, как оно было вызвано; интерпретатор имеет контроль над добавлением к нему, но он предоставляет только текущую трассировку всякий раз, когда обрабатывается исключение. Не существует API (даже при использовании функций трассировки) для передачи вашей собственной трассировки интерпретатору, а объекты трассировки неизменяемы (это то, чем занимается этот хак Jinja, связанный с вещами уровня C).

Итак, предполагая, что вас интересует сокращенная трассировка не для дальнейшего программного использования, а только для удобного вывода, лучшим вариантом будет excepthook, который контролирует, как трассировка выводится на консоль. Для определения того, где остановить печать, можно использовать специальную локальную переменную (это немного более надежно, чем ограничение трассировки до ее длины минус 1 или что-то в этом роде). В этом примере требуется Python 3.5 (для traceback.walk_tb):

import sys
import traceback

def check_raise(function):
    __exclude_from_traceback_from_here__ = True
    try:
        return function()
    except Exception:
        raise RuntimeError('An exception was raised.')

def print_traceback(exc_type, exc_value, tb):
    for i, (frame, lineno) in enumerate(traceback.walk_tb(tb)):
        if '__exclude_from_traceback_from_here__' in frame.f_code.co_varnames:
            limit = i
            break
    else:
        limit = None
    traceback.print_exception(
        exc_type, exc_value, tb, limit=limit, chain=False)

sys.excepthook = print_traceback

def function():
    1/0

check_raise(function)

Теперь это результат:

Traceback (most recent call last):
  File "tb2.py", line 26, in <module>
    check_raise(function)
RuntimeError: An exception was raised.
person Thomas Lotze    schedule 21.02.2016
comment
Это неплохо. Можно ли сохранить строку sys.excepthook внутри модуля? Я хочу вставить весь этот код в модуль, чтобы он выглядел по-другому. Я обновлю свой вопрос, чтобы он был более ясным. - person Joel; 22.02.2016
comment
Да, это технически возможно. Однако я бы предложил поместить его в функцию инициализации внутри вашего модуля и вызвать один раз в начале вашей программы, которая использует этот модуль. Требуя от программиста сознательной инициализации хука, он становится менее мегапиксельным (явный лучше, чем неявный) и, что более важно, позволяет делать такие вещи при инициализации процесса и в правильном порядке в случае, если другие модули возятся с теми же возможностями. . В противном случае, excepthook будет установлен всякий раз, когда модуль будет импортирован первым. - person Thomas Lotze; 22.02.2016
comment
Ах, черт ... это может быть проблемой, а? Я не хочу, чтобы он полностью завладел системой исключений ... Может быть, он может провести тест, чтобы убедиться, что он исправляет только исключения, сгенерированные моим модулем, и позволяет другим проходить без проблем ...? - person Joel; 23.02.2016
comment
Это просто. Если имя маркера не найдено ни в одном кадре трассировки, оно уже печатается без изменений. Единственное, о чем нужно позаботиться, - это оставить цепочку нетронутой, если трассировка не принадлежит вам, т.е. передать chain=False в print_exception только для ваших исключений. - person Thomas Lotze; 23.02.2016

Я не могу поверить, что публикую это

Делая это вы идете против дзен .

Особых случаев недостаточно, чтобы нарушать правила.

Но если вы настаиваете, вот ваш магический код.

check_raise.py

import sys
import traceback

def raise_if_string_is_true(string):
    if string == 'true':
        #the frame that called this one
        f = sys._getframe().f_back
        #the most USELESS error message ever
        e = RuntimeError("An exception was raised.")

        #the first line of an error message
        print('Traceback (most recent call last):',file=sys.stderr)
        #the stack information, from f and above
        traceback.print_stack(f)
        #the last line of the error
        print(*traceback.format_exception_only(type(e),e),
              file=sys.stderr, sep="",end="")

        #exit the program
        #if something catches this you will cause so much confusion
        raise SystemExit(1)
        # SystemExit is the only exception that doesn't trigger an error message by default.

Это чистый питон, он не мешает sys.excepthook и даже в блоке try он не перехватывается с помощью except Exception:, хотя перехватывается с помощью except:

test.py

import check_raise

check_raise.raise_if_string_is_true("true")
print("this should never be printed")

предоставит вам желаемое (ужасно неинформативное и чрезвычайно сфальсифицированное) сообщение трассировки.

Tadhgs-MacBook-Pro:Documents Tadhg$ python3 test.py
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    check_raise.raise_if_string_is_true("true")
RuntimeError: An exception was raised.
Tadhgs-MacBook-Pro:Documents Tadhg$
person Tadhg McDonald-Jensen    schedule 26.03.2016
comment
Я выполнял очистку и нашел это. К вашему сведению, ваш sys._getframe() вызов задокументирован как внутренний и специализированный, в настоящее время существует в CPython и может не существовать в других реализациях. Я считаю, что подразумевается, что эта функция не требуется спецификацией модуля sys. В этом смысле это не совсем чистый Python. - person Joel; 25.07.2019
comment
Но - я считаю, что основная мысль: «Не делай этого», вероятно, является наиболее правильным ответом. - person Joel; 25.07.2019

РЕДАКТИРОВАТЬ: предыдущая версия не содержала цитат или объяснений.

Я предлагаю обратиться к PEP 3134, в котором говорится в разделе Мотивация :

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

Когда Exception возникает с атрибутом __cause__, сообщение трассировки принимает форму:

Traceback (most recent call last):
 <CAUSE TRACEBACK>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  <MAIN TRACEBACK>

Насколько я понимаю, это именно то, чего вы пытаетесь достичь; четко указать, что причина ошибки не в вашем модуле, а в другом месте. Если вместо этого вы пытаетесь опустить информацию в трассировку, как предлагает ваше edit, то остальная часть этого ответа не принесет вам никакой пользы.


Замечание о синтаксисе:

Атрибут __cause__ в объектах исключения всегда инициализируется значением None. Он задается новой формой оператора «рейз»:

   raise EXCEPTION from CAUSE

что эквивалентно:

    exc = EXCEPTION
    exc.__cause__ = CAUSE
    raise exc

так что минимальный пример будет примерно таким:

def function():
    int("fail")

def check_raise(function):
    try:
        function()
    except Exception as original_error:
        err = RuntimeError("An exception was raised.")
        raise err from original_error

check_raise(function)

что дает такое сообщение об ошибке:

Traceback (most recent call last):
  File "/PATH/test.py", line 7, in check_raise
    function()
  File "/PATH/test.py", line 3, in function
    int("fail")
ValueError: invalid literal for int() with base 10: 'fail'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/PATH/test.py", line 12, in <module>
    check_raise(function)
  File "/PATH/test.py", line 10, in check_raise
    raise err from original_error
RuntimeError: An exception was raised.

Однако первая строка причины - это инструкция в блоке try строки check_raise:

  File "/PATH/test.py", line 7, in check_raise
    function()

поэтому перед повышением err может быть (а может и не быть) желательно удалить самый внешний фрейм трассировки из original_error:

except Exception as original_error:
    err = RuntimeError("An exception was raised.")
    original_error.__traceback__ = original_error.__traceback__.tb_next
    raise err from original_error

Таким образом, единственная строка в трассировке, которая, по-видимому, исходит от check_raise, - это самый последний оператор raise, который нельзя пропустить с чистым кодом Python, хотя в зависимости от того, насколько информативным является сообщение, вы можете очень четко указать, что ваш модуль не был причиной проблема:

err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
the traceback for the error is shown above.""".format(function,check_raise))

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

def check_raise(function):
    try:
        function()
    except Exception as original_error:
        err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
the traceback for the error is shown above.""".format(function,check_raise))
        original_error.__traceback__ = original_error.__traceback__.tb_next
        raise err from original_error

def test_chain():
    check_raise(test)

def test():
    raise ValueError

check_raise(test_chain)

дает мне следующее сообщение об ошибке:

Traceback (most recent call last):
  File "/Users/Tadhg/Documents/test.py", line 16, in test
    raise ValueError
ValueError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/Tadhg/Documents/test.py", line 13, in test_chain
    check_raise(test)
  File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
    raise err from original_error
RuntimeError: test encountered an error during call to __main__.check_raise
the traceback for the error is shown above.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/Tadhg/Documents/test.py", line 18, in <module>
    check_raise(test_chain)
  File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
    raise err from original_error
RuntimeError: test_chain encountered an error during call to __main__.check_raise
the traceback for the error is shown above.

Да, он длинный, но он значительно информативнее, чем:

Traceback (most recent call last):
  File "/Users/Tadhg/Documents/test.py", line 18, in <module>
    check_raise(test_chain)
RuntimeError: An exception was raised.

не говоря уже о том, что исходная ошибка все еще может использоваться, даже если программа не заканчивается:

import traceback

def check_raise(function):
    ...

def fail():
    raise ValueError

try:
    check_raise(fail)
except RuntimeError as e:
    cause = e.__cause__
    print("check_raise failed because of this error:")
    traceback.print_exception(type(cause), cause, cause.__traceback__)

print("and the program continues...")
person Tadhg McDonald-Jensen    schedule 24.01.2016
comment
Закрыть. На самом деле мне не нужна последняя часть, проверяемый процесс и исключение. - person Joel; 22.02.2016
comment
Я предполагаю, что вы имели в виду, что не хотите, чтобы raise x from e присутствовал, но я не понимаю, почему это может быть проблемой, если в части ошибки конкретно указано The above exception was the direct cause of the following exception:, в любом случае я отредактировал ответ, чтобы объяснить ответ и показать преимущества его использования. - person Tadhg McDonald-Jensen; 22.02.2016
comment
Это просто беспорядок. Настоящая мотивация всего этого заключается в том, что я хочу, чтобы это проверяло версию модуля git. Я пишу модули, которые используются скриптами. Но мои модули иногда меняются довольно быстро - они нестабильны. Поэтому я сделал еще один модуль (VCheck), который проверяет версию этих модулей git. Если это правильная версия, скрипт запускается. Если это не так, VCheck должен вызвать исключение. Я хочу, чтобы исключением была неправильная версия и обратная связь со сценарием, а не VCheck, определяющий в недрах, что версия неправильная, и обратная связь с VCheck. - person Joel; 23.02.2016
comment
Я заметил, что встроенные функции, такие как int или open, работают так, как вы предлагаете, поэтому, возможно, вам просто нужно сделать свою функцию на C: P - person Tadhg McDonald-Jensen; 23.02.2016
comment
Да, я думал об этом ... Но это становится до смешного сложно. Я думаю, что пытаюсь сделать то, для чего Python не предназначен ... - person Joel; 23.02.2016
comment
Рассматривали ли вы просто возврат True, если проверка сработала, и False, если она не удалась? тогда вы можете просто assert проверить в сценарии, и именно на это будет указывать последняя строка трассировки. И да, попытка сделать Traceback менее информативным, чем это возможно, очень не-Pythonesque. - person Tadhg McDonald-Jensen; 23.02.2016