Repository: tornadoweb/tornado

Issue:
WebsocketHandler.set_nodelay results in assertion failure
In previous versions of Torando, WebsocketHandler.set_nodelay could be called from open to set the nodelay option.  In Tornado 6.0, this results in an assertion failure:

  File ".../tornado/websocket.py", line 561, in set_nodelay
    assert self.stream is not None


Discussion:
stonebig: Jupyterlab and Jupyter Notebook are both broken under Tornado-6.0 in waiting of this.
ploxiln: just mypy is dissatisfied:
```
tornado/websocket.py:561: error: Item "None" of "Optional[WebSocketProtocol]" has no attribute "stream"
```
stonebig: apparently, problem resolved per Notebook-5.7.5

https://github.com/jupyter/notebook/blob/master/docs/source/changelog.rst

Patch:
diff --git a/maint/scripts/download_wheels.py b/maint/scripts/download_wheels.py
deleted file mode 100755
index 03379058..00000000
--- a/maint/scripts/download_wheels.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env python3
-
-import asyncio
-import json
-import pathlib
-import sys
-from tornado.httpclient import AsyncHTTPClient
-
-BASE_URL = "https://ci.appveyor.com/api"
-
-
-async def fetch_job(directory, job):
-    http = AsyncHTTPClient()
-    artifacts = await http.fetch(f"{BASE_URL}/buildjobs/{job}/artifacts")
-    paths = [pathlib.PurePosixPath(a["fileName"]) for a in json.loads(artifacts.body)]
-
-    for path in paths:
-        artifact = await http.fetch(f"{BASE_URL}/buildjobs/{job}/artifacts/{path}")
-        with open(directory.joinpath(path.name), "wb") as f:
-            f.write(artifact.body)
-
-
-async def main():
-    http = AsyncHTTPClient()
-    try:
-        _, version = sys.argv
-    except ValueError:
-        print("usage: maint/scripts/download_wheels.py v6.0.1", file=sys.stderr)
-        sys.exit(1)
-
-    directory = pathlib.Path(f"downloads-{version}")
-    directory.mkdir(exist_ok=True)
-
-    build = await http.fetch(f"{BASE_URL}/projects/bdarnell/tornado/branch/{version}")
-    jobs = [job["jobId"] for job in json.loads(build.body)["build"]["jobs"]]
-
-    await asyncio.gather(*(fetch_job(directory, job) for job in jobs))
-
-
-if __name__ == "__main__":
-    asyncio.run(main())
diff --git a/tornado/platform/twisted.py b/tornado/platform/twisted.py
index e563094a..38816313 100644
--- a/tornado/platform/twisted.py
+++ b/tornado/platform/twisted.py
@@ -17,6 +17,8 @@
 This module lets you run applications and libraries written for
 Twisted in a Tornado application.  It can be used in two modes,
 depending on which library's underlying event loop you want to use.
+
+This module has been tested with Twisted versions 11.0.0 and newer.
 """
 
 import socket
diff --git a/tornado/websocket.py b/tornado/websocket.py
index 00d08bab..d991fee5 100644
--- a/tornado/websocket.py
+++ b/tornado/websocket.py
@@ -558,8 +558,8 @@ class WebSocketHandler(tornado.web.RequestHandler):
 
         .. versionadded:: 3.1
         """
-        assert self.stream is not None
-        self.stream.set_nodelay(value)
+        assert self.ws_connection is not None
+        self.ws_connection.set_nodelay(value)
 
     def on_connection_close(self) -> None:
         if self.ws_connection:
@@ -714,6 +714,10 @@ class WebSocketProtocol(abc.ABC):
     async def _receive_frame_loop(self) -> None:
         raise NotImplementedError()
 
+    @abc.abstractmethod
+    def set_nodelay(self, x: bool) -> None:
+        raise NotImplementedError()
+
 
 class _PerMessageDeflateCompressor(object):
     def __init__(
@@ -1345,6 +1349,9 @@ class WebSocketProtocol13(WebSocketProtocol):
         self.write_ping(b"")
         self.last_ping = now
 
+    def set_nodelay(self, x: bool) -> None:
+        self.stream.set_nodelay(x)
+
 
 class WebSocketClientConnection(simple_httpclient._HTTPConnection):
     """WebSocket client connection.


Test patch:
diff --git a/tornado/test/websocket_test.py b/tornado/test/websocket_test.py
index 715ecf1e..d4b7a741 100644
--- a/tornado/test/websocket_test.py
+++ b/tornado/test/websocket_test.py
@@ -198,6 +198,12 @@ class ErrorInAsyncOpenHandler(TestWebSocketHandler):
         raise Exception("boom")
 
 
+class NoDelayHandler(TestWebSocketHandler):
+    def open(self):
+        self.set_nodelay(True)
+        self.write_message("hello")
+
+
 class WebSocketBaseTestCase(AsyncHTTPTestCase):
     @gen.coroutine
     def ws_connect(self, path, **kwargs):
@@ -258,6 +264,7 @@ class WebSocketTest(WebSocketBaseTestCase):
                 ),
                 ("/error_in_open", ErrorInOpenHandler),
                 ("/error_in_async_open", ErrorInAsyncOpenHandler),
+                ("/nodelay", NoDelayHandler),
             ],
             template_loader=DictLoader({"message.html": "<b>{{ message }}</b>"}),
         )
@@ -562,6 +569,12 @@ class WebSocketTest(WebSocketBaseTestCase):
             res = yield ws.read_message()
         self.assertIsNone(res)
 
+    @gen_test
+    def test_nodelay(self):
+        ws = yield self.ws_connect("/nodelay")
+        res = yield ws.read_message()
+        self.assertEqual(res, "hello")
+
 
 class NativeCoroutineOnMessageHandler(TestWebSocketHandler):
     def initialize(self, **kwargs):