Re-raising exceptions with modified message

  • Thread starter Christoph Zwerschke
  • Start date
S

samwyse

What is the best way to re-raise any exception with a message
supplemented with additional information (e.g. line number in a
template)?
[...]
That leaves the issue of the name being changed for
UnicodeDecodeError, which might be fixable by diddling with __name__
properties. Or perhaps SorryEx needs to be a factory that returns
exception classes; the last line would be "SorryEx(e)()". I'll have
to play with this a bit.

OK, the following mostly works. You probably want the factory to copy
more of the original class into the SorryEx class each time, since
someone catching an exception may expect to look at things besides its
string representation.

def SorryFactory(e):
class SorryEx(Exception):
def __init__(self):
self._e = e
def __getattr__(self, name):
return getattr(self._e, name)
def __str__(self):
return str(self._e) + ", sorry!"
SorryEx.__name__ = e.__class__.__name__
return SorryEx

def test(code):
try:
code()
except Exception, e:
try:
raise e.__class__, str(e) + ", sorry!"
except TypeError:
raise SorryFactory(e)()

test(lambda: unicode('\xe4'))
 
S

samwyse

What is the best way to re-raise any exception with a message
supplemented with additional information (e.g. line number in a
template)? Let's say for simplicity I just want to add "sorry" to every
exception message.

OK, this seems pretty minimal, yet versatile. It would be nice to be
able to patch the traceback, but it turns out that it's fairly hard to
do. If you really want to do that, however, go take a look at
http://lucumr.pocoo.org/cogitations/2007/06/16/patching-python-tracebacks-part-two/


# Written by Sam Denton <[email protected]>
# You may use, copy, or distribute this work,
# as long as you give credit to the original author.
def rewriten_exception(old, f):
class Empty(): pass
new = Empty()
new.__class__ = old.__class__
new.__dict__ = old.__dict__.copy()
new.__str__ = f
return new

def test(code):
try:
code()
except Exception, e:
raise rewriten_exception(e, lambda: str(e) + ", sorry!")

test(lambda: unicode('\xe4'))
test(lambda: 1/0)
 
C

Christoph Zwerschke

Did you run this?
With Py < 2.5 I get a syntax error, and with Py 2.5 I get:

new.__class__ = old.__class__
TypeError: __class__ must be set to a class

-- Chris
 
C

Christoph Zwerschke

samwyse said:
def test(code):
try:
code()
except Exception, e:
try:
raise e.__class__, str(e) + ", sorry!"
except TypeError:
raise SorryFactory(e)()

Ok, you're suggestig the naive approach if it works and the factory
approach I came up with last as a fallback. Maybe a suitable compromize.

-- Chris
 
S

samwyse

Did you run this?
With Py < 2.5 I get a syntax error, and with Py 2.5 I get:

new.__class__ = old.__class__
TypeError: __class__ must be set to a class

-- Chris

Damn, I'd have sworn I ran the final copy that I posted, but
apparently I did manage to have a typo creep in as I was prettifying
the code. You need to lose the '()' in the definition of Empty. (I'd
orignally had it subclass Exception but discovered that it wasn't
needed.)

class Empty: pass

I can't figure out the other complaint, though, as old.__class_ should
be a class. I guess I need to upgrade; I am using PythonWin 2.4.3
(#69, Apr 11 2006, 15:32:42) [MSC v.1310 32 bit (Intel)] on win32.
(Of course, at work they're still stuck on 2.4.2.) Printing
type(old.__class__) gives me <type 'classobj'>; maybe using
setattr(new, '__class__', old.__class__)
instead of the assignment would work, or maybe it's a bug/feature
introduced in 2.5. (Trying this code:
class Empty(old.__class__): pass
brings us back to the "TypeError: function takes exactly 5 arguments
(0 given)" message that we're trying to avoid.)


Anyway, running the corrected version under 2.4.X gives me this:

Traceback (most recent call last):
File "C:\Python24\Lib\site-packages\pythonwin\pywin\framework
\scriptutils.py", line 310, in RunScript
exec codeObject in __main__.__dict__
File "C:\Documents and Settings\dentos\Desktop\scripting
\modify_message.py", line 19, in ?
test(lambda: unicode('\xe4'))
File "C:\Documents and Settings\dentos\Desktop\scripting
\modify_message.py", line 16, in test
raise modify_message(e, lambda: str(e) + ", sorry!")
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position
0: ordinal not in range(128), sorry!
 
S

samwyse

Hmmm, under Python 2.4.X, printing repr(old.__class__) gives me this:
<class exceptions.UnicodeDecodeError at 0x00A24F00>
while under 2.5.X, I get this:
<type 'exceptions.UnicodeDecodeError'>


So, let's try sub-classing the type:

def modify_message(old, f):
class Empty: pass
new = Empty()
print "old.__class__ =", repr(old.__class__)
print "Empty =", repr(Empty)
new.__class__ = Empty

class Excpt(old.__class__): pass
print "Excpt =", repr(Excpt)
print "Excpt.__class__ =", repr(Excpt.__class__)
new.__class__ = Excpt

new.__dict__ = old.__dict__.copy()
new.__str__ = f
return new

Nope, that gives us the same message:

old.__class__ = <type 'exceptions.UnicodeDecodeError'>
Empty = <class __main__.Empty at 0x00AB0AB0>
Excpt = <class '__main__.Excpt'>
Excpt.__class__ = <type 'type'>
Traceback (most recent call last):
[...]
TypeError: __class__ must be set to a class

Excpt ceratinly appears to be a class. Does anyone smarter than me
know what's going on here?
 
C

Christoph Zwerschke

samwyse said:
TypeError: __class__ must be set to a class

Excpt ceratinly appears to be a class. Does anyone smarter than me
know what's going on here?

Not that I want to appear smarter, but I think the problem here is that
exceptions are new-style classes now, whereas Empty is an old-style
class. But even if you define Empty as a new-style class, it will not
work, you get:

TypeError: __class__ assignment: only for heap types

This tells us that we cannot change the attributes of a built-in
exception. If it would be possible, I simply would have overridden the
__str__ method of the original exception in the first place.

-- Chris
 
S

samwyse

Hmmm, under Python 2.4.X, printing repr(old.__class__) gives me this:
<class exceptions.UnicodeDecodeError at 0x00A24F00>
while under 2.5.X, I get this:
<type 'exceptions.UnicodeDecodeError'>

So, let's try sub-classing the type:

def modify_message(old, f):
class Empty: pass
new = Empty()
print "old.__class__ =", repr(old.__class__)
print "Empty =", repr(Empty)
new.__class__ = Empty

class Excpt(old.__class__): pass
print "Excpt =", repr(Excpt)
print "Excpt.__class__ =", repr(Excpt.__class__)
new.__class__ = Excpt

new.__dict__ = old.__dict__.copy()
new.__str__ = f
return new

Nope, that gives us the same message:

old.__class__ = <type 'exceptions.UnicodeDecodeError'>
Empty = <class __main__.Empty at 0x00AB0AB0>
Excpt = <class '__main__.Excpt'>
Excpt.__class__ = <type 'type'>
Traceback (most recent call last):
[...]
TypeError: __class__ must be set to a class

Excpt certainly appears to be a class. Does anyone smarter than me
know what's going on here?

OK, in classobject.h, we find this:

#define PyClass_Check(op) ((op)->ob_type == &PyClass_Type)

That seems straightforward enough. And the relevant message appears
in classobject.c here:

static int
instance_setattr(PyInstanceObject *inst, PyObject *name, PyObject *v)
[...]
if (strcmp(sname, "__class__") == 0) {
if (v == NULL || !PyClass_Check(v)) {
PyErr_SetString(PyExc_TypeError,
"__class__ must be set to a class");
return -1;
}

Back in our test code, we got these:
Empty = <class __main__.Empty at 0x00AB0AB0>
Excpt = <class '__main__.Excpt'>

The first class (Empty) passes the PyClass_Check macro, the second one
(Excpt) evidently fails. I'll need to dig deeper. Meanwhile, I still
have to wonder why the code doesn't allow __class_ to be assigned a
type instead of a class. Why can't we do this in the C code (assuming
the appropriate PyType_Check macro):

if (v == NULL || !(PyClass_Check(v) || PyType_Check(v))) {
 
S

samwyse

(Yes, I probably should have said CPython in my subject, not Python.
Sorry.)

OK, in classobject.h, we find this:

#define PyClass_Check(op) ((op)->ob_type == &PyClass_Type)

That seems straightforward enough. And the relevant message appears
in classobject.c here:

static int
instance_setattr(PyInstanceObject *inst, PyObject *name, PyObject *v)
[...]
if (strcmp(sname, "__class__") == 0) {
if (v == NULL || !PyClass_Check(v)) {
PyErr_SetString(PyExc_TypeError,
"__class__ must be set to a class");
return -1;
}

Back in our test code, we got these:
Empty = <class __main__.Empty at 0x00AB0AB0>
Excpt = <class '__main__.Excpt'>

The first class (Empty) passes the PyClass_Check macro, the second one
(Excpt) evidently fails. I'll need to dig deeper. Meanwhile, I still
have to wonder why the code doesn't allow __class_ to be assigned a
type instead of a class. Why can't we do this in the C code (assuming
the appropriate PyType_Check macro):

if (v == NULL || !(PyClass_Check(v) || PyType_Check(v))) {

After a good night's sleep, I can see that Empty is a "real" class;
i.e. its repr() is handled by class_repr() in classobject.c. Excpt,
on the other hand, is a type; i.e. its repr is handled by type_repr()
in typeobject.c. (You can tell because class_repr() returns a value
formatted as "<class %s.%s at %p>" whereas type_repr returns a value
formatted as "<%s '%s.%s'>", where the first %s gets filled with
either "type" or "class".)

This is looking more and more like a failure to abide by PEP 252/253.
I think that the patch is simple, but I'm unusre of the
ramifications. I also haven't looked at the 2.4 source to see how
things used to work. Still, I think that I've got a work-around for
OP's problem, I just need to test it under both 2.4 and 2.5.
 
S

samwyse

Not that I want to appear smarter, but I think the problem here is that
exceptions are new-style classes now, whereas Empty is an old-style
class. But even if you define Empty as a new-style class, it will not
work, you get:

TypeError: __class__ assignment: only for heap types

This tells us that we cannot change the attributes of a built-in
exception. If it would be possible, I simply would have overridden the
__str__ method of the original exception in the first place.

-- Chris

Chris, you owe me a beer if you're ever in St. Louis, or I'm ever in
Germany.

# ----- CUT HERE -----

# Written by Sam Denton <[email protected]>
# You may use, copy, or distribute this work,
# as long as you give credit to the original author.

# tested successfully under Python 2.4.1, 2.4.3, 2.5.1

"""
What is the best way to re-raise any exception with a message
supplemented with additional information (e.g. line number in a
template)? Let's say for simplicity I just want to add "sorry" to
every exception message.

Here is an example of typical usage:
.... try:
.... code()
.... except Exception, e:
.... simplicity = lambda self: str(e) + ", sorry!"
.... raise modify_message(e, simplicity)

Note that if we want to re-cycle the original exception's message,
then we need our re-formatter (here called 'simplicity') to be
defined inside the exception handler. I tried verious approaches
to defining the re-formater, but all of them eventually needed a
closure; I decided that I liked this approach best.

This harness wraps the example so that doctest doesn't get upset.
.... try:
.... typical_usage(code)
.... except Exception, e:
.... print "%s: %s" % (e.__class__.__name__, str(e))

Now for some test cases:
ZeroDivisionError: integer division or modulo by zero, sorry!
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position
0: ordinal not in range(128), sorry!

"""

def modify_message(old, f):
"""modify_message(exception, mutator) --> exception

Modifies the string representation of an exception.
"""
class NewStyle(old.__class__):
def __init__(self): pass
NewStyle.__name__ = old.__class__.__name__
NewStyle.__str__ = f
new = NewStyle()
new.__dict__ = old.__dict__.copy()
return new

def _test():
import doctest
return doctest.testmod(verbose=True)

if __name__ == "__main__":
_test()
 
C

Christoph Zwerschke

samwyse said:
> NewStyle.__name__ = old.__class__.__name__

Simple, but that does the trick!
> new.__dict__ = old.__dict__.copy()

Unfortunately, that does not work, since the attributes are not
writeable and thus do not appear in __dict__.

But my __getattr__ solution does not work either, since the attributes
are set to None when initialized, so __getattr__ is never called.

Need to think about this point some more...

Anyway, the beer is on me ;-)

-- Chris
 
C

Christoph Zwerschke

Christoph said:
But my __getattr__ solution does not work either, since the attributes
are set to None when initialized, so __getattr__ is never called.

Here is a simple solution, but it depends on the existence of the args
attribute that "will eventually be deprecated" according to the docs:

def PoliteException(e):
E = e.__class__
class PoliteException(E):
def __str__(self):
return str(e) + ", sorry!"
PoliteException.__name__ = E.__name__
return PoliteException(*e.args)

try:
unicode('\xe4')
except Exception, e:
p = PoliteException(e)
assert p.reason == e.reason
raise p
 
C

Christoph Zwerschke

Christoph said:
Here is a simple solution, but it depends on the existence of the args
attribute that "will eventually be deprecated" according to the docs:

Ok, here is another solution that does not depend on args:

def PoliteException(e):
E = e.__class__
class PoliteException(E):
def __init__(self):
for arg in dir(e):
if not arg.startswith('_'):
setattr(self, arg, getattr(e, arg))
def __str__(self):
return str(e) + ", sorry!"
PoliteException.__name__ = E.__name__
return PoliteException()

try:
unicode('\xe4')
except Exception, e:
p = PoliteException(e)
assert p.reason == e.reason
raise p
 
C

Christoph Zwerschke

Christoph said:
Here is a simple solution, but it depends on the existence of the args
attribute that "will eventually be deprecated" according to the docs:

Just found another amazingly simple solution that does neither use teh
..args (docs: "will eventually be deprecated") attribute nor the dir()
function (docs: "its detailed behavior may change across releases").
Instead it relies on the fact that the exception itselfs behaves like
its args tuple (checked with Py 2.3, 2.4 and 2.5).

As another twist, I set the wrapper exception module to the module of
the original exception so that the output looks more like the output of
the original exception (i.e. simply "UnicodeDecodeError" instead of
"__main__.UnicodeDecodeError").

The code now looks like this:

def PoliteException(e):
E = e.__class__
class PoliteException(E):
def __str__(self):
return str(e) + ", sorry!"
PoliteException.__name__ = E.__name__
PoliteException.__module__ = E.__module__
return PoliteException(*e)

try:
unicode('\xe4')
except Exception, e:
p = PoliteException(e)
assert p.reason == e.reason
raise p
 
F

fumanchu

Here is a simple solution, but it depends
on the existence of the args attribute that
"will eventually be deprecated" according
to the docs

If you don't mind using .args, then the solution is usually as simple
as:


try:
Thing.do(arg1, arg2)
except Exception, e:
e.args += (Thing.state, arg1, arg2)
raise


No over-engineering needed. ;)


Robert Brewer
System Architect
Amor Ministries
(e-mail address removed)
 
G

Gabriel Genellina

If you don't mind using .args, then the solution is usually as simple
as:


try:
Thing.do(arg1, arg2)
except Exception, e:
e.args += (Thing.state, arg1, arg2)
raise


No over-engineering needed. ;)

If you read enough of this long thread, you'll see that the original
requirement was to enhance the *message* displayed by a normal traceback -
the OP has no control over the callers, but wants to add useful
information to any exception.
Your code does not qualify:

py> try:
.... open("a file that does not exist")
.... except Exception,e:
.... e.args += ("Sorry",)
.... raise
....
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IOError: [Errno 2] No such file or directory: 'a file that does not exist'
py> try:
.... x = u"á".encode("ascii")
.... except Exception,e:
.... e.args += ("Sorry",)
.... raise
....
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe1' in
position 0:
ordinal not in range(128)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,596
Members
45,142
Latest member
DewittMill
Top