Repository: tornadoweb/tornado
Pull request #2375 (2018-05-04 23:25:59)

Deprecate web.asynchronous, stack_context, and more
This is the last planned round of deprecations for 5.1. `tornado.web.asynchronous`, the whole `stack_context` module, the callback argument to `RequestHandler.flush` and `TornadoReactor` are all going away. `AsyncTestCase.stop` and `wait` are doc-deprecated but won't be removed. 


Patch:
diff --git a/tornado/auth.py b/tornado/auth.py
index 0069efcb..36a08dfb 100644
--- a/tornado/auth.py
+++ b/tornado/auth.py
@@ -136,7 +136,7 @@ def _auth_return_future(f):
             else:
                 future_set_exc_info(future, (typ, value, tb))
                 return True
-        with ExceptionStackContext(handle_exception):
+        with ExceptionStackContext(handle_exception, delay_warning=True):
             f(*args, **kwargs)
         return future
     return wrapper
diff --git a/tornado/concurrent.py b/tornado/concurrent.py
index 85076681..398aa643 100644
--- a/tornado/concurrent.py
+++ b/tornado/concurrent.py
@@ -528,7 +528,7 @@ def return_future(f):
             future_set_exc_info(future, (typ, value, tb))
             return True
         exc_info = None
-        with ExceptionStackContext(handle_error):
+        with ExceptionStackContext(handle_error, delay_warning=True):
             try:
                 result = f(*args, **kwargs)
                 if result is not None:
diff --git a/tornado/http1connection.py b/tornado/http1connection.py
index 1c5eadf8..af7abe7c 100644
--- a/tornado/http1connection.py
+++ b/tornado/http1connection.py
@@ -21,6 +21,7 @@
 from __future__ import absolute_import, division, print_function
 
 import re
+import warnings
 
 from tornado.concurrent import (Future, future_add_done_callback,
                                 future_set_result_unless_cancelled)
@@ -401,6 +402,8 @@ class HTTP1Connection(httputil.HTTPConnection):
             future.exception()
         else:
             if callback is not None:
+                warnings.warn("callback argument is deprecated, use returned Future instead",
+                              DeprecationWarning)
                 self._write_callback = stack_context.wrap(callback)
             else:
                 future = self._write_future = Future()
@@ -440,6 +443,8 @@ class HTTP1Connection(httputil.HTTPConnection):
             self._write_future.exception()
         else:
             if callback is not None:
+                warnings.warn("callback argument is deprecated, use returned Future instead",
+                              DeprecationWarning)
                 self._write_callback = stack_context.wrap(callback)
             else:
                 future = self._write_future = Future()
diff --git a/tornado/httputil.py b/tornado/httputil.py
index 9c607b8c..22a64c31 100644
--- a/tornado/httputil.py
+++ b/tornado/httputil.py
@@ -591,6 +591,11 @@ class HTTPConnection(object):
         The ``version`` field of ``start_line`` is ignored.
 
         Returns a `.Future` if no callback is given.
+
+        .. deprecated:: 5.1
+
+           The ``callback`` argument is deprecated and will be removed
+           in Tornado 6.0.
         """
         raise NotImplementedError()
 
@@ -599,6 +604,11 @@ class HTTPConnection(object):
 
         The callback will be run when the write is complete.  If no callback
         is given, returns a Future.
+
+        .. deprecated:: 5.1
+
+           The ``callback`` argument is deprecated and will be removed
+           in Tornado 6.0.
         """
         raise NotImplementedError()
 
diff --git a/tornado/platform/twisted.py b/tornado/platform/twisted.py
index 4ae98be9..b38a755c 100644
--- a/tornado/platform/twisted.py
+++ b/tornado/platform/twisted.py
@@ -124,6 +124,13 @@ class TornadoReactor(PosixReactorBase):
 
     .. versionchanged:: 5.0
        The ``io_loop`` argument (deprecated since version 4.1) has been removed.
+
+    .. deprecated:: 5.1
+
+       This class will be removed in Tornado 6.0. Use
+       ``twisted.internet.asyncioreactor.AsyncioSelectorReactor``
+       instead.
+
     """
     def __init__(self):
         self._io_loop = tornado.ioloop.IOLoop.current()
@@ -350,6 +357,10 @@ def install():
     .. versionchanged:: 5.0
        The ``io_loop`` argument (deprecated since version 4.1) has been removed.
 
+    .. deprecated:: 5.1
+
+       This functio will be removed in Tornado 6.0. Use
+       ``twisted.internet.asyncioreactor.install`` instead.
     """
     reactor = TornadoReactor()
     from twisted.internet.main import installReactor  # type: ignore
@@ -411,6 +422,11 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
 
     See also :meth:`tornado.ioloop.IOLoop.install` for general notes on
     installing alternative IOLoops.
+
+    .. deprecated:: 5.1
+
+       The `asyncio` event loop will be the only available implementation in
+       Tornado 6.0.
     """
     def initialize(self, reactor=None, **kwargs):
         super(TwistedIOLoop, self).initialize(**kwargs)
diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py
index 7696dd18..4df4898a 100644
--- a/tornado/simple_httpclient.py
+++ b/tornado/simple_httpclient.py
@@ -230,7 +230,11 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
         # Timeout handle returned by IOLoop.add_timeout
         self._timeout = None
         self._sockaddr = None
-        with stack_context.ExceptionStackContext(self._handle_exception):
+        IOLoop.current().add_callback(self.run)
+
+    @gen.coroutine
+    def run(self):
+        try:
             self.parsed = urlparse.urlsplit(_unicode(self.request.url))
             if self.parsed.scheme not in ("http", "https"):
                 raise ValueError("Unsupported url scheme: %s" %
@@ -248,7 +252,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
                 host = host[1:-1]
             self.parsed_hostname = host  # save final host for _on_connect
 
-            if request.allow_ipv6 is False:
+            if self.request.allow_ipv6 is False:
                 af = socket.AF_INET
             else:
                 af = socket.AF_UNSPEC
@@ -260,10 +264,93 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
                 self._timeout = self.io_loop.add_timeout(
                     self.start_time + timeout,
                     stack_context.wrap(functools.partial(self._on_timeout, "while connecting")))
-            fut = self.tcp_client.connect(host, port, af=af,
-                                          ssl_options=ssl_options,
-                                          max_buffer_size=self.max_buffer_size)
-            fut.add_done_callback(stack_context.wrap(self._on_connect))
+                stream = yield self.tcp_client.connect(
+                    host, port, af=af,
+                    ssl_options=ssl_options,
+                    max_buffer_size=self.max_buffer_size)
+
+                if self.final_callback is None:
+                    # final_callback is cleared if we've hit our timeout.
+                    stream.close()
+                    return
+                self.stream = stream
+                self.stream.set_close_callback(self.on_connection_close)
+                self._remove_timeout()
+                if self.final_callback is None:
+                    return
+                if self.request.request_timeout:
+                    self._timeout = self.io_loop.add_timeout(
+                        self.start_time + self.request.request_timeout,
+                        stack_context.wrap(functools.partial(self._on_timeout, "during request")))
+                if (self.request.method not in self._SUPPORTED_METHODS and
+                        not self.request.allow_nonstandard_methods):
+                    raise KeyError("unknown method %s" % self.request.method)
+                for key in ('network_interface',
+                            'proxy_host', 'proxy_port',
+                            'proxy_username', 'proxy_password',
+                            'proxy_auth_mode'):
+                    if getattr(self.request, key, None):
+                        raise NotImplementedError('%s not supported' % key)
+                if "Connection" not in self.request.headers:
+                    self.request.headers["Connection"] = "close"
+                if "Host" not in self.request.headers:
+                    if '@' in self.parsed.netloc:
+                        self.request.headers["Host"] = self.parsed.netloc.rpartition('@')[-1]
+                    else:
+                        self.request.headers["Host"] = self.parsed.netloc
+                username, password = None, None
+                if self.parsed.username is not None:
+                    username, password = self.parsed.username, self.parsed.password
+                elif self.request.auth_username is not None:
+                    username = self.request.auth_username
+                    password = self.request.auth_password or ''
+                if username is not None:
+                    if self.request.auth_mode not in (None, "basic"):
+                        raise ValueError("unsupported auth_mode %s",
+                                         self.request.auth_mode)
+                    auth = utf8(username) + b":" + utf8(password)
+                    self.request.headers["Authorization"] = (b"Basic " +
+                                                             base64.b64encode(auth))
+                if self.request.user_agent:
+                    self.request.headers["User-Agent"] = self.request.user_agent
+                if not self.request.allow_nonstandard_methods:
+                    # Some HTTP methods nearly always have bodies while others
+                    # almost never do. Fail in this case unless the user has
+                    # opted out of sanity checks with allow_nonstandard_methods.
+                    body_expected = self.request.method in ("POST", "PATCH", "PUT")
+                    body_present = (self.request.body is not None or
+                                    self.request.body_producer is not None)
+                    if ((body_expected and not body_present) or
+                            (body_present and not body_expected)):
+                        raise ValueError(
+                            'Body must %sbe None for method %s (unless '
+                            'allow_nonstandard_methods is true)' %
+                            ('not ' if body_expected else '', self.request.method))
+                if self.request.expect_100_continue:
+                    self.request.headers["Expect"] = "100-continue"
+                if self.request.body is not None:
+                    # When body_producer is used the caller is responsible for
+                    # setting Content-Length (or else chunked encoding will be used).
+                    self.request.headers["Content-Length"] = str(len(
+                        self.request.body))
+                if (self.request.method == "POST" and
+                        "Content-Type" not in self.request.headers):
+                    self.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+                if self.request.decompress_response:
+                    self.request.headers["Accept-Encoding"] = "gzip"
+                req_path = ((self.parsed.path or '/') +
+                            (('?' + self.parsed.query) if self.parsed.query else ''))
+                self.connection = self._create_connection(stream)
+                start_line = httputil.RequestStartLine(self.request.method,
+                                                       req_path, '')
+                self.connection.write_headers(start_line, self.request.headers)
+                if self.request.expect_100_continue:
+                    yield self.connection.read_response(self)
+                else:
+                    yield self._write_body(True)
+        except Exception:
+            if not self._handle_exception(*sys.exc_info()):
+                raise
 
     def _get_ssl_options(self, scheme):
         if scheme == "https":
@@ -301,95 +388,14 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
         self._timeout = None
         error_message = "Timeout {0}".format(info) if info else "Timeout"
         if self.final_callback is not None:
-            raise HTTPTimeoutError(error_message)
+            self._handle_exception(HTTPTimeoutError, HTTPTimeoutError(error_message),
+                                   None)
 
     def _remove_timeout(self):
         if self._timeout is not None:
             self.io_loop.remove_timeout(self._timeout)
             self._timeout = None
 
-    def _on_connect(self, stream_fut):
-        stream = stream_fut.result()
-        if self.final_callback is None:
-            # final_callback is cleared if we've hit our timeout.
-            stream.close()
-            return
-        self.stream = stream
-        self.stream.set_close_callback(self.on_connection_close)
-        self._remove_timeout()
-        if self.final_callback is None:
-            return
-        if self.request.request_timeout:
-            self._timeout = self.io_loop.add_timeout(
-                self.start_time + self.request.request_timeout,
-                stack_context.wrap(functools.partial(self._on_timeout, "during request")))
-        if (self.request.method not in self._SUPPORTED_METHODS and
-                not self.request.allow_nonstandard_methods):
-            raise KeyError("unknown method %s" % self.request.method)
-        for key in ('network_interface',
-                    'proxy_host', 'proxy_port',
-                    'proxy_username', 'proxy_password',
-                    'proxy_auth_mode'):
-            if getattr(self.request, key, None):
-                raise NotImplementedError('%s not supported' % key)
-        if "Connection" not in self.request.headers:
-            self.request.headers["Connection"] = "close"
-        if "Host" not in self.request.headers:
-            if '@' in self.parsed.netloc:
-                self.request.headers["Host"] = self.parsed.netloc.rpartition('@')[-1]
-            else:
-                self.request.headers["Host"] = self.parsed.netloc
-        username, password = None, None
-        if self.parsed.username is not None:
-            username, password = self.parsed.username, self.parsed.password
-        elif self.request.auth_username is not None:
-            username = self.request.auth_username
-            password = self.request.auth_password or ''
-        if username is not None:
-            if self.request.auth_mode not in (None, "basic"):
-                raise ValueError("unsupported auth_mode %s",
-                                 self.request.auth_mode)
-            auth = utf8(username) + b":" + utf8(password)
-            self.request.headers["Authorization"] = (b"Basic " +
-                                                     base64.b64encode(auth))
-        if self.request.user_agent:
-            self.request.headers["User-Agent"] = self.request.user_agent
-        if not self.request.allow_nonstandard_methods:
-            # Some HTTP methods nearly always have bodies while others
-            # almost never do. Fail in this case unless the user has
-            # opted out of sanity checks with allow_nonstandard_methods.
-            body_expected = self.request.method in ("POST", "PATCH", "PUT")
-            body_present = (self.request.body is not None or
-                            self.request.body_producer is not None)
-            if ((body_expected and not body_present) or
-                    (body_present and not body_expected)):
-                raise ValueError(
-                    'Body must %sbe None for method %s (unless '
-                    'allow_nonstandard_methods is true)' %
-                    ('not ' if body_expected else '', self.request.method))
-        if self.request.expect_100_continue:
-            self.request.headers["Expect"] = "100-continue"
-        if self.request.body is not None:
-            # When body_producer is used the caller is responsible for
-            # setting Content-Length (or else chunked encoding will be used).
-            self.request.headers["Content-Length"] = str(len(
-                self.request.body))
-        if (self.request.method == "POST" and
-                "Content-Type" not in self.request.headers):
-            self.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
-        if self.request.decompress_response:
-            self.request.headers["Accept-Encoding"] = "gzip"
-        req_path = ((self.parsed.path or '/') +
-                    (('?' + self.parsed.query) if self.parsed.query else ''))
-        self.connection = self._create_connection(stream)
-        start_line = httputil.RequestStartLine(self.request.method,
-                                               req_path, '')
-        self.connection.write_headers(start_line, self.request.headers)
-        if self.request.expect_100_continue:
-            self._read_response()
-        else:
-            self._write_body(True)
-
     def _create_connection(self, stream):
         stream.set_nodelay(True)
         connection = HTTP1Connection(
@@ -402,31 +408,21 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
             self._sockaddr)
         return connection
 
+    @gen.coroutine
     def _write_body(self, start_read):
         if self.request.body is not None:
             self.connection.write(self.request.body)
         elif self.request.body_producer is not None:
             fut = self.request.body_producer(self.connection.write)
             if fut is not None:
-                fut = gen.convert_yielded(fut)
-
-                def on_body_written(fut):
-                    fut.result()
-                    self.connection.finish()
-                    if start_read:
-                        self._read_response()
-                self.io_loop.add_future(fut, on_body_written)
-                return
+                yield fut
         self.connection.finish()
         if start_read:
-            self._read_response()
-
-    def _read_response(self):
-        # Ensure that any exception raised in read_response ends up in our
-        # stack context.
-        self.io_loop.add_future(
-            self.connection.read_response(self),
-            lambda f: f.result())
+            try:
+                yield self.connection.read_response(self)
+            except StreamClosedError:
+                if not self._handle_exception(*sys.exc_info()):
+                    raise
 
     def _release(self):
         if self.release_callback is not None:
diff --git a/tornado/stack_context.py b/tornado/stack_context.py
index 2f26f384..a1eca4c7 100644
--- a/tornado/stack_context.py
+++ b/tornado/stack_context.py
@@ -64,12 +64,18 @@ Here are a few rules of thumb for when it's necessary:
   persist across asynchronous calls, create a new `StackContext` (or
   `ExceptionStackContext`), and make your asynchronous calls in a ``with``
   block that references your `StackContext`.
+
+.. deprecated:: 5.1
+
+   The ``stack_context`` package is deprecated and will be removed in
+   Tornado 6.0.
 """
 
 from __future__ import absolute_import, division, print_function
 
 import sys
 import threading
+import warnings
 
 from tornado.util import raise_exc_info
 
@@ -107,6 +113,8 @@ class StackContext(object):
     and not necessary in most applications.
     """
     def __init__(self, context_factory):
+        warnings.warn("StackContext is deprecated and will be removed in Tornado 6.0",
+                      DeprecationWarning)
         self.context_factory = context_factory
         self.contexts = []
         self.active = True
@@ -174,8 +182,20 @@ class ExceptionStackContext(object):
 
     If the exception handler returns true, the exception will be
     consumed and will not be propagated to other exception handlers.
+
+    .. versionadded:: 5.1
+
+       The ``delay_warning`` argument can be used to delay the emission
+       of DeprecationWarnings until an exception is caught by the
+       ``ExceptionStackContext``, which facilitates certain transitional
+       use cases.
     """
-    def __init__(self, exception_handler):
+    def __init__(self, exception_handler, delay_warning=False):
+        self.delay_warning = delay_warning
+        if not self.delay_warning:
+            warnings.warn(
+                "StackContext is deprecated and will be removed in Tornado 6.0",
+                DeprecationWarning)
         self.exception_handler = exception_handler
         self.active = True
 
@@ -184,6 +204,10 @@ class ExceptionStackContext(object):
 
     def exit(self, type, value, traceback):
         if type is not None:
+            if self.delay_warning:
+                warnings.warn(
+                    "StackContext is deprecated and will be removed in Tornado 6.0",
+                    DeprecationWarning)
             return self.exception_handler(type, value, traceback)
 
     def __enter__(self):
diff --git a/tornado/web.py b/tornado/web.py
index 1c077fd2..d7dbdb1c 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -78,6 +78,7 @@ import time
 import tornado
 import traceback
 import types
+import warnings
 from inspect import isclass
 from io import BytesIO
 
@@ -1022,6 +1023,20 @@ class RequestHandler(object):
         self.on_finish()
         self._break_cycles()
 
+    def detach(self):
+        """Take control of the underlying stream.
+
+        Returns the underlying `.IOStream` object and stops all
+        further HTTP processing. Intended for implementing protocols
+        like websockets that tunnel over an HTTP handshake.
+
+        This method is only supported when HTTP/1.1 is used.
+
+        .. versionadded:: 5.1
+        """
+        self._finished = True
+        return self.request.connection.detach()
+
     def _break_cycles(self):
         # Break up a reference cycle between this handler and the
         # _ui_module closures to allow for faster GC on CPython.
@@ -1688,7 +1703,14 @@ def asynchronous(method):
     .. versionchanged:: 4.3 Returning anything but ``None`` or a
        yieldable object from a method decorated with ``@asynchronous``
        is an error. Such return values were previously ignored silently.
+
+    .. deprecated:: 5.1
+
+       This decorator is deprecated and will be removed in Tornado 6.0.
+       Use coroutines instead.
     """
+    warnings.warn("@asynchronous is deprecated, use coroutines instead",
+                  DeprecationWarning)
     # Delay the IOLoop import because it's not available on app engine.
     from tornado.ioloop import IOLoop
 
@@ -1696,7 +1718,7 @@ def asynchronous(method):
     def wrapper(self, *args, **kwargs):
         self._auto_finish = False
         with stack_context.ExceptionStackContext(
-                self._stack_context_handle_exception):
+                self._stack_context_handle_exception, delay_warning=True):
             result = method(self, *args, **kwargs)
             if result is not None:
                 result = gen.convert_yielded(result)
diff --git a/tornado/websocket.py b/tornado/websocket.py
index 9a84188f..cc2d4cce 100644
--- a/tornado/websocket.py
+++ b/tornado/websocket.py
@@ -21,7 +21,6 @@ from __future__ import absolute_import, division, print_function
 import base64
 import hashlib
 import os
-import sys
 import struct
 import tornado.escape
 import tornado.web
@@ -146,7 +145,6 @@ class WebSocketHandler(tornado.web.RequestHandler):
         self.stream = None
         self._on_close_called = False
 
-    @tornado.web.asynchronous
     def get(self, *args, **kwargs):
         self.open_args = args
         self.open_kwargs = kwargs
@@ -482,7 +480,7 @@ class WebSocketHandler(tornado.web.RequestHandler):
                 self, compression_options=self.get_compression_options())
 
     def _attach_stream(self):
-        self.stream = self.request.connection.detach()
+        self.stream = self.detach()
         self.stream.set_close_callback(self.on_connection_close)
         # disable non-WS methods
         for method in ["write", "redirect", "set_header", "set_cookie",
@@ -513,7 +511,8 @@ class WebSocketProtocol(object):
         try:
             result = callback(*args, **kwargs)
         except Exception:
-            self.handler.log_exception(*sys.exc_info())
+            app_log.error("Uncaught exception in %s",
+                          getattr(self.request, 'path', None), exc_info=True)
             self._abort()
         else:
             if result is not None:


Test patch:
diff --git a/tornado/test/auth_test.py b/tornado/test/auth_test.py
index 0d26a35a..e0fd437b 100644
--- a/tornado/test/auth_test.py
+++ b/tornado/test/auth_test.py
@@ -26,17 +26,18 @@ class OpenIdClientLoginHandlerLegacy(RequestHandler, OpenIdMixin):
     def initialize(self, test):
         self._OPENID_ENDPOINT = test.get_url('/openid/server/authenticate')
 
-    @asynchronous
-    def get(self):
-        if self.get_argument('openid.mode', None):
-            with warnings.catch_warnings():
-                warnings.simplefilter('ignore', DeprecationWarning)
-                self.get_authenticated_user(
-                    self.on_user, http_client=self.settings['http_client'])
-                return
-        res = self.authenticate_redirect()
-        assert isinstance(res, Future)
-        assert res.done()
+    with ignore_deprecation():
+        @asynchronous
+        def get(self):
+            if self.get_argument('openid.mode', None):
+                with warnings.catch_warnings():
+                    warnings.simplefilter('ignore', DeprecationWarning)
+                    self.get_authenticated_user(
+                        self.on_user, http_client=self.settings['http_client'])
+                    return
+            res = self.authenticate_redirect()
+            assert isinstance(res, Future)
+            assert res.done()
 
     def on_user(self, user):
         if user is None:
@@ -78,16 +79,17 @@ class OAuth1ClientLoginHandlerLegacy(RequestHandler, OAuthMixin):
     def _oauth_consumer_token(self):
         return dict(key='asdf', secret='qwer')
 
-    @asynchronous
-    def get(self):
-        if self.get_argument('oauth_token', None):
-            with warnings.catch_warnings():
-                warnings.simplefilter('ignore', DeprecationWarning)
-                self.get_authenticated_user(
-                    self.on_user, http_client=self.settings['http_client'])
-            return
-        res = self.authorize_redirect(http_client=self.settings['http_client'])
-        assert isinstance(res, Future)
+    with ignore_deprecation():
+        @asynchronous
+        def get(self):
+            if self.get_argument('oauth_token', None):
+                with warnings.catch_warnings():
+                    warnings.simplefilter('ignore', DeprecationWarning)
+                    self.get_authenticated_user(
+                        self.on_user, http_client=self.settings['http_client'])
+                return
+            res = self.authorize_redirect(http_client=self.settings['http_client'])
+            assert isinstance(res, Future)
 
     def on_user(self, user):
         if user is None:
@@ -226,12 +228,13 @@ class TwitterClientHandler(RequestHandler, TwitterMixin):
 
 
 class TwitterClientLoginHandlerLegacy(TwitterClientHandler):
-    @asynchronous
-    def get(self):
-        if self.get_argument("oauth_token", None):
-            self.get_authenticated_user(self.on_user)
-            return
-        self.authorize_redirect()
+    with ignore_deprecation():
+        @asynchronous
+        def get(self):
+            if self.get_argument("oauth_token", None):
+                self.get_authenticated_user(self.on_user)
+                return
+            self.authorize_redirect()
 
     def on_user(self, user):
         if user is None:
diff --git a/tornado/test/concurrent_test.py b/tornado/test/concurrent_test.py
index 1df0532c..be6840ed 100644
--- a/tornado/test/concurrent_test.py
+++ b/tornado/test/concurrent_test.py
@@ -143,8 +143,9 @@ class ReturnFutureTest(AsyncTestCase):
 
     def test_delayed_failure(self):
         future = self.delayed_failure()
-        self.io_loop.add_future(future, self.stop)
-        future2 = self.wait()
+        with ignore_deprecation():
+            self.io_loop.add_future(future, self.stop)
+            future2 = self.wait()
         self.assertIs(future, future2)
         with self.assertRaises(ZeroDivisionError):
             future.result()
@@ -362,7 +363,7 @@ class ClientTestMixin(object):
     def test_callback_error(self):
         with ignore_deprecation():
             self.client.capitalize("HELLO", callback=self.stop)
-        self.assertRaisesRegexp(CapError, "already capitalized", self.wait)
+            self.assertRaisesRegexp(CapError, "already capitalized", self.wait)
 
     def test_future(self):
         future = self.client.capitalize("hello")
diff --git a/tornado/test/curl_httpclient_test.py b/tornado/test/curl_httpclient_test.py
index d0cfa979..b7a85952 100644
--- a/tornado/test/curl_httpclient_test.py
+++ b/tornado/test/curl_httpclient_test.py
@@ -108,9 +108,10 @@ class CurlHTTPClientTestCase(AsyncHTTPTestCase):
             error_event.set()
             return True
 
-        with ExceptionStackContext(error_handler):
-            request = HTTPRequest(self.get_url('/custom_reason'),
-                                  prepare_curl_callback=lambda curl: 1 / 0)
+        with ignore_deprecation():
+            with ExceptionStackContext(error_handler):
+                request = HTTPRequest(self.get_url('/custom_reason'),
+                                      prepare_curl_callback=lambda curl: 1 / 0)
         yield [error_event.wait(), self.http_client.fetch(request)]
         self.assertEqual(1, len(exc_info))
         self.assertIs(exc_info[0][0], ZeroDivisionError)
diff --git a/tornado/test/gen_test.py b/tornado/test/gen_test.py
index 8b2f62b9..721d927d 100644
--- a/tornado/test/gen_test.py
+++ b/tornado/test/gen_test.py
@@ -1368,7 +1368,6 @@ class GenCoroutineSequenceHandler(RequestHandler):
 
 
 class GenCoroutineUnfinishedSequenceHandler(RequestHandler):
-    @asynchronous
     @gen.coroutine
     def get(self):
         yield gen.moment
diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
index 851f1268..60c8f490 100644
--- a/tornado/test/httpclient_test.py
+++ b/tornado/test/httpclient_test.py
@@ -228,8 +228,9 @@ Transfer-Encoding: chunked
             if chunk == b'qwer':
                 1 / 0
 
-        with ExceptionStackContext(error_handler):
-            self.fetch('/chunk', streaming_callback=streaming_cb)
+        with ignore_deprecation():
+            with ExceptionStackContext(error_handler):
+                self.fetch('/chunk', streaming_callback=streaming_cb)
 
         self.assertEqual(chunks, [b'asdf', b'qwer'])
         self.assertEqual(1, len(exc_info))
@@ -344,8 +345,9 @@ Transfer-Encoding: chunked
             if header_line.lower().startswith('content-type:'):
                 1 / 0
 
-        with ExceptionStackContext(error_handler):
-            self.fetch('/chunk', header_callback=header_callback)
+        with ignore_deprecation():
+            with ExceptionStackContext(error_handler):
+                self.fetch('/chunk', header_callback=header_callback)
         self.assertEqual(len(exc_info), 1)
         self.assertIs(exc_info[0][0], ZeroDivisionError)
 
diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py
index 4bca757a..5abb7fe3 100644
--- a/tornado/test/httpserver_test.py
+++ b/tornado/test/httpserver_test.py
@@ -14,7 +14,7 @@ from tornado.netutil import ssl_options_to_context
 from tornado.simple_httpclient import SimpleAsyncHTTPClient
 from tornado.testing import AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCase, ExpectLog, gen_test  # noqa: E501
 from tornado.test.util import unittest, skipOnTravis, ignore_deprecation
-from tornado.web import Application, RequestHandler, asynchronous, stream_request_body
+from tornado.web import Application, RequestHandler, stream_request_body
 
 from contextlib import closing
 import datetime
@@ -668,9 +668,11 @@ class KeepAliveTest(AsyncHTTPTestCase):
                 self.write(''.join(chr(i % 256) * 1024 for i in range(512)))
 
         class FinishOnCloseHandler(RequestHandler):
-            @asynchronous
+            @gen.coroutine
             def get(self):
                 self.flush()
+                never_finish = Event()
+                yield never_finish.wait()
 
             def on_connection_close(self):
                 # This is not very realistic, but finishing the request
diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py
index b1b22b1a..646a1d43 100644
--- a/tornado/test/ioloop_test.py
+++ b/tornado/test/ioloop_test.py
@@ -25,7 +25,8 @@ from tornado.log import app_log
 from tornado.platform.select import _Select
 from tornado.stack_context import ExceptionStackContext, StackContext, wrap, NullContext
 from tornado.testing import AsyncTestCase, bind_unused_port, ExpectLog, gen_test
-from tornado.test.util import unittest, skipIfNonUnix, skipOnTravis, skipBefore35, exec_test
+from tornado.test.util import (unittest, skipIfNonUnix, skipOnTravis,
+                               skipBefore35, exec_test, ignore_deprecation)
 
 try:
     from concurrent import futures
@@ -416,17 +417,18 @@ class TestIOLoop(AsyncTestCase):
                 self.wait()
 
     def test_spawn_callback(self):
-        # An added callback runs in the test's stack_context, so will be
-        # re-raised in wait().
-        self.io_loop.add_callback(lambda: 1 / 0)
-        with self.assertRaises(ZeroDivisionError):
-            self.wait()
-        # A spawned callback is run directly on the IOLoop, so it will be
-        # logged without stopping the test.
-        self.io_loop.spawn_callback(lambda: 1 / 0)
-        self.io_loop.add_callback(self.stop)
-        with ExpectLog(app_log, "Exception in callback"):
-            self.wait()
+        with ignore_deprecation():
+            # An added callback runs in the test's stack_context, so will be
+            # re-raised in wait().
+            self.io_loop.add_callback(lambda: 1 / 0)
+            with self.assertRaises(ZeroDivisionError):
+                self.wait()
+            # A spawned callback is run directly on the IOLoop, so it will be
+            # logged without stopping the test.
+            self.io_loop.spawn_callback(lambda: 1 / 0)
+            self.io_loop.add_callback(self.stop)
+            with ExpectLog(app_log, "Exception in callback"):
+                self.wait()
 
     @skipIfNonUnix
     def test_remove_handler_from_handler(self):
@@ -535,11 +537,12 @@ class TestIOLoopAddCallback(AsyncTestCase):
             self.assertNotIn('c2', self.active_contexts)
             self.stop()
 
-        with StackContext(functools.partial(self.context, 'c1')):
-            wrapped = wrap(f1)
+        with ignore_deprecation():
+            with StackContext(functools.partial(self.context, 'c1')):
+                wrapped = wrap(f1)
 
-        with StackContext(functools.partial(self.context, 'c2')):
-            self.add_callback(wrapped)
+            with StackContext(functools.partial(self.context, 'c2')):
+                self.add_callback(wrapped)
 
         self.wait()
 
@@ -553,11 +556,12 @@ class TestIOLoopAddCallback(AsyncTestCase):
             self.assertNotIn('c2', self.active_contexts)
             self.stop((foo, bar))
 
-        with StackContext(functools.partial(self.context, 'c1')):
-            wrapped = wrap(f1)
+        with ignore_deprecation():
+            with StackContext(functools.partial(self.context, 'c1')):
+                wrapped = wrap(f1)
 
-        with StackContext(functools.partial(self.context, 'c2')):
-            self.add_callback(wrapped, 1, bar=2)
+            with StackContext(functools.partial(self.context, 'c2')):
+                self.add_callback(wrapped, 1, bar=2)
 
         result = self.wait()
         self.assertEqual(result, (1, 2))
@@ -602,11 +606,12 @@ class TestIOLoopFutures(AsyncTestCase):
 
         # stack_context propagates to the ioloop callback, but the worker
         # task just has its exceptions caught and saved in the Future.
-        with futures.ThreadPoolExecutor(1) as pool:
-            with ExceptionStackContext(handle_exception):
-                self.io_loop.add_future(pool.submit(task), callback)
-            ready.set()
-        self.wait()
+        with ignore_deprecation():
+            with futures.ThreadPoolExecutor(1) as pool:
+                with ExceptionStackContext(handle_exception):
+                    self.io_loop.add_future(pool.submit(task), callback)
+                ready.set()
+            self.wait()
 
         self.assertEqual(self.exception.args[0], "callback")
         self.assertEqual(self.future.exception().args[0], "worker")
diff --git a/tornado/test/netutil_test.py b/tornado/test/netutil_test.py
index 5c738185..b0c25c33 100644
--- a/tornado/test/netutil_test.py
+++ b/tornado/test/netutil_test.py
@@ -59,8 +59,8 @@ class _ResolverErrorTestMixin(object):
             self.stop(exc_val)
             return True  # Halt propagation.
 
-        with ExceptionStackContext(handler):
-            with ignore_deprecation():
+        with ignore_deprecation():
+            with ExceptionStackContext(handler):
                 self.resolver.resolve('an invalid domain', 80, callback=self.stop)
 
         result = self.wait()
diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py
index 818fdad8..378c13d3 100644
--- a/tornado/test/simple_httpclient_test.py
+++ b/tornado/test/simple_httpclient_test.py
@@ -17,6 +17,7 @@ from tornado.httpclient import AsyncHTTPClient
 from tornado.httputil import HTTPHeaders, ResponseStartLine
 from tornado.ioloop import IOLoop
 from tornado.iostream import UnsatisfiableReadError
+from tornado.locks import Event
 from tornado.log import gen_log
 from tornado.concurrent import Future
 from tornado.netutil import Resolver, bind_sockets
@@ -27,7 +28,7 @@ from tornado.testing import (AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCas
                              ExpectLog, gen_test)
 from tornado.test.util import (skipOnTravis, skipIfNoIPv6, refusing_port, skipBefore35,
                                exec_test, ignore_deprecation)
-from tornado.web import RequestHandler, Application, asynchronous, url, stream_request_body
+from tornado.web import RequestHandler, Application, url, stream_request_body
 
 
 class SimpleHTTPClientCommonTestCase(httpclient_test.HTTPClientCommonTestCase):
@@ -42,24 +43,26 @@ class TriggerHandler(RequestHandler):
         self.queue = queue
         self.wake_callback = wake_callback
 
-    @asynchronous
+    @gen.coroutine
     def get(self):
         logging.debug("queuing trigger")
         self.queue.append(self.finish)
         if self.get_argument("wake", "true") == "true":
             self.wake_callback()
+        never_finish = Event()
+        yield never_finish.wait()
 
 
 class HangHandler(RequestHandler):
-    @asynchronous
+    @gen.coroutine
     def get(self):
-        pass
+        never_finish = Event()
+        yield never_finish.wait()
 
 
 class ContentLengthHandler(RequestHandler):
-    @asynchronous
     def get(self):
-        self.stream = self.request.connection.detach()
+        self.stream = self.detach()
         IOLoop.current().spawn_callback(self.write_response)
 
     @gen.coroutine
@@ -107,13 +110,12 @@ class HostEchoHandler(RequestHandler):
 
 
 class NoContentLengthHandler(RequestHandler):
-    @asynchronous
     def get(self):
         if self.request.version.startswith('HTTP/1'):
             # Emulate the old HTTP/1.0 behavior of returning a body with no
             # content-length.  Tornado handles content-length at the framework
             # level so we have to go around it.
-            stream = self.request.connection.detach()
+            stream = self.detach()
             stream.write(b"HTTP/1.0 200 OK\r\n\r\n"
                          b"hello")
             stream.close()
diff --git a/tornado/test/stack_context_test.py b/tornado/test/stack_context_test.py
index 243ea47f..61411d2f 100644
--- a/tornado/test/stack_context_test.py
+++ b/tornado/test/stack_context_test.py
@@ -18,12 +18,13 @@ class TestRequestHandler(RequestHandler):
     def __init__(self, app, request):
         super(TestRequestHandler, self).__init__(app, request)
 
-    @asynchronous
-    def get(self):
-        logging.debug('in get()')
-        # call self.part2 without a self.async_callback wrapper.  Its
-        # exception should still get thrown
-        IOLoop.current().add_callback(self.part2)
+    with ignore_deprecation():
+        @asynchronous
+        def get(self):
+            logging.debug('in get()')
+            # call self.part2 without a self.async_callback wrapper.  Its
+            # exception should still get thrown
+            IOLoop.current().add_callback(self.part2)
 
     def part2(self):
         logging.debug('in part2()')
diff --git a/tornado/test/testing_test.py b/tornado/test/testing_test.py
index eb3445e9..9b47c92b 100644
--- a/tornado/test/testing_test.py
+++ b/tornado/test/testing_test.py
@@ -33,12 +33,13 @@ def set_environ(name, value):
 
 class AsyncTestCaseTest(AsyncTestCase):
     def test_exception_in_callback(self):
-        self.io_loop.add_callback(lambda: 1 / 0)
-        try:
-            self.wait()
-            self.fail("did not get expected exception")
-        except ZeroDivisionError:
-            pass
+        with ignore_deprecation():
+            self.io_loop.add_callback(lambda: 1 / 0)
+            try:
+                self.wait()
+                self.fail("did not get expected exception")
+            except ZeroDivisionError:
+                pass
 
     def test_wait_timeout(self):
         time = self.io_loop.time
@@ -69,15 +70,16 @@ class AsyncTestCaseTest(AsyncTestCase):
         self.wait(timeout=0.15)
 
     def test_multiple_errors(self):
-        def fail(message):
-            raise Exception(message)
-        self.io_loop.add_callback(lambda: fail("error one"))
-        self.io_loop.add_callback(lambda: fail("error two"))
-        # The first error gets raised; the second gets logged.
-        with ExpectLog(app_log, "multiple unhandled exceptions"):
-            with self.assertRaises(Exception) as cm:
-                self.wait()
-        self.assertEqual(str(cm.exception), "error one")
+        with ignore_deprecation():
+            def fail(message):
+                raise Exception(message)
+            self.io_loop.add_callback(lambda: fail("error one"))
+            self.io_loop.add_callback(lambda: fail("error two"))
+            # The first error gets raised; the second gets logged.
+            with ExpectLog(app_log, "multiple unhandled exceptions"):
+                with self.assertRaises(Exception) as cm:
+                    self.wait()
+            self.assertEqual(str(cm.exception), "error one")
 
 
 class AsyncHTTPTestCaseTest(AsyncHTTPTestCase):
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
index 7f053ef0..45072aac 100644
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -8,6 +8,7 @@ from tornado.httputil import format_timestamp
 from tornado.ioloop import IOLoop
 from tornado.iostream import IOStream
 from tornado import locale
+from tornado.locks import Event
 from tornado.log import app_log, gen_log
 from tornado.simple_httpclient import SimpleAsyncHTTPClient
 from tornado.template import DictLoader
@@ -369,9 +370,11 @@ class ConnectionCloseHandler(RequestHandler):
     def initialize(self, test):
         self.test = test
 
-    @asynchronous
+    @gen.coroutine
     def get(self):
         self.test.on_handler_waiting()
+        never_finish = Event()
+        yield never_finish.wait()
 
     def on_connection_close(self):
         self.test.on_connection_close()
@@ -548,14 +551,17 @@ class OptionalPathHandler(RequestHandler):
 class FlowControlHandler(RequestHandler):
     # These writes are too small to demonstrate real flow control,
     # but at least it shows that the callbacks get run.
-    @asynchronous
-    def get(self):
-        self.write("1")
-        self.flush(callback=self.step2)
+    with ignore_deprecation():
+        @asynchronous
+        def get(self):
+            self.write("1")
+            with ignore_deprecation():
+                self.flush(callback=self.step2)
 
     def step2(self):
         self.write("2")
-        self.flush(callback=self.step3)
+        with ignore_deprecation():
+            self.flush(callback=self.step3)
 
     def step3(self):
         self.write("3")
@@ -1805,27 +1811,29 @@ class MultipleExceptionTest(SimpleHandlerTestCase):
     class Handler(RequestHandler):
         exc_count = 0
 
-        @asynchronous
-        def get(self):
-            IOLoop.current().add_callback(lambda: 1 / 0)
-            IOLoop.current().add_callback(lambda: 1 / 0)
+        with ignore_deprecation():
+            @asynchronous
+            def get(self):
+                IOLoop.current().add_callback(lambda: 1 / 0)
+                IOLoop.current().add_callback(lambda: 1 / 0)
 
         def log_exception(self, typ, value, tb):
             MultipleExceptionTest.Handler.exc_count += 1
 
     def test_multi_exception(self):
-        # This test verifies that multiple exceptions raised into the same
-        # ExceptionStackContext do not generate extraneous log entries
-        # due to "Cannot send error response after headers written".
-        # log_exception is called, but it does not proceed to send_error.
-        response = self.fetch('/')
-        self.assertEqual(response.code, 500)
-        response = self.fetch('/')
-        self.assertEqual(response.code, 500)
-        # Each of our two requests generated two exceptions, we should have
-        # seen at least three of them by now (the fourth may still be
-        # in the queue).
-        self.assertGreater(MultipleExceptionTest.Handler.exc_count, 2)
+        with ignore_deprecation():
+            # This test verifies that multiple exceptions raised into the same
+            # ExceptionStackContext do not generate extraneous log entries
+            # due to "Cannot send error response after headers written".
+            # log_exception is called, but it does not proceed to send_error.
+            response = self.fetch('/')
+            self.assertEqual(response.code, 500)
+            response = self.fetch('/')
+            self.assertEqual(response.code, 500)
+            # Each of our two requests generated two exceptions, we should have
+            # seen at least three of them by now (the fourth may still be
+            # in the queue).
+            self.assertGreater(MultipleExceptionTest.Handler.exc_count, 2)
 
 
 @wsgi_safe
diff --git a/tornado/testing.py b/tornado/testing.py
index 04ea3816..06fa8a1c 100644
--- a/tornado/testing.py
+++ b/tornado/testing.py
@@ -265,7 +265,7 @@ class AsyncTestCase(unittest.TestCase):
             raise_exc_info(failure)
 
     def run(self, result=None):
-        with ExceptionStackContext(self._handle_exception):
+        with ExceptionStackContext(self._handle_exception, delay_warning=True):
             super(AsyncTestCase, self).run(result)
         # As a last resort, if an exception escaped super.run() and wasn't
         # re-raised in tearDown, raise it here.  This will cause the
@@ -279,6 +279,10 @@ class AsyncTestCase(unittest.TestCase):
 
         Keyword arguments or a single positional argument passed to `stop()` are
         saved and will be returned by `wait()`.
+
+        .. deprecated:: 5.1
+
+           `stop` and `wait` are deprecated; use ``@gen_test`` instead.
         """
         assert _arg is None or not kwargs
         self.__stop_args = kwargs or _arg
@@ -300,6 +304,10 @@ class AsyncTestCase(unittest.TestCase):
 
         .. versionchanged:: 3.1
            Added the ``ASYNC_TEST_TIMEOUT`` environment variable.
+
+        .. deprecated:: 5.1
+
+           `stop` and `wait` are deprecated; use ``@gen_test`` instead.
         """
         if timeout is None:
             timeout = get_async_test_timeout()