Chain exceptions correctly on py3.x

In order to chain exceptions add a helper that can
be used to chain exceptions automatically (when able)
and use it in the various places we are already creating
a new exception with a prior cause so that on py3.x the
newly created exception has its 'cause' associated using
the syntax available to do chaining in py3.x (which formats
nicely as well in that python version).

Change-Id: Iffddb27dbfe80816d6032e4b5532a0011ceedc95
This commit is contained in:
Joshua Harlow
2015-02-11 18:22:20 -08:00
parent 28bece7c7c
commit b7eb26c546
2 changed files with 109 additions and 2 deletions

View File

@@ -15,17 +15,50 @@
# under the License.
import os
import sys
import traceback
import six
def raise_with_cause(exc_cls, message, *args, **kwargs):
"""Helper to raise + chain exceptions (when able) and associate a *cause*.
NOTE(harlowja): Since in py3.x exceptions can be chained (due to
:pep:`3134`) we should try to raise the desired exception with the given
*cause* (or extract a *cause* from the current stack if able) so that the
exception formats nicely in old and new versions of python. Since py2.x
does **not** support exception chaining (or formatting) our root exception
class has a :py:meth:`~taskflow.exceptions.TaskFlowException.pformat`
method that can be used to get *similar* information instead (and this
function makes sure to retain the *cause* in that case as well so
that the :py:meth:`~taskflow.exceptions.TaskFlowException.pformat` method
shows them).
:param exc_cls: the :py:class:`~taskflow.exceptions.TaskFlowException`
class to raise.
:param message: the text/str message that will be passed to
the exceptions constructor as its first positional
argument.
:param args: any additional positional arguments to pass to the
exceptions constructor.
:param kwargs: any additional keyword arguments to pass to the
exceptions constructor.
"""
if 'cause' not in kwargs:
exc_type, exc, exc_tb = sys.exc_info()
if exc is not None:
kwargs['cause'] = exc
del(exc_type, exc, exc_tb)
six.raise_from(exc_cls(message, *args, **kwargs), kwargs.get('cause'))
class TaskFlowException(Exception):
"""Base class for *most* exceptions emitted from this library.
NOTE(harlowja): in later versions of python we can likely remove the need
to have a cause here as PY3+ have implemented PEP 3134 which handles
chaining in a much more elegant manner.
to have a ``cause`` here as PY3+ have implemented :pep:`3134` which
handles chaining in a much more elegant manner.
:param message: the exception message, typically some string that is
useful for consumers to view when debugging or analyzing

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2013 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
import testtools
from taskflow import exceptions as exc
from taskflow import test
class TestExceptions(test.TestCase):
def test_cause(self):
capture = None
try:
raise exc.TaskFlowException("broken", cause=IOError("dead"))
except Exception as e:
capture = e
self.assertIsNotNone(capture)
self.assertIsInstance(capture, exc.TaskFlowException)
self.assertIsNotNone(capture.cause)
self.assertIsInstance(capture.cause, IOError)
def test_cause_pformat(self):
capture = None
try:
raise exc.TaskFlowException("broken", cause=IOError("dead"))
except Exception as e:
capture = e
self.assertIsNotNone(capture)
self.assertGreater(0, len(capture.pformat()))
def test_raise_with(self):
capture = None
try:
raise IOError('broken')
except Exception:
try:
exc.raise_with_cause(exc.TaskFlowException, 'broken')
except Exception as e:
capture = e
self.assertIsNotNone(capture)
self.assertIsInstance(capture, exc.TaskFlowException)
self.assertIsNotNone(capture.cause)
self.assertIsInstance(capture.cause, IOError)
@testtools.skipIf(not six.PY3, 'py3.x is not available')
def test_raise_with_cause(self):
capture = None
try:
raise IOError('broken')
except Exception:
try:
exc.raise_with_cause(exc.TaskFlowException, 'broken')
except Exception as e:
capture = e
self.assertIsNotNone(capture)
self.assertIsInstance(capture, exc.TaskFlowException)
self.assertIsNotNone(capture.cause)
self.assertIsInstance(capture.cause, IOError)
self.assertIsNotNone(capture.__cause__)
self.assertIsInstance(capture.__cause__, IOError)