Skip to content

Commit f84caf4

Browse files
committed
python#6108: unicode(exception) and str(exception) should return the same message
1 parent db69f01 commit f84caf4

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

Lib/test/test_exceptions.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,116 @@ def g():
430430
self.assertTrue("maximum recursion depth exceeded" in str(v), v)
431431

432432

433+
434+
# Helper class used by TestSameStrAndUnicodeMsg
435+
class ExcWithOverriddenStr(Exception):
436+
"""Subclass of Exception that accepts a keyword 'msg' arg that is
437+
returned by __str__. 'msg' won't be included in self.args"""
438+
def __init__(self, *args, **kwargs):
439+
self.msg = kwargs.pop('msg') # msg should always be present
440+
super(ExcWithOverriddenStr, self).__init__(*args, **kwargs)
441+
def __str__(self):
442+
return self.msg
443+
444+
445+
class TestSameStrAndUnicodeMsg(unittest.TestCase):
446+
"""unicode(err) should return the same message of str(err). See #6108"""
447+
448+
def check_same_msg(self, exc, msg):
449+
"""Helper function that checks if str(exc) == unicode(exc) == msg"""
450+
self.assertEqual(str(exc), msg)
451+
self.assertEqual(str(exc), unicode(exc))
452+
453+
def test_builtin_exceptions(self):
454+
"""Check same msg for built-in exceptions"""
455+
# These exceptions implement a __str__ method that uses the args
456+
# to create a better error message. unicode(e) should return the same
457+
# message.
458+
exceptions = [
459+
SyntaxError('invalid syntax', ('<string>', 1, 3, '2+*3')),
460+
IOError(2, 'No such file or directory'),
461+
KeyError('both should have the same quotes'),
462+
UnicodeDecodeError('ascii', '\xc3\xa0', 0, 1,
463+
'ordinal not in range(128)'),
464+
UnicodeEncodeError('ascii', u'\u1234', 0, 1,
465+
'ordinal not in range(128)')
466+
]
467+
for exception in exceptions:
468+
self.assertEqual(str(exception), unicode(exception))
469+
470+
def test_0_args(self):
471+
"""Check same msg for Exception with 0 args"""
472+
# str() and unicode() on an Exception with no args should return an
473+
# empty string
474+
self.check_same_msg(Exception(), '')
475+
476+
def test_0_args_with_overridden___str__(self):
477+
"""Check same msg for exceptions with 0 args and overridden __str__"""
478+
# str() and unicode() on an exception with overridden __str__ that
479+
# returns an ascii-only string should return the same string
480+
for msg in ('foo', u'foo'):
481+
self.check_same_msg(ExcWithOverriddenStr(msg=msg), msg)
482+
483+
# if __str__ returns a non-ascii unicode string str() should fail
484+
# but unicode() should return the unicode string
485+
e = ExcWithOverriddenStr(msg=u'f\xf6\xf6') # no args
486+
self.assertRaises(UnicodeEncodeError, str, e)
487+
self.assertEqual(unicode(e), u'f\xf6\xf6')
488+
489+
def test_1_arg(self):
490+
"""Check same msg for Exceptions with 1 arg"""
491+
for arg in ('foo', u'foo'):
492+
self.check_same_msg(Exception(arg), arg)
493+
494+
# if __str__ is not overridden and self.args[0] is a non-ascii unicode
495+
# string, str() should try to return str(self.args[0]) and fail.
496+
# unicode() should return unicode(self.args[0]) and succeed.
497+
e = Exception(u'f\xf6\xf6')
498+
self.assertRaises(UnicodeEncodeError, str, e)
499+
self.assertEqual(unicode(e), u'f\xf6\xf6')
500+
501+
def test_1_arg_with_overridden___str__(self):
502+
"""Check same msg for exceptions with overridden __str__ and 1 arg"""
503+
# when __str__ is overridden and __unicode__ is not implemented
504+
# unicode(e) returns the same as unicode(e.__str__()).
505+
for msg in ('foo', u'foo'):
506+
self.check_same_msg(ExcWithOverriddenStr('arg', msg=msg), msg)
507+
508+
# if __str__ returns a non-ascii unicode string, str() should fail
509+
# but unicode() should succeed.
510+
e = ExcWithOverriddenStr('arg', msg=u'f\xf6\xf6') # 1 arg
511+
self.assertRaises(UnicodeEncodeError, str, e)
512+
self.assertEqual(unicode(e), u'f\xf6\xf6')
513+
514+
def test_many_args(self):
515+
"""Check same msg for Exceptions with many args"""
516+
argslist = [
517+
(3, 'foo'),
518+
(1, u'foo', 'bar'),
519+
(4, u'f\xf6\xf6', u'bar', 'baz')
520+
]
521+
# both str() and unicode() should return a repr() of the args
522+
for args in argslist:
523+
self.check_same_msg(Exception(*args), repr(args))
524+
525+
def test_many_args_with_overridden___str__(self):
526+
"""Check same msg for exceptions with overridden __str__ and many args"""
527+
# if __str__ returns an ascii string / ascii unicode string
528+
# both str() and unicode() should succeed
529+
for msg in ('foo', u'foo'):
530+
e = ExcWithOverriddenStr('arg1', u'arg2', u'f\xf6\xf6', msg=msg)
531+
self.check_same_msg(e, msg)
532+
533+
# if __str__ returns a non-ascii unicode string, str() should fail
534+
# but unicode() should succeed
535+
e = ExcWithOverriddenStr('arg1', u'f\xf6\xf6', u'arg3', # 3 args
536+
msg=u'f\xf6\xf6')
537+
self.assertRaises(UnicodeEncodeError, str, e)
538+
self.assertEqual(unicode(e), u'f\xf6\xf6')
539+
540+
433541
def test_main():
434-
run_unittest(ExceptionTests)
542+
run_unittest(ExceptionTests, TestSameStrAndUnicodeMsg)
435543

436544
if __name__ == '__main__':
437545
test_main()

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ What's New in Python 2.7 alpha 2?
1212
Core and Builtins
1313
-----------------
1414

15+
- Issue #6108: unicode(exception) and str(exception) should return the same
16+
message when only __str__ (and not __unicode__) is overridden in the subclass.
17+
1518
- Issue #6834: replace the implementation for the 'python' and 'pythonw'
1619
executables on OSX.
1720

Objects/exceptions.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,21 @@ BaseException_unicode(PyBaseExceptionObject *self)
122122
{
123123
PyObject *out;
124124

125+
/* issue6108: if __str__ has been overridden in the subclass, unicode()
126+
should return the message returned by __str__ as used to happen
127+
before this method was implemented. */
128+
if (Py_TYPE(self)->tp_str != (reprfunc)BaseException_str) {
129+
PyObject *str;
130+
/* Unlike PyObject_Str, tp_str can return unicode (i.e. return the
131+
equivalent of unicode(e.__str__()) instead of unicode(str(e))). */
132+
str = Py_TYPE(self)->tp_str((PyObject*)self);
133+
if (str == NULL)
134+
return NULL;
135+
out = PyObject_Unicode(str);
136+
Py_DECREF(str);
137+
return out;
138+
}
139+
125140
switch (PyTuple_GET_SIZE(self->args)) {
126141
case 0:
127142
out = PyUnicode_FromString("");

0 commit comments

Comments
 (0)