¿Agregar información a una excepción?

142

Quiero lograr algo como esto:

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')

Pero lo que obtengo es:

Traceback..
  IOError('Stuff')

¿Alguna pista sobre cómo lograr esto? ¿Cómo hacerlo tanto en Python 2 como en 3?

anijhaw
fuente
Mientras buscaba documentación para el messageatributo Exception , encontré esta pregunta SO, BaseException.message en desuso en Python 2.6 , que parece indicar que su uso ahora está desaconsejado (y por qué no está en los documentos).
Martineau
lamentablemente, ese enlace ya no parece funcionar.
Michael Scott Cuthbert
1
@MichaelScottCuthbert he aquí una buena alternativa: itmaybeahack.com/book/python-2.6/html/p02/…
Niels Keurentjes
Aquí hay una muy buena explicación de cuál es el estado del atributo del mensaje y su relación con el atributo args y PEP 352 . Es del libro gratuito Building Skills in Python de Steven F. Lott.
Martineau

Respuestas:

118

Lo haría así, así que cambiar su tipo foo()no requerirá también cambiarlo 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

Actualización 1

Aquí hay una ligera modificación que conserva el rastreo 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

Actualización 2

Para Python 3.x, el código en mi primera actualización es sintácticamente incorrecto y la idea de tener un messageatributo activadoBaseException se retractó en un cambio a PEP 352 el 16/05/2012 (mi primera actualización se publicó el 12/03/2012) . Entonces, actualmente, en Python 3.5.2 de todos modos, necesitaría hacer algo en este sentido para preservar el rastreo y no codificar el tipo de excepción en la función bar(). También tenga en cuenta que habrá la línea:

During handling of the above exception, another exception occurred:

en los mensajes de rastreo que se muestran.

# 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')

Actualización 3

Un comentarista preguntó si había una manera que funcione tanto en Python 2 y 3. Aunque la respuesta podría parecer que es "No", debido a las diferencias de sintaxis, no es una forma de evitar que mediante el uso de una función de ayuda al igual que reraise()en el sixAgregar- en el módulo Entonces, si prefiere no usar la biblioteca por alguna razón, a continuación se muestra una versión independiente simplificada.

Tenga en cuenta también que, dado que la excepción se vuelve a generar dentro de la reraise()función, aparecerá en cualquier rastreo que se genere, pero el resultado final es lo que desea.

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
fuente
3
Eso pierde el rastro hacia atrás, algo así como derrotar el punto de agregar información a una excepción existente. Además, no funciona las excepciones con ctor que toma> 1 argumentos (el tipo es algo que no puede controlar desde el lugar donde captura la excepción).
Václav Slavík
1
@ Václav: es bastante fácil evitar que se pierda la traza inversa, como se muestra en la actualización que agregué. Si bien esto todavía no maneja todas las excepciones imaginables, funciona para casos similares a los que se muestran en la pregunta del OP.
Martineau
1
Esto no está del todo bien. Si se anula el tipo (e) __str__, puede obtener resultados no deseados. También tenga en cuenta que el segundo argumento se pasa al constructor dado por el primer argumento, lo que produce un poco sin sentido type(e)(type(e)(e.message). En tercer lugar, e.message está en desuso a favor de e.args [0].
bukzor
1
Entonces, ¿no hay una forma portátil que funcione tanto en Python 2 como en 3?
Elias Dorneles
1
@martineau ¿Cuál es el propósito de importar dentro del bloque excepto? ¿Es esto para ahorrar memoria importando solo cuando sea necesario?
AllTradesJack
115

En caso de que haya venido aquí buscando una solución para Python 3, el manual dice:

Cuando se genera una nueva excepción (en lugar de utilizar un bare raisepara volver a generar la excepción que se está manejando actualmente), el contexto de excepción implícito se puede complementar con una causa explícita al usar from with raise:

raise new_exc from original_exc

Ejemplo:

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 se ve así al 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.

Convirtiendo un TypeErrormensaje totalmente indescriptible en un bonito mensaje con sugerencias hacia una solución sin estropear la excepción original

Chris
fuente
14
Esta es la mejor solución, ya que la excepción resultante apunta a la causa original, proporciona más detalles.
JT
¿Hay alguna solución a la que podamos agregar algún mensaje pero que no genere una nueva excepción? Me refiero a simplemente extender el mensaje de la instancia de excepción.
edcSam
Yaa ~~ funciona, pero se siente como algo que no debería hacerme. El mensaje se almacena en e.args, pero eso es una Tupla, por lo que no se puede cambiar. Así que primero copie argsen una lista, luego modifíquelo, luego cópielo nuevamente como una Tupla:args = list(e.args) args[0] = 'bar' e.args = tuple(args)
Chris
27

Suponiendo que no desea o no puede modificar foo (), puede hacer esto:

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

De hecho, esta es la única solución aquí que resuelve el problema en Python 3 sin un mensaje feo y confuso "Durante el manejo de la excepción anterior, ocurrió otra excepción".

En caso de que la línea de resubir se agregue a la traza de la pila, escribir en raise elugar de raisehará el truco.

Steve Howard
fuente
pero en este caso, si la excepción cambia en foo, también tengo que cambiar la barra, ¿verdad?
anijhaw
1
Si detecta Exception (editado anteriormente), puede detectar cualquier excepción de biblioteca estándar (así como las que heredan de Exception y llaman a Exception .__ init__).
Steve Howard
66
para ser más completo / cooperativo, incluya las otras partes de la tupla original:e.args = ('mynewstr' + e.args[0],) + e.args[1:]
Dubslow
1
@ nmz787 De hecho, esta es la mejor solución para Python 3. ¿Cuál es exactamente tu error?
Cristiano
1
@Dubslow y martineau incorporé tus sugerencias en una edición.
Cristiano
9

No me gustan todas las respuestas dadas hasta ahora. Todavía son demasiado detallados en mi humilde opinión. En salida de código y mensaje.

Todo lo que quiero tener es el seguimiento de la pila que apunta a la excepción de origen, sin cosas de excepción en el medio, por lo que no se crean nuevas excepciones, solo se vuelve a elevar el original con todos los estados relevantes del marco de la pila que condujeron allí.

Steve Howard dio una buena respuesta que quiero extender, no, reducir ... solo a Python 3.

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

Lo único nuevo es la expansión / desempaquetado de parámetros, que lo hace lo suficientemente pequeño y fácil de usar.

Intentalo:

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

Esto te 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'")

Una simple impresión bonita podría ser algo así como

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

Un enfoque útil que utilicé es usar el atributo de clase como almacenamiento para detalles, ya que el atributo de clase es accesible tanto desde el objeto de clase como desde la instancia de clase:

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

Luego en tu código:

raise CustomError({'data': 5})

Y al detectar un error:

except CustomError as e:
    # Do whatever you want with the exception instance
    print(e.details)
Kee
fuente
No es realmente útil ya que el OP solicita que los detalles se impriman como parte del seguimiento de la pila cuando se lanza la excepción original y no se captura.
cowbert
Creo que la solución es buena. Pero la descripción no es cierta. Los atributos de clase se copian en instancias cuando los instancia. Por lo tanto, cuando modifique el atributo "detalles" de la instancia, el atributo de clase seguirá siendo Ninguno. De todos modos, queremos este comportamiento aquí.
Adam Wallner el
2

A diferencia de las respuestas anteriores, esto funciona frente a excepciones con muy mal __str__. Sin embargo, modifica el tipo, para factorizar __str__implementaciones inútiles .

Todavía me gustaría encontrar una mejora adicional que no modifique el 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')

El rastreo original y el tipo (nombre) se conservan.

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
fuente
2

Proporcionaré un fragmento de código que utilizo a menudo cuando quiera agregar información adicional a una excepción. Trabajo tanto en Python 2.7 como en 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))

El código anterior da como resultado el siguiente resultado:

---------------------------------------------------------------------------
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


Sé que esto se desvía un poco del ejemplo proporcionado en la pregunta, pero sin embargo espero que alguien lo encuentre útil.

Pedro M Duarte
fuente
1

Puede definir su propia excepción que hereda de otro y crear su propio constructor para establecer el valor.

Por ejemplo:

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

   def __str__(self):
     return repr(self.value)
Alexander Kiselev
fuente
2
No aborda la necesidad de cambiar / agregar algo a la messageexcepción original (pero podría solucionarse, creo).
Martineau
-6

Tal vez

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