Skip to content

Commit 76a2e32

Browse files
committed
Add future.utils.raise_from (issue #86)
1 parent 33e26d0 commit 76a2e32

File tree

2 files changed

+160
-2
lines changed

2 files changed

+160
-2
lines changed

future/tests/test_utils.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from future.builtins import *
99
from future.utils import (old_div, istext, isbytes, native, PY2, PY3,
1010
native_str, raise_, as_native_str, ensure_new_type,
11-
bytes_to_native_str)
11+
bytes_to_native_str, raise_from)
1212

1313
from numbers import Integral
1414
from future.tests.base import unittest, skip26
@@ -141,7 +141,16 @@ def with_traceback():
141141
except IOError as e:
142142
self.assertEqual(str(e), "An error")
143143

144-
144+
def test_raise_from_None(self):
145+
try:
146+
try:
147+
raise TypeError("foo")
148+
except:
149+
raise_from(ValueError(), None)
150+
except ValueError as e:
151+
self.assertTrue(isinstance(e.__context__, TypeError))
152+
self.assertIsNone(e.__cause__)
153+
145154
@skip26
146155
def test_as_native_str(self):
147156
"""
@@ -190,5 +199,97 @@ def test_bytes_to_native_str(self):
190199
self.assertEqual(type(s), native_str)
191200

192201

202+
class TestCause(unittest.TestCase):
203+
"""
204+
Except for the first method, these were adapted from Py3.3's
205+
Lib/test/test_raise.py.
206+
"""
207+
def test_normal_use(self):
208+
"""
209+
Adapted from PEP 3134 docs
210+
"""
211+
# Setup:
212+
class DatabaseError(Exception):
213+
pass
214+
215+
# Python 2 and 3:
216+
from future.utils import raise_from
217+
218+
class FileDatabase:
219+
def __init__(self, filename):
220+
try:
221+
self.file = open(filename)
222+
except IOError as exc:
223+
raise_from(DatabaseError('failed to open'), exc)
224+
225+
# Testing the above:
226+
try:
227+
fd = FileDatabase('non_existent_file.txt')
228+
except Exception as e:
229+
assert isinstance(e.__cause__, IOError) # FileNotFoundError on
230+
# Py3.3+ inherits from IOError
231+
232+
def testCauseSyntax(self):
233+
try:
234+
try:
235+
try:
236+
raise TypeError
237+
except Exception:
238+
raise_from(ValueError, None)
239+
except ValueError as exc:
240+
self.assertIsNone(exc.__cause__)
241+
self.assertTrue(exc.__suppress_context__)
242+
exc.__suppress_context__ = False
243+
raise exc
244+
except ValueError as exc:
245+
e = exc
246+
247+
self.assertIsNone(e.__cause__)
248+
self.assertFalse(e.__suppress_context__)
249+
self.assertIsInstance(e.__context__, TypeError)
250+
251+
def test_invalid_cause(self):
252+
try:
253+
raise_from(IndexError, 5)
254+
except TypeError as e:
255+
self.assertIn("exception cause", str(e))
256+
else:
257+
self.fail("No exception raised")
258+
259+
def test_class_cause(self):
260+
try:
261+
raise_from(IndexError, KeyError)
262+
except IndexError as e:
263+
self.assertIsInstance(e.__cause__, KeyError)
264+
else:
265+
self.fail("No exception raised")
266+
267+
@unittest.expectedFailure
268+
def test_instance_cause(self):
269+
cause = KeyError('blah')
270+
try:
271+
raise_from(IndexError, cause)
272+
except IndexError as e:
273+
# FAILS:
274+
self.assertTrue(e.__cause__ is cause)
275+
# Even this weaker version seems to fail, although repr(cause) looks correct.
276+
# Is there something strange about testing exceptions for equality?
277+
self.assertEqual(e.__cause__, cause)
278+
else:
279+
self.fail("No exception raised")
280+
281+
def test_erroneous_cause(self):
282+
class MyException(Exception):
283+
def __init__(self):
284+
raise RuntimeError()
285+
286+
try:
287+
raise_from(IndexError, MyException)
288+
except RuntimeError:
289+
pass
290+
else:
291+
self.fail("No exception raised")
292+
293+
193294
if __name__ == '__main__':
194295
unittest.main()

future/utils/__init__.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@
7474
import sys
7575
import numbers
7676
import functools
77+
import copy
78+
import inspect
79+
7780

7881
PY3 = sys.version_info[0] == 3
7982
PY2 = sys.version_info[0] == 2
@@ -336,7 +339,49 @@ def getexception():
336339
return sys.exc_info()[1]
337340

338341

342+
def _get_caller_globals_and_locals():
343+
"""
344+
Returns the globals and locals of the calling frame.
345+
346+
Is there an alternative to frame hacking here?
347+
"""
348+
caller_frame = inspect.stack()[2]
349+
myglobals = caller_frame[0].f_globals
350+
mylocals = caller_frame[0].f_locals
351+
return myglobals, mylocals
352+
353+
354+
def _repr_strip(mystring):
355+
"""
356+
Returns the string without any initial or final quotes.
357+
"""
358+
r = repr(mystring)
359+
if r.startswith("'") and r.endswith("'"):
360+
return r[1:-1]
361+
else:
362+
return r
363+
364+
339365
if PY3:
366+
def raise_from(exc, cause):
367+
"""
368+
Equivalent to:
369+
370+
raise EXCEPTION from CAUSE
371+
372+
on Python 3. (See PEP 3134).
373+
"""
374+
# Is either arg an exception class (e.g. IndexError) rather than
375+
# instance (e.g. IndexError('my message here')? If so, pass the
376+
# name of the class undisturbed through to "raise ... from ...".
377+
if isinstance(exc, type) and issubclass(exc, Exception):
378+
exc = exc.__name__
379+
if isinstance(cause, type) and issubclass(cause, Exception):
380+
cause = cause.__name__
381+
execstr = "raise " + _repr_strip(exc) + " from " + _repr_strip(cause)
382+
myglobals, mylocals = _get_caller_globals_and_locals()
383+
exec(execstr, myglobals, mylocals)
384+
340385
def raise_(tp, value=None, tb=None):
341386
"""
342387
A function that matches the Python 2.x ``raise`` statement. This
@@ -358,6 +403,18 @@ def raise_with_traceback(exc, traceback=Ellipsis):
358403
raise exc.with_traceback(traceback)
359404

360405
else:
406+
def raise_from(exc, cause):
407+
"""
408+
Equivalent to:
409+
410+
raise EXCEPTION from CAUSE
411+
412+
on Python 3. (See PEP 3134).
413+
"""
414+
exc_copy = copy.copy(exc)
415+
exc_copy.__cause__ = cause
416+
raise exc_copy
417+
361418
exec('''
362419
def raise_(tp, value=None, tb=None):
363420
raise tp, value, tb

0 commit comments

Comments
 (0)