Skip to content
This repository has been archived by the owner on Apr 29, 2021. It is now read-only.

Commit

Permalink
Ugly hack to support Python 3.5 with the PEP 479
Browse files Browse the repository at this point in the history
  • Loading branch information
vstinner committed Feb 5, 2016
1 parent a8b8ad4 commit 9fb2a3d
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 45 deletions.
2 changes: 2 additions & 0 deletions TODO.rst
@@ -1,5 +1,7 @@
Unsorted "TODO" tasks:

* Python 3.5: Fix test_task_repr()
* Python 3.4: Fix test_asyncio()
* Drop platform without ssl module?
* streams.py:FIXME: should we support __aiter__ and __anext__ in Trollius?
* replace selectors.py with selectors34:
Expand Down
2 changes: 2 additions & 0 deletions doc/changelog.rst
Expand Up @@ -7,6 +7,8 @@ Version 2.1

Changes:

* Ugly hack to support Python 3.5 with the PEP 479. asyncio coroutines are
not supported on Python 3.5.
* Better exception traceback. Patch written by Dhawal Yogesh Bhanushali.
* Drop support for Python 2.6 and 3.2.

Expand Down
34 changes: 25 additions & 9 deletions tests/test_tasks.py
Expand Up @@ -159,6 +159,7 @@ def test_async_warning(self):
'function is deprecated, use ensure_'):
self.assertIs(f, asyncio.async(f))

@unittest.skipIf(PY35, 'FIXME: test broken on Python 3.5')
def test_get_stack(self):
non_local = {'T': None}

Expand Down Expand Up @@ -225,29 +226,37 @@ def notmuch():

coro = format_coroutine(coro_qualname, 'running', src,
t._source_traceback, generator=True)
self.assertEqual(repr(t),
'<Task pending %s cb=[<Dummy>()]>' % coro)
# FIXME: it correctly broken on Python 3.5+
if not coroutines._PEP479:
self.assertEqual(repr(t),
'<Task pending %s cb=[<Dummy>()]>' % coro)

# test cancelling Task
t.cancel() # Does not take immediate effect!
self.assertEqual(repr(t),
'<Task cancelling %s cb=[<Dummy>()]>' % coro)
# FIXME: it correctly broken on Python 3.5+
if not coroutines._PEP479:
self.assertEqual(repr(t),
'<Task cancelling %s cb=[<Dummy>()]>' % coro)

# test cancelled Task
self.assertRaises(asyncio.CancelledError,
self.loop.run_until_complete, t)
coro = format_coroutine(coro_qualname, 'done', src,
t._source_traceback)
self.assertEqual(repr(t),
'<Task cancelled %s>' % coro)
# FIXME: it correctly broken on Python 3.5+
if not coroutines._PEP479:
self.assertEqual(repr(t),
'<Task cancelled %s>' % coro)

# test finished Task
t = asyncio.Task(notmuch(), loop=self.loop)
self.loop.run_until_complete(t)
coro = format_coroutine(coro_qualname, 'done', src,
t._source_traceback)
self.assertEqual(repr(t),
"<Task finished %s result='abc'>" % coro)
# FIXME: it correctly broken on Python 3.5+
if not coroutines._PEP479:
self.assertEqual(repr(t),
"<Task finished %s result='abc'>" % coro)

def test_task_repr_coro_decorator(self):
self.loop.set_debug(False)
Expand Down Expand Up @@ -1647,6 +1656,9 @@ def call(arg):
cw.send(None)
try:
cw.send(arg)
except coroutines.ReturnException as ex:
ex.raised = True
return ex.value
except StopIteration as ex:
ex.raised = True
return ex.value
Expand Down Expand Up @@ -1689,7 +1701,11 @@ def kill_me(loop):
self.assertEqual(len(self.loop._ready), 0)

# remove the future used in kill_me(), and references to the task
del coro.gi_frame.f_locals['future']
if coroutines._PEP479:
coro = coro.gi_frame.f_locals.pop('coro')
del coro.gi_frame.f_locals['future']
else:
del coro.gi_frame.f_locals['future']
coro = None
source_traceback = task._source_traceback
task = None
Expand Down
124 changes: 90 additions & 34 deletions trollius/coroutines.py
Expand Up @@ -7,6 +7,7 @@
import opcode
import os
import sys
import textwrap
import traceback
import types

Expand Down Expand Up @@ -77,14 +78,50 @@ def yield_from_gen(gen):
_YIELD_FROM_BUG = False


if compat.PY33:
# Don't use the Return class on Python 3.3 and later to support asyncio
if compat.PY35:
return_base_class = Exception
else:
return_base_class = StopIteration

class ReturnException(return_base_class):
def __init__(self, *args):
return_base_class.__init__(self)
if not args:
self.value = None
elif len(args) == 1:
self.value = args[0]
else:
self.value = args
self.raised = False
if _DEBUG:
frame = sys._getframe(1)
self._source_traceback = traceback.extract_stack(frame)
# explicitly clear the reference to avoid reference cycles
frame = None
else:
self._source_traceback = None

def __del__(self):
if self.raised:
return

fmt = 'Return(%r) used without raise'
if self._source_traceback:
fmt += '\nReturn created at (most recent call last):\n'
tb = ''.join(traceback.format_list(self._source_traceback))
fmt += tb.rstrip()
logger.error(fmt, self.value)


if compat.PY33 and not compat.PY35:
# Don't use the Return class on Python 3.3 and 3.4 to support asyncio
# coroutines (to avoid the warning emited in Return destructor).
#
# The problem is that Return inherits from StopIteration. "yield from
# trollius_coroutine". Task._step() does not receive the Return exception,
# because "yield from" handles it internally. So it's not possible to set
# the raised attribute to True to avoid the warning in Return destructor.
# The problem is that ReturnException inherits from StopIteration.
# "yield from trollius_coroutine". Task._step() does not receive the Return
# exception, because "yield from" handles it internally. So it's not
# possible to set the raised attribute to True to avoid the warning in
# Return destructor.
def Return(*args):
if not args:
value = None
Expand All @@ -94,34 +131,7 @@ def Return(*args):
value = args
return StopIteration(value)
else:
class Return(StopIteration):
def __init__(self, *args):
StopIteration.__init__(self)
if not args:
self.value = None
elif len(args) == 1:
self.value = args[0]
else:
self.value = args
self.raised = False
if _DEBUG:
frame = sys._getframe(1)
self._source_traceback = traceback.extract_stack(frame)
# explicitly clear the reference to avoid reference cycles
frame = None
else:
self._source_traceback = None

def __del__(self):
if self.raised:
return

fmt = 'Return(%r) used without raise'
if self._source_traceback:
fmt += '\nReturn created at (most recent call last):\n'
tb = ''.join(traceback.format_list(self._source_traceback))
fmt += tb.rstrip()
logger.error(fmt, self.value)
Return = ReturnException


def debug_wrapper(gen):
Expand Down Expand Up @@ -297,6 +307,47 @@ def _wraps(wrapped,
else:
_wraps = functools.wraps

_PEP479 = (sys.version_info >= (3, 5))
if _PEP479:
# Need exec() because yield+return raises a SyntaxError on Python 2
exec(textwrap.dedent('''
def pep479_wrapper(func, coro_func):
@_wraps(func)
def pep479_wrapped(*args, **kw):
coro = coro_func(*args, **kw)
value = None
error = None
while True:
try:
if error is not None:
value = coro.throw(error)
elif value is not None:
value = coro.send(value)
else:
value = next(coro)
except RuntimeError:
# FIXME: special case for
# FIXME: "isinstance(exc.__context__, StopIteration)"?
raise
except StopIteration as exc:
return exc.value
except Return as exc:
exc.raised = True
return exc.value
except BaseException as exc:
raise
try:
value = yield value
error = None
except BaseException as exc:
value = None
error = exc
return pep479_wrapped
'''))


def coroutine(func):
"""Decorator to mark coroutines.
Expand Down Expand Up @@ -331,6 +382,11 @@ def coro(*args, **kw):
res = yield From(await_meth())
raise Return(res)

if _PEP479:
# FIXME: use @_wraps
coro = pep479_wrapper(func, coro)
coro = _wraps(func)(coro)

if not _DEBUG:
if _types_coroutine is None:
wrapper = coro
Expand Down
14 changes: 12 additions & 2 deletions trollius/tasks.py
Expand Up @@ -23,7 +23,7 @@
from . import executor
from . import futures
from .locks import Lock, Condition, Semaphore, _ContextManager
from .coroutines import coroutine, From, Return
from .coroutines import coroutine, From, Return, ReturnException



Expand Down Expand Up @@ -257,12 +257,22 @@ def _step(self, value=None, exc=None, exc_tb=None):
result = coro.throw(exc)
else:
result = coro.send(value)
# On Python 3.3 and Python 3.4, ReturnException is not used in
# practice. But this except is kept to have a single code base
# for all Python versions.
except coroutines.ReturnException as exc:
if isinstance(exc, ReturnException):
exc.raised = True
result = exc.value
else:
result = None
self.set_result(result)
except StopIteration as exc:
if compat.PY33:
# asyncio Task object? get the result of the coroutine
result = exc.value
else:
if isinstance(exc, Return):
if isinstance(exc, ReturnException):
exc.raised = True
result = exc.value
else:
Expand Down

0 comments on commit 9fb2a3d

Please sign in to comment.