Repository: tornadoweb/tornado

Issue:
asyncio's ensure_future for Python versions <3.4.4
**Version**
Python: 3.4.3
Tornado: 5.0

**Issue**
I use the `raven` Python library that has Tornado as a requirement. When installing raven in a new environment, a newer version of Tornado was installed (5.0) which resulted in the following exception:

```
  File "/opt/python/run/venv/local/lib/python3.4/site-packages/raven/transport/tornado.py", line 17, in <module>
    from tornado.httpclient import AsyncHTTPClient, HTTPClient
  File "/opt/python/run/venv/local/lib64/python3.4/site-packages/tornado/httpclient.py", line 49, in <module>
    from tornado import gen, httputil, stack_context
  File "/opt/python/run/venv/local/lib64/python3.4/site-packages/tornado/gen.py", line 1295, in <module>
    _wrap_awaitable = asyncio.ensure_future
AttributeError: 'module' object has no attribute 'ensure_future'
```

The previous version of Tornado I was using (4.5.3) does not have this error. Downgrading Tornado from 5.0 to 4.5.3 resolves the problem.

Looking into the issue, I believe it's caused by Tornado's incompatibility with using an older version of Python's `asyncio`. Should the import for `ensure_future` for Python version <3.4.4 
be `from asyncio import async as ensure_future` ?



Discussion:
Sigh. It's annoying when cpython introduces new asyncio features in patch releases. I guess we could fall back to using `asyncio.async` instead of `asyncio.ensure_future`, but I think it's a bit of a pain to do that in a way that's compatible with python 3.7 (where `async` is becoming a proper keyword). 

Is there some reason you're not running the latest patch release for your version of Python? We've had to set other minimum version requirements in Tornado 5.0 that are documented in the release notes (for python 2, we require 2.7.9, and for python 3.5 we require 3.5.2). Since we don't test with older patch releases (and it's not reasonable to test with every patch release of every supported minor release), it's best to stay on the latest. 
In my case I was using an Elastic Beanstalk Amazon AMI that had a Python version of 3.4.3. It's only in the last couple of weeks that this AMI has been updated to Python 3.4.7.

It's not a big deal for me to upgrade to the latest release, but it may catch a few others unaware.

Yes, this breaks the bootstrap action that installs Jupyter on EMR. https://aws.amazon.com/blogs/big-data/running-jupyter-notebook-and-jupyterhub-on-amazon-emr/
Note that Debian 8 (Jessie) has Python 3.4.2, and I believe that Jessie is still a very common base for offspring distros (especially the embedded-oriented ones).

Question: would it make sense to just ban the older 3.4.x releases by adding:
`'!=3.4.0, != 3.4.1, != 3.4.2, !=3.4.3'` to L137 in `setup.py` (https://github.com/tornadoweb/tornado/blob/master/setup.py )?

This way, once 5.1 is released, people would regain the ability to `pip install tornado` on Jessie-based distros/containers without breaking anything.

p.s. Of course, dropping Py3.4 support altogether would work just as well, if it fits your release plan...
OK, debian jessie is a good enough reason to do the conditional import dance in tornado 5.0.1. 

Resolving patch:
diff --git a/tornado/gen.py b/tornado/gen.py
--- a/tornado/gen.py
+++ b/tornado/gen.py
@@ -1292,7 +1292,12 @@ def _wrap_awaitable(x):
                         break
         raise Return(_r)
 else:
-    _wrap_awaitable = asyncio.ensure_future
+    try:
+        _wrap_awaitable = asyncio.ensure_future
+    except AttributeError:
+        # asyncio.ensure_future was introduced in Python 3.4.4, but
+        # Debian jessie still ships with 3.4.2 so try the old name.
+        _wrap_awaitable = getattr(asyncio, 'async')
 
 
 def convert_yielded(yielded):
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -281,7 +281,7 @@ def current(instance=True):
         else:
             try:
                 loop = asyncio.get_event_loop()
-            except RuntimeError:
+            except (RuntimeError, AssertionError):
                 if not instance:
                     return None
                 raise
diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py
--- a/tornado/platform/asyncio.py
+++ b/tornado/platform/asyncio.py
@@ -104,7 +104,7 @@ def _handle_events(self, fd, events):
     def start(self):
         try:
             old_loop = asyncio.get_event_loop()
-        except RuntimeError:
+        except (RuntimeError, AssertionError):
             old_loop = None
         try:
             self._setup_logging()
@@ -211,7 +211,7 @@ def make_current(self):
         if not self.is_current:
             try:
                 self.old_asyncio = asyncio.get_event_loop()
-            except RuntimeError:
+            except (RuntimeError, AssertionError):
                 self.old_asyncio = None
             self.is_current = True
         asyncio.set_event_loop(self.asyncio_loop)
@@ -270,7 +270,9 @@ class AnyThreadEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
     def get_event_loop(self):
         try:
             return super().get_event_loop()
-        except RuntimeError:
+        except (RuntimeError, AssertionError):
+            # This was an AssertionError in python 3.4.2 (which ships with debian jessie)
+            # and changed to a RuntimeError in 3.4.3.
             # "There is no current event loop in thread %r"
             loop = self.new_event_loop()
             self.set_event_loop(loop)


Test patch:
diff --git a/tornado/test/asyncio_test.py b/tornado/test/asyncio_test.py
--- a/tornado/test/asyncio_test.py
+++ b/tornado/test/asyncio_test.py
@@ -150,7 +150,7 @@ def get_and_close_event_loop():
     def run_policy_test(self, accessor, expected_type):
         # With the default policy, non-main threads don't get an event
         # loop.
-        self.assertRaises(RuntimeError,
+        self.assertRaises((RuntimeError, AssertionError),
                           self.executor.submit(accessor).result)
         # Set the policy and we can get a loop.
         asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())