From 0d884a2fc5ec947960884767c998abc1b2d05b69 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 16 Jun 2015 15:57:11 -0700 Subject: [PATCH] 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 --- doc/source/utils.rst | 5 +++++ taskflow/exceptions.py | 33 +++++++++++++-------------- taskflow/tests/unit/test_failure.py | 30 ++++++++++++++----------- taskflow/types/failure.py | 12 ++++++---- taskflow/utils/mixins.py | 35 +++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 taskflow/utils/mixins.py diff --git a/doc/source/utils.rst b/doc/source/utils.rst index ac0dd5c4a..ab051237c 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 2f22b4f7a..857d6634d 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 fab9cb9a9..9e5b4f79a 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 d713098df..fa831ace6 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) @@ -387,7 +391,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 000000000..5bb0fa47f --- /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__()