Skip to content

Commit 7c0a98f

Browse files
committed
Merge branch 'raise_from' into v0.12.x
2 parents af5325e + b9ab878 commit 7c0a98f

File tree

4 files changed

+254
-3
lines changed

4 files changed

+254
-3
lines changed

docs/notebooks/Writing Python 2-3 compatible code.ipynb

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"metadata": {
33
"name": "",
4-
"signature": "sha256:a318d976dd8aacb67a39acbcafddb513645c61ae27874c0d32d4a64c1621ca35"
4+
"signature": "sha256:f4ac8456d9c7976cb130d88556b7bbe2c3494a960d29e3eeffdca80a91b080e3"
55
},
66
"nbformat": 3,
77
"nbformat_minor": 0,
@@ -294,6 +294,76 @@
294294
"metadata": {},
295295
"outputs": []
296296
},
297+
{
298+
"cell_type": "markdown",
299+
"metadata": {},
300+
"source": [
301+
"Exception chaining (PEP 3134):"
302+
]
303+
},
304+
{
305+
"cell_type": "code",
306+
"collapsed": false,
307+
"input": [
308+
"# Setup:\n",
309+
"class DatabaseError(Exception):\n",
310+
" pass"
311+
],
312+
"language": "python",
313+
"metadata": {},
314+
"outputs": [],
315+
"prompt_number": 3
316+
},
317+
{
318+
"cell_type": "code",
319+
"collapsed": false,
320+
"input": [
321+
"# Python 3 only\n",
322+
"class FileDatabase:\n",
323+
" def __init__(self, filename):\n",
324+
" try:\n",
325+
" self.file = open(filename)\n",
326+
" except IOError as exc:\n",
327+
" raise DatabaseError('failed to open') from exc"
328+
],
329+
"language": "python",
330+
"metadata": {},
331+
"outputs": []
332+
},
333+
{
334+
"cell_type": "code",
335+
"collapsed": false,
336+
"input": [
337+
"# Python 2 and 3:\n",
338+
"from future.utils import raise_from\n",
339+
"\n",
340+
"class FileDatabase:\n",
341+
" def __init__(self, filename):\n",
342+
" try:\n",
343+
" self.file = open(filename)\n",
344+
" except IOError as exc:\n",
345+
" raise_from(DatabaseError('failed to open'), exc)"
346+
],
347+
"language": "python",
348+
"metadata": {},
349+
"outputs": [],
350+
"prompt_number": 16
351+
},
352+
{
353+
"cell_type": "code",
354+
"collapsed": false,
355+
"input": [
356+
"# Testing the above:\n",
357+
"try:\n",
358+
" fd = FileDatabase('non_existent_file.txt')\n",
359+
"except Exception as e:\n",
360+
" assert isinstance(e.__cause__, IOError) # FileNotFoundError on Py3.3+ inherits from IOError"
361+
],
362+
"language": "python",
363+
"metadata": {},
364+
"outputs": [],
365+
"prompt_number": 17
366+
},
297367
{
298368
"cell_type": "heading",
299369
"level": 3,

docs/whatsnew.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ What's new in version 0.12.5
1313
(issue #81). It still converts ``obj.next()`` method calls to
1414
``next(obj)`` correctly.
1515
- Add :ref:`compatible-idioms` from Ed Schofield's PyConAU 2014 talk.
16+
- Add ``future.utils.raise_from`` as an equivalent to Py3's ``raise ... from ...`` syntax (issue #86).
1617

1718

1819
.. whats-new-0.12.4:

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: 79 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,40 @@ 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+
# Is either arg an exception class (e.g. IndexError) rather than
415+
# instance (e.g. IndexError('my message here')? If so, pass the
416+
# name of the class undisturbed through to "raise ... from ...".
417+
if isinstance(exc, type) and issubclass(exc, Exception):
418+
e = exc()
419+
# exc = exc.__name__
420+
# execstr = "e = " + _repr_strip(exc) + "()"
421+
# myglobals, mylocals = _get_caller_globals_and_locals()
422+
# exec(execstr, myglobals, mylocals)
423+
else:
424+
e = exc
425+
e.__suppress_context__ = False
426+
if isinstance(cause, type) and issubclass(cause, Exception):
427+
e.__cause__ = cause()
428+
e.__suppress_context__ = True
429+
elif cause is None:
430+
e.__cause__ = None
431+
e.__suppress_context__ = True
432+
elif isinstance(cause, BaseException):
433+
e.__cause__ = cause
434+
e.__suppress_context__ = True
435+
else:
436+
raise TypeError("exception causes must derive from BaseException")
437+
e.__context__ = sys.exc_info()[1]
438+
raise e
439+
361440
exec('''
362441
def raise_(tp, value=None, tb=None):
363442
raise tp, value, tb

0 commit comments

Comments
 (0)