Use encodeutils for exception -> string function

The oslo.utils library now provides a better version of
this that always returns a unicode exception message, so
update our usage to use it (and remove our own local
function). This guarantee of unicode also means we have to
update a few other places to make sure we get back bytes
or unicode as needed.

Change-Id: I924380408aaf6d2aec418ceaaf623c75900268f7
This commit is contained in:
Joshua Harlow 2015-06-16 15:57:11 -07:00
parent 27baaf46ad
commit 0d884a2fc5
5 changed files with 81 additions and 34 deletions

View File

@ -38,6 +38,11 @@ Miscellaneous
.. automodule:: taskflow.utils.misc .. automodule:: taskflow.utils.misc
Mixins
~~~~~~
.. automodule:: taskflow.utils.mixins
Persistence Persistence
~~~~~~~~~~~ ~~~~~~~~~~~

View File

@ -20,6 +20,7 @@ import traceback
from oslo_utils import excutils from oslo_utils import excutils
from oslo_utils import reflection from oslo_utils import reflection
import six import six
from taskflow.utils import mixins
def raise_with_cause(exc_cls, message, *args, **kwargs): def raise_with_cause(exc_cls, message, *args, **kwargs):
@ -231,7 +232,7 @@ class NotImplementedError(NotImplementedError):
""" """
class WrappedFailure(Exception): class WrappedFailure(mixins.StrMixin, Exception):
"""Wraps one or several failure objects. """Wraps one or several failure objects.
When exception/s cannot be re-raised (for example, because the value and When exception/s cannot be re-raised (for example, because the value and
@ -284,20 +285,18 @@ class WrappedFailure(Exception):
return result return result
return None return None
def __str__(self): def __bytes__(self):
causes = [exception_message(cause) for cause in self._causes] buf = six.BytesIO()
return 'WrappedFailure: %s' % causes buf.write(b'WrappedFailure: [')
causes_gen = (six.binary_type(cause) for cause in self._causes)
buf.write(b", ".join(causes_gen))
buf.write(b']')
return buf.getvalue()
def __unicode__(self):
def exception_message(exc): buf = six.StringIO()
"""Return the string representation of exception. buf.write(u'WrappedFailure: [')
causes_gen = (six.text_type(cause) for cause in self._causes)
:param exc: exception object to get a string representation of. buf.write(u", ".join(causes_gen))
""" buf.write(u']')
# NOTE(imelnikov): Dealing with non-ascii data in python is difficult: return buf.getvalue()
# https://bugs.launchpad.net/taskflow/+bug/1275895
# https://bugs.launchpad.net/taskflow/+bug/1276053
try:
return six.text_type(exc)
except UnicodeError:
return str(exc)

View File

@ -16,6 +16,7 @@
import sys import sys
from oslo_utils import encodeutils
import six import six
from six.moves import cPickle as pickle from six.moves import cPickle as pickle
import testtools import testtools
@ -301,9 +302,19 @@ class NonAsciiExceptionsTestCase(test.TestCase):
def test_exception_with_non_ascii_str(self): def test_exception_with_non_ascii_str(self):
bad_string = chr(200) bad_string = chr(200)
fail = failure.Failure.from_exception(ValueError(bad_string)) excp = ValueError(bad_string)
self.assertEqual(fail.exception_str, bad_string) fail = failure.Failure.from_exception(excp)
self.assertEqual(str(fail), 'Failure: ValueError: %s' % bad_string) self.assertEqual(fail.exception_str,
encodeutils.exception_to_unicode(excp))
# This is slightly different on py2 vs py3... due to how
# __str__ or __unicode__ is called and what is expected from
# both...
if six.PY2:
msg = encodeutils.exception_to_unicode(excp)
expected = 'Failure: ValueError: %s' % msg.encode('utf-8')
else:
expected = u'Failure: ValueError: \xc8'
self.assertEqual(str(fail), expected)
def test_exception_non_ascii_unicode(self): def test_exception_non_ascii_unicode(self):
hi_ru = u'привет' hi_ru = u'привет'
@ -316,18 +327,11 @@ class NonAsciiExceptionsTestCase(test.TestCase):
def test_wrapped_failure_non_ascii_unicode(self): def test_wrapped_failure_non_ascii_unicode(self):
hi_cn = u'' hi_cn = u''
fail = ValueError(hi_cn) fail = ValueError(hi_cn)
self.assertEqual(hi_cn, exceptions.exception_message(fail)) self.assertEqual(hi_cn, encodeutils.exception_to_unicode(fail))
fail = failure.Failure.from_exception(fail) fail = failure.Failure.from_exception(fail)
wrapped_fail = exceptions.WrappedFailure([fail]) wrapped_fail = exceptions.WrappedFailure([fail])
if six.PY2:
# Python 2.x will unicode escape it, while python 3.3+ will not,
# so we sadly have to differentiate between these two...
expected_result = (u"WrappedFailure: " expected_result = (u"WrappedFailure: "
"[u'Failure: ValueError: %s']" "[Failure: ValueError: %s]" % (hi_cn))
% (hi_cn.encode("unicode-escape")))
else:
expected_result = (u"WrappedFailure: "
"['Failure: ValueError: %s']" % (hi_cn))
self.assertEqual(expected_result, six.text_type(wrapped_fail)) self.assertEqual(expected_result, six.text_type(wrapped_fail))
def test_failure_equality_with_non_ascii_str(self): def test_failure_equality_with_non_ascii_str(self):

View File

@ -19,12 +19,16 @@ import os
import sys import sys
import traceback import traceback
from oslo_utils import encodeutils
from oslo_utils import reflection from oslo_utils import reflection
import six import six
from taskflow import exceptions as exc from taskflow import exceptions as exc
from taskflow.utils import mixins
from taskflow.utils import schema_utils as su from taskflow.utils import schema_utils as su
_exception_message = encodeutils.exception_to_unicode
def _copy_exc_info(exc_info): def _copy_exc_info(exc_info):
if exc_info is None: if exc_info is None:
@ -65,7 +69,7 @@ def _are_equal_exc_info_tuples(ei1, ei2):
if ei1[0] is not ei2[0]: if ei1[0] is not ei2[0]:
return False return False
if not all((type(ei1[1]) == type(ei2[1]), if not all((type(ei1[1]) == type(ei2[1]),
exc.exception_message(ei1[1]) == exc.exception_message(ei2[1]), _exception_message(ei1[1]) == _exception_message(ei2[1]),
repr(ei1[1]) == repr(ei2[1]))): repr(ei1[1]) == repr(ei2[1]))):
return False return False
if ei1[2] == ei2[2]: if ei1[2] == ei2[2]:
@ -75,7 +79,7 @@ def _are_equal_exc_info_tuples(ei1, ei2):
return tb1 == tb2 return tb1 == tb2
class Failure(object): class Failure(mixins.StrMixin):
"""An immutable object that represents failure. """An immutable object that represents failure.
Failure objects encapsulate exception information so that they can be Failure objects encapsulate exception information so that they can be
@ -191,7 +195,7 @@ class Failure(object):
if not self._exc_type_names: if not self._exc_type_names:
raise TypeError("Invalid exception type '%s' (%s)" raise TypeError("Invalid exception type '%s' (%s)"
% (exc_info[0], type(exc_info[0]))) % (exc_info[0], type(exc_info[0])))
self._exception_str = exc.exception_message(self._exc_info[1]) self._exception_str = _exception_message(self._exc_info[1])
self._traceback_str = ''.join( self._traceback_str = ''.join(
traceback.format_tb(self._exc_info[2])) traceback.format_tb(self._exc_info[2]))
self._causes = kwargs.pop('causes', None) self._causes = kwargs.pop('causes', None)
@ -387,7 +391,7 @@ class Failure(object):
self._causes = tuple(self._extract_causes_iter(self.exception)) self._causes = tuple(self._extract_causes_iter(self.exception))
return self._causes return self._causes
def __str__(self): def __unicode__(self):
return self.pformat() return self.pformat()
def pformat(self, traceback=False): def pformat(self, traceback=False):

35
taskflow/utils/mixins.py Normal file
View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
class StrMixin(object):
"""Mixin that helps deal with the PY2 and PY3 method differences.
http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/ explains
why this is quite useful...
"""
if six.PY2:
def __str__(self):
try:
return self.__bytes__()
except AttributeError:
return self.__unicode__().encode('utf-8')
else:
def __str__(self):
return self.__unicode__()