diff --git a/.travis.yml b/.travis.yml index 2c31985d84..f770221f14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ before_install: fi install: - - pip install --pre .[test] + - pip install --pre .[test] $EXTRA_PIP - wget https://github.com/jgm/pandoc/releases/download/1.19.1/pandoc-1.19.1-1-amd64.deb && sudo dpkg -i pandoc-1.19.1-1-amd64.deb @@ -69,6 +69,10 @@ matrix: dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069) - python: 3.6 env: GROUP=docs + - python: 3.6 # extra test for older tornados + env: + - GROUP=python + - EXTRA_PIP="tornado<5" after_success: - codecov diff --git a/jupyter_server/base/zmqhandlers.py b/jupyter_server/base/zmqhandlers.py index a6830a5d47..99b64f9e04 100644 --- a/jupyter_server/base/zmqhandlers.py +++ b/jupyter_server/base/zmqhandlers.py @@ -172,7 +172,7 @@ def open(self, *args, **kwargs): def send_ping(self): """send a ping to keep the websocket alive""" - if self.stream.closed() and self.ping_callback is not None: + if self.ws_connection is None and self.ping_callback is not None: self.ping_callback.stop() return @@ -237,7 +237,7 @@ def _reserialize_reply(self, msg_or_list, channel=None): def _on_zmq_reply(self, stream, msg_list): # Sometimes this gets triggered when the on_close method is scheduled in the # eventloop but hasn't been called. - if self.stream.closed() or stream.closed(): + if self.ws_connection is None or stream.closed(): self.log.warning("zmq message arrived on closed channel") self.close() return @@ -281,7 +281,8 @@ def get(self, *args, **kwargs): # assign and yield in two step to avoid tornado 3 issues res = self.pre_get() yield gen.maybe_future(res) - super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs) + res = super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs) + yield gen.maybe_future(res) def initialize(self): self.log.debug("Initializing websocket connection %s", self.request.path) diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index de1b3ea6f6..7513402c8c 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -12,12 +12,30 @@ import sys from distutils.version import LooseVersion +try: + from inspect import isawaitable +except ImportError: + def isawaitable(f): + """If isawaitable is undefined, nothing is awaitable""" + return False + +try: + from concurrent.futures import Future as ConcurrentFuture +except ImportError: + class ConcurrentFuture: + """If concurrent.futures isn't importable, nothing will be a c.f.Future""" + pass + try: from urllib.parse import quote, unquote, urlparse except ImportError: from urllib import quote, unquote from urlparse import urlparse +# tornado.concurrent.Future is asyncio.Future +# in tornado >=5 with Python 3 +from tornado.concurrent import Future as TornadoFuture +from tornado import gen from ipython_genutils import py3compat # UF_HIDDEN is a stat flag not defined in the stat module. @@ -305,3 +323,32 @@ def _check_pid_posix(pid): check_pid = _check_pid_win32 else: check_pid = _check_pid_posix + + +def maybe_future(obj): + """Like tornado's gen.maybe_future + but more compatible with asyncio for recent versions + of tornado + """ + if isinstance(obj, TornadoFuture): + return obj + elif isawaitable(obj): + return asyncio.ensure_future(obj) + elif isinstance(obj, ConcurrentFuture): + return asyncio.wrap_future(obj) + else: + # not awaitable, wrap scalar in future + f = TornadoFuture() + f.set_result(obj) + return f + +# monkeypatch tornado gen.maybe_future +# on Python 3 +# TODO: remove monkeypatch after backporting smaller fix to 5.x +try: + import asyncio +except ImportError: + pass +else: + import tornado.gen + tornado.gen.maybe_future = maybe_future