Adicionando informações a uma exceção?

142

Eu quero conseguir algo assim:

def foo():
   try:
       raise IOError('Stuff ')
   except:
       raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
       e.message = e.message + 'happens at %s' % arg1
       raise

bar('arg1')
Traceback...
  IOError('Stuff Happens at arg1')

Mas o que eu recebo é:

Traceback..
  IOError('Stuff')

Alguma pista de como conseguir isso? Como fazer isso em Python 2 e 3?

anijhaw
fonte
Enquanto procurava por documentação para o messageatributo Exception , encontrei essa pergunta de SO, BaseException.message descontinuado no Python 2.6 , que parece indicar que seu uso agora é desencorajado (e por que não está nos documentos).
martineau
infelizmente, esse link não parece mais funcionar.
Michael Scott Cuthbert
1
@MichaelScottCuthbert, aqui está uma boa alternativa: itmaybeahack.com/book/python-2.6/html/p02/…
Niels Keurentjes
Aqui está uma explicação realmente boa de qual é o status do atributo de mensagem e sua relação com o atributo args e o PEP 352 . É do livro gratuito Building Skills in Python, de Steven F. Lott.
martineau

Respostas:

118

Eu faria assim, para alterar seu tipo foo()não exigirá também a alteração bar().

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
        foo()
    except Exception as e:
        raise type(e)(e.message + ' happens at %s' % arg1)

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    raise type(e)(e.message + ' happens at %s' % arg1)
IOError: Stuff happens at arg1

Atualização 1

Aqui está uma pequena modificação que preserva o retorno original:

...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e), type(e)(e.message +
                               ' happens at %s' % arg1), sys.exc_info()[2]

bar('arg1')

Traceback (most recent call last):
  File "test.py", line 16, in <module>
    bar('arg1')
  File "test.py", line 11, in bar
    foo()
  File "test.py", line 5, in foo
    raise IOError('Stuff')
IOError: Stuff happens at arg1

Atualização 2

Para Python 3.x, o código na minha primeira atualização é sintaticamente incorreto mais a idéia de ter um messageatributo em BaseExceptionfoi recolhido em uma mudança de PEP 352 em 2012-05-16 (minha primeira atualização foi publicada em 2012-03-12) . Portanto, atualmente, no Python 3.5.2 de qualquer maneira, você precisaria fazer algo nesse sentido para preservar o retorno e não codificar o tipo de exceção na função bar(). Observe também que haverá a linha:

During handling of the above exception, another exception occurred:

nas mensagens de retorno exibidas.

# for Python 3.x
...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e)(str(e) +
                      ' happens at %s' % arg1).with_traceback(sys.exc_info()[2])

bar('arg1')

Atualização 3

Um comentarista perguntou se havia uma maneira de funcionar no Python 2 e 3. Embora a resposta possa parecer "Não" devido às diferenças de sintaxe, há é uma maneira de contornar isso usando uma função auxiliar como reraise()nasix add- no módulo. Portanto, se você preferir não usar a biblioteca por algum motivo, a seguir está uma versão autônoma simplificada.

Observe também que, como a exceção é gerada novamente dentro da reraise()função, ela aparecerá em qualquer retorno de retorno gerado, mas o resultado final é o que você deseja.

import sys

if sys.version_info.major < 3:  # Python 2?
    # Using exec avoids a SyntaxError in Python 3.
    exec("""def reraise(exc_type, exc_value, exc_traceback=None):
                raise exc_type, exc_value, exc_traceback""")
else:
    def reraise(exc_type, exc_value, exc_traceback=None):
        if exc_value is None:
            exc_value = exc_type()
        if exc_value.__traceback__ is not exc_traceback:
            raise exc_value.with_traceback(exc_traceback)
        raise exc_value

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
        reraise(type(e), type(e)(str(e) +
                                 ' happens at %s' % arg1), sys.exc_info()[2])

bar('arg1')
Martineau
fonte
3
Isso perde o retorno, meio que derrota o ponto de adicionar informações a uma exceção existente. Além disso, ele não funciona exceções com o ctor que recebe argumentos> 1 (o tipo é algo que você não pode controlar no local em que você captura a exceção).
Václav Slavík
1
@ Václav: É bastante fácil evitar a perda do backtrace - como mostra a atualização que adicionei. Embora isso ainda não lide com todas as exceções possíveis, funciona para casos semelhantes ao que foi mostrado na pergunta do OP.
martineau
1
Isso não está certo. Se o tipo (e) for substituído __str__, você poderá obter resultados indesejáveis. Observe também que o segundo argumento é passado para o construtor fornecido pelo primeiro argumento, o que gera uma certa disparate type(e)(type(e)(e.message). Em terceiro lugar, o e.message é preterido em favor do e.args [0].
bukzor
1
então, não existe uma maneira portátil que funcione no Python 2 e 3?
Elias Dorneles
1
@martineau Qual é o objetivo de importar dentro do bloco exceto? Isso economiza memória importando apenas quando necessário?
AllTradesJack
115

Caso você tenha vindo aqui procurando uma solução para o Python 3, o manual diz:

Ao gerar uma nova exceção (em vez de usar um bare raisepara aumentar novamente a exceção que está sendo tratada no momento), o contexto implícito da exceção pode ser complementado com uma causa explícita usando from with raise:

raise new_exc from original_exc

Exemplo:

try:
    return [permission() for permission in self.permission_classes]
except TypeError as e:
    raise TypeError("Make sure your view's 'permission_classes' are iterable. "
                    "If you use '()' to generate a set with a single element "
                    "make sure that there is a comma behind the one (element,).") from e

Que fica assim no final:

2017-09-06 16:50:14,797 [ERROR] django.request: Internal Server Error: /v1/sendEmail/
Traceback (most recent call last):
File "venv/lib/python3.4/site-packages/rest_framework/views.py", line 275, in get_permissions
    return [permission() for permission in self.permission_classes]
TypeError: 'type' object is not iterable 

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

Traceback (most recent call last):
    # Traceback removed...
TypeError: Make sure your view's Permission_classes are iterable. If 
     you use parens () to generate a set with a single element make 
     sure that there is a (comma,) behind the one element.

Transformar uma TypeErrormensagem totalmente indefinida em uma boa mensagem com dicas para uma solução sem estragar a Exceção original.

Chris
fonte
14
Essa é a melhor solução, já que a exceção resultante aponta para a causa original, forneça mais detalhes.
JT
existe alguma solução que possamos adicionar alguma mensagem, mas ainda não gerar uma nova exceção? Quero dizer, apenas estender a mensagem da instância de exceção.
edcSam
Yaa ~~ funciona, mas parece algo que não deveria ser feito comigo. A mensagem é armazenada em e.args, mas é uma tupla, portanto não pode ser alterada. Então, primeiro copie argspara uma lista, modifique-a e copie-a novamente como uma tupla:args = list(e.args) args[0] = 'bar' e.args = tuple(args)
Chris
27

Supondo que você não queira ou não possa modificar foo (), faça o seguinte:

try:
    raise IOError('stuff')
except Exception as e:
    if len(e.args) >= 1:
        e.args = (e.args[0] + ' happens',) + e.args[1:]
    raise

Esta é realmente a única solução aqui que resolve o problema no Python 3 sem uma mensagem feia e confusa de "Durante o tratamento da exceção acima, ocorreu outra exceção".

Caso a linha de re-raise deva ser adicionada ao rastreamento da pilha, escrever em raise evez de raisefará o truque.

Steve Howard
fonte
mas neste caso, se a exceção mudar em foo, tenho que mudar de barra também, certo?
Anijhaw 19/05/11
1
Se você capturar Exception (editado acima), poderá capturar qualquer exceção de biblioteca padrão (bem como aquelas que herdam de Exception e chamam Exception .__ init__).
Steve Howard
6
para ser mais completo / cooperativo, inclua as outras partes da tupla original:e.args = ('mynewstr' + e.args[0],) + e.args[1:]
Dubslow
1
@ nmz787 Essa é a melhor solução para o Python 3. Qual é exatamente o seu erro?
Christian
1
@Dubslow e martineau Incorporei suas sugestões em uma edição.
Christian
9

Eu não gosto de todas as respostas dadas até agora. Eles ainda são muito detalhados. Na saída de código e mensagem.

Tudo o que eu quero é o stacktrace apontando para a exceção de origem, sem nenhuma exceção, portanto, sem criação de novas exceções, apenas re-aumentando o original com todas as informações relevantes. estados quadro da pilha, que levaram até lá.

Steve Howard deu uma boa resposta que eu quero estender, não, reduzir ... apenas para python 3.

except Exception as e:
    e.args = ("Some failure state", *e.args)
    raise

A única novidade é o parâmetro de expansão / descompactação que o torna pequeno e fácil o suficiente para eu usar.

Tente:

foo = None

try:
    try:
        state = "bar"
        foo.append(state)

    except Exception as e:
        e.args = ("Appending '"+state+"' failed", *e.args)
        raise

    print(foo[0]) # would raise too

except Exception as e:
    e.args = ("print(foo) failed: " + str(foo), *e.args)
    raise

Isso lhe dará:

Traceback (most recent call last):
  File "test.py", line 6, in <module>
    foo.append(state)
AttributeError: ('print(foo) failed: None', "Appending 'bar' failed", "'NoneType' object has no attribute 'append'")

Uma impressão bonita e simples pode ser algo como

print("\n".join( "-"*i+" "+j for i,j in enumerate(e.args)))
6EQUJ5
fonte
5

Uma abordagem prática que usei é usar o atributo de classe como armazenamento para obter detalhes, pois o atributo de classe pode ser acessado no objeto de classe e na instância de classe:

class CustomError(Exception):
    def __init__(self, details: Dict):
        self.details = details

Então no seu código:

raise CustomError({'data': 5})

E ao detectar um erro:

except CustomError as e:
    # Do whatever you want with the exception instance
    print(e.details)
Kee
fonte
Não é realmente útil, pois o OP está solicitando que os detalhes sejam impressos como parte do rastreamento da pilha quando a exceção original é lançada e não capturada.
cowbert
Eu acho que a solução é boa. Mas a descrição não é verdadeira. Os atributos de classe são copiados para instâncias quando você os instancia. Portanto, quando você modifica o atributo "detalhes", da instância, o atributo da classe ainda será Nenhum. De qualquer forma, queremos esse comportamento aqui.
Adam Wallner
2

Diferentemente das respostas anteriores, isso funciona com exceções muito ruins __str__. No entanto, ele modifica o tipo, a fim de considerar fatores inúteis__str__ implementações .

Ainda gostaria de encontrar uma melhoria adicional que não modifique o tipo.

from contextlib import contextmanager
@contextmanager
def helpful_info():
    try:
        yield
    except Exception as e:
        class CloneException(Exception): pass
        CloneException.__name__ = type(e).__name__
        CloneException.__module___ = type(e).__module__
        helpful_message = '%s\n\nhelpful info!' % e
        import sys
        raise CloneException, helpful_message, sys.exc_traceback


class BadException(Exception):
    def __str__(self):
        return 'wat.'

with helpful_info():
    raise BadException('fooooo')

O retorno original e o tipo (nome) são preservados.

Traceback (most recent call last):
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
  File "/usr/lib64/python2.6/contextlib.py", line 34, in __exit__
    self.gen.throw(type, value, traceback)
  File "re_raise.py", line 5, in helpful_info
    yield
  File "re_raise.py", line 20, in <module>
    raise BadException('fooooo')
__main__.BadException: wat.

helpful info!
Bukzor
fonte
2

Fornecerei um trecho de código que uso com frequência sempre que quiser adicionar informações extras a uma exceção. Eu trabalho tanto em Python 2.7 e 3.6.

import sys
import traceback

try:
    a = 1
    b = 1j

    # The line below raises an exception because
    # we cannot compare int to complex.
    m = max(a, b)  

except Exception as ex:
    # I create my  informational message for debugging:
    msg = "a=%r, b=%r" % (a, b)

    # Gather the information from the original exception:
    exc_type, exc_value, exc_traceback = sys.exc_info()

    # Format the original exception for a nice printout:
    traceback_string = ''.join(traceback.format_exception(
        exc_type, exc_value, exc_traceback))

    # Re-raise a new exception of the same class as the original one, 
    # using my custom message and the original traceback:
    raise type(ex)("%s\n\nORIGINAL TRACEBACK:\n\n%s\n" % (msg, traceback_string))

O código acima resulta na seguinte saída:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-09b74752c60d> in <module>()
     14     raise type(ex)(
     15         "%s\n\nORIGINAL TRACEBACK:\n\n%s\n" %
---> 16         (msg, traceback_string))

TypeError: a=1, b=1j

ORIGINAL TRACEBACK:

Traceback (most recent call last):
  File "<ipython-input-6-09b74752c60d>", line 7, in <module>
    m = max(a, b)  # Cannot compare int to complex
TypeError: no ordering relation is defined for complex numbers


Sei que isso se desvia um pouco do exemplo fornecido na pergunta, mas, no entanto, espero que alguém ache útil.

Pedro M Duarte
fonte
1

Você pode definir sua própria exceção que herda de outra e criar seu próprio construtor para definir valor.

Por exemplo:

class MyError(Exception):
   def __init__(self, value):
     self.value = value
     Exception.__init__(self)

   def __str__(self):
     return repr(self.value)
Alexander Kiselev
fonte
2
Não aborda a necessidade de alterar / acrescentar algo à messageexceção original (mas poderia ser corrigido, eu acho).
Martineau
-6

Talvez

except Exception as e:
    raise IOError(e.message + 'happens at %s'%arg1)
Malvolio
fonte