diff --git a/doc/source/utils.rst b/doc/source/utils.rst index 22a27089..d8da6c0c 100644 --- a/doc/source/utils.rst +++ b/doc/source/utils.rst @@ -38,6 +38,11 @@ Miscellaneous .. automodule:: taskflow.utils.misc +Mixins +~~~~~~ + +.. automodule:: taskflow.utils.mixins + Persistence ~~~~~~~~~~~ diff --git a/taskflow/exceptions.py b/taskflow/exceptions.py index 2f22b4f7..857d6634 100644 --- a/taskflow/exceptions.py +++ b/taskflow/exceptions.py @@ -20,6 +20,7 @@ import traceback from oslo_utils import excutils from oslo_utils import reflection import six +from taskflow.utils import mixins 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. When exception/s cannot be re-raised (for example, because the value and @@ -284,20 +285,18 @@ class WrappedFailure(Exception): return result return None - def __str__(self): - causes = [exception_message(cause) for cause in self._causes] - return 'WrappedFailure: %s' % causes + def __bytes__(self): + buf = six.BytesIO() + 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 exception_message(exc): - """Return the string representation of exception. - - :param exc: exception object to get a string representation of. - """ - # NOTE(imelnikov): Dealing with non-ascii data in python is difficult: - # 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) + def __unicode__(self): + buf = six.StringIO() + buf.write(u'WrappedFailure: [') + causes_gen = (six.text_type(cause) for cause in self._causes) + buf.write(u", ".join(causes_gen)) + buf.write(u']') + return buf.getvalue() diff --git a/taskflow/tests/unit/test_failure.py b/taskflow/tests/unit/test_failure.py index fab9cb9a..9e5b4f79 100644 --- a/taskflow/tests/unit/test_failure.py +++ b/taskflow/tests/unit/test_failure.py @@ -16,6 +16,7 @@ import sys +from oslo_utils import encodeutils import six from six.moves import cPickle as pickle import testtools @@ -301,9 +302,19 @@ class NonAsciiExceptionsTestCase(test.TestCase): def test_exception_with_non_ascii_str(self): bad_string = chr(200) - fail = failure.Failure.from_exception(ValueError(bad_string)) - self.assertEqual(fail.exception_str, bad_string) - self.assertEqual(str(fail), 'Failure: ValueError: %s' % bad_string) + excp = ValueError(bad_string) + fail = failure.Failure.from_exception(excp) + 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): hi_ru = u'привет' @@ -316,18 +327,11 @@ class NonAsciiExceptionsTestCase(test.TestCase): def test_wrapped_failure_non_ascii_unicode(self): hi_cn = u'嗨' 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) 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: " - "[u'Failure: ValueError: %s']" - % (hi_cn.encode("unicode-escape"))) - else: - expected_result = (u"WrappedFailure: " - "['Failure: ValueError: %s']" % (hi_cn)) + expected_result = (u"WrappedFailure: " + "[Failure: ValueError: %s]" % (hi_cn)) self.assertEqual(expected_result, six.text_type(wrapped_fail)) def test_failure_equality_with_non_ascii_str(self): diff --git a/taskflow/types/failure.py b/taskflow/types/failure.py index dafe73e6..a0084bb8 100644 --- a/taskflow/types/failure.py +++ b/taskflow/types/failure.py @@ -19,12 +19,16 @@ import os import sys import traceback +from oslo_utils import encodeutils from oslo_utils import reflection import six from taskflow import exceptions as exc +from taskflow.utils import mixins from taskflow.utils import schema_utils as su +_exception_message = encodeutils.exception_to_unicode + def _copy_exc_info(exc_info): if exc_info is None: @@ -65,7 +69,7 @@ def _are_equal_exc_info_tuples(ei1, ei2): if ei1[0] is not ei2[0]: return False 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]))): return False if ei1[2] == ei2[2]: @@ -75,7 +79,7 @@ def _are_equal_exc_info_tuples(ei1, ei2): return tb1 == tb2 -class Failure(object): +class Failure(mixins.StrMixin): """An immutable object that represents failure. Failure objects encapsulate exception information so that they can be @@ -191,7 +195,7 @@ class Failure(object): if not self._exc_type_names: raise TypeError("Invalid exception type '%s' (%s)" % (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( traceback.format_tb(self._exc_info[2])) self._causes = kwargs.pop('causes', None) @@ -389,7 +393,7 @@ class Failure(object): self._causes = tuple(self._extract_causes_iter(self.exception)) return self._causes - def __str__(self): + def __unicode__(self): return self.pformat() def pformat(self, traceback=False): diff --git a/taskflow/utils/mixins.py b/taskflow/utils/mixins.py new file mode 100644 index 00000000..5bb0fa47 --- /dev/null +++ b/taskflow/utils/mixins.py @@ -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__()