Skip to content

Commit 213e25c

Browse files
authored
Better Handling of Asyncio (#1035)
* wip handle warnings * switch from pytest-tornasync to pytest-asyncio * wip * remove unused plugin * wip clean up manager * more progress * more cleanup * fix auth tests * more cleanup * wip * fix login test * clean up clean up and examples * lint and fix prerelease * bump to 3.8+ * lint * make fixtures compatible with pytest-tornasync again * more cleanup * autouse does not work in async fixtures without pytest-asyncio * fix handling of io_loop * more cleanup * try isolating the del method * add a way to close all open sockets * fix typing * more socket cleanup * allow socket warnings for now * skip some windows tests * deprecated run_sync_in_loop * bump pyupgrade
1 parent a8bf9cf commit 213e25c

24 files changed

+220
-226
lines changed

.github/workflows/integration-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
fail-fast: false
1212
matrix:
1313
os: [ubuntu-latest]
14-
python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7"]
14+
python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
1515
steps:
1616
- name: Checkout
1717
uses: actions/checkout@v2

.github/workflows/python-tests.yml

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,47 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
os: [ubuntu-latest, windows-latest, macos-latest]
21-
python-version: ["3.7", "3.10"]
21+
python-version: ["3.8", "3.11"]
2222
include:
2323
- os: windows-latest
2424
python-version: "3.9"
2525
- os: ubuntu-latest
2626
python-version: "pypy-3.8"
2727
- os: macos-latest
28-
python-version: "3.8"
28+
python-version: "3.10"
2929
steps:
3030
- name: Checkout
3131
uses: actions/checkout@v2
3232
- name: Base Setup
3333
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
3434
- name: Run the tests
3535
if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(matrix.os, 'windows') }}
36-
run: hatch run cov:test || hatch run cov:test --lf
36+
run: hatch run cov:test -W default || hatch run cov:test -W default --lf
3737
- name: Run the tests on pypy and windows
3838
if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(matrix.os, 'windows') }}
39-
run: hatch run test:test || hatch run test:test --lf
39+
run: hatch run test:test -W default || hatch run test:test -W default --lf
4040
- name: Coverage
4141
run: |
4242
pip install codecov
4343
codecov
4444
45+
client8:
46+
runs-on: ${{ matrix.os }}
47+
timeout-minutes: 20
48+
strategy:
49+
fail-fast: false
50+
matrix:
51+
os: [ubuntu-latest, windows-latest, macos-latest]
52+
python-version: ["3.10"]
53+
steps:
54+
- name: Checkout
55+
uses: actions/checkout@v2
56+
- name: Base Setup
57+
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
58+
- run: |
59+
pip install -U pre jupyter_client
60+
hatch run test:test || hatch run test:test --lf
61+
4562
pre-commit:
4663
name: pre-commit
4764
runs-on: ubuntu-latest
@@ -94,7 +111,7 @@ jobs:
94111
- name: Base Setup
95112
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
96113
with:
97-
python_version: "3.7"
114+
python_version: "3.8"
98115
- name: Install miniumum versions
99116
uses: jupyterlab/maintainer-tools/.github/actions/install-minimums@v1
100117
- name: Run the unit tests
@@ -110,11 +127,11 @@ jobs:
110127
- name: Base Setup
111128
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
112129
with:
113-
python_version: "3.11.0-beta - 3.11.0"
130+
python_version: "3.11"
114131
- name: Install the Python dependencies
115132
run: |
116133
pip install --no-deps .
117-
pip install --pre --upgrade "jupyter_server[test]"
134+
pip install --pre --upgrade ".[test]"
118135
- name: List installed packages
119136
run: |
120137
pip freeze

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ repos:
5050
rev: v3.2.0
5151
hooks:
5252
- id: pyupgrade
53-
args: [--py37-plus]
53+
args: [--py38-plus]
5454

5555
- repo: https://github.com/PyCQA/doc8
5656
rev: v1.0.0

examples/simple/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ You need `python3` to build and run the server extensions.
1010
# Clone, create a conda env and install from source.
1111
git clone https://github.com/jupyter/jupyter_server && \
1212
cd examples/simple && \
13-
conda create -y -n jupyter-server-example python=3.7 && \
13+
conda create -y -n jupyter-server-example python=3.9 && \
1414
conda activate jupyter-server-example && \
1515
pip install -e .[test]
1616
```

examples/simple/pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[pytest]
22
# Disable any upper exclusion.
33
norecursedirs =
4+
asyncio_mode = auto

examples/simple/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ def add_data_files(path):
3737
version=VERSION,
3838
description="Jupyter Server Example",
3939
long_description=open("README.md").read(),
40-
python_requires=">=3.7",
40+
python_requires=">=3.8",
4141
install_requires=[
4242
"jupyter_server",
4343
"jinja2",
4444
],
4545
extras_require={
46-
"test": ["pytest"],
46+
"test": ["pytest", "pytest-asyncio"],
4747
},
4848
include_package_data=True,
4949
cmdclass=cmdclass,

jupyter_server/__init__.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""The Jupyter Server"""
22
import os
33
import pathlib
4-
import subprocess
5-
import sys
64

75
DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
86
DEFAULT_TEMPLATE_PATH_LIST = [
@@ -21,12 +19,3 @@
2119

2220
def _cleanup():
2321
pass
24-
25-
26-
# patch subprocess on Windows for python<3.7
27-
# see https://bugs.python.org/issue37380
28-
# the fix for python3.7: https://github.com/python/cpython/pull/15706/files
29-
if sys.platform == "win32":
30-
if sys.version_info < (3, 7):
31-
subprocess._cleanup = _cleanup
32-
subprocess._active = None

jupyter_server/base/zmqhandlers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from jupyter_client.session import Session
2020
from tornado import ioloop, web
2121
from tornado.iostream import IOStream
22-
from tornado.websocket import WebSocketHandler
22+
from tornado.websocket import WebSocketClosedError, WebSocketHandler
2323

2424
from .handlers import JupyterHandler
2525

@@ -302,7 +302,10 @@ def _on_zmq_reply(self, stream, msg_list):
302302
except Exception:
303303
self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
304304
else:
305-
self.write_message(msg, binary=isinstance(msg, bytes))
305+
try:
306+
self.write_message(msg, binary=isinstance(msg, bytes))
307+
except WebSocketClosedError as e:
308+
self.log.warning(str(e))
306309

307310

308311
class AuthenticatedZMQStreamHandler(ZMQStreamHandler, JupyterHandler):

jupyter_server/pytest_plugin.py

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Jupyter Development Team.
22
# Distributed under the terms of the Modified BSD License.
3+
import asyncio
34
import importlib
45
import io
56
import json
@@ -9,11 +10,14 @@
910
import sys
1011
import urllib.parse
1112
from binascii import hexlify
13+
from contextlib import closing
1214

1315
import jupyter_core.paths
1416
import nbformat
1517
import pytest
1618
import tornado
19+
import tornado.testing
20+
from pytest_tornasync.plugin import AsyncHTTPServerClient
1721
from tornado.escape import url_escape
1822
from tornado.httpclient import HTTPClientError
1923
from tornado.websocket import WebSocketHandler
@@ -35,16 +39,17 @@
3539
]
3640

3741

38-
import asyncio
39-
40-
if os.name == "nt" and sys.version_info >= (3, 7):
42+
if os.name == "nt":
4143
asyncio.set_event_loop_policy(
4244
asyncio.WindowsSelectorEventLoopPolicy() # type:ignore[attr-defined]
4345
)
4446

4547

4648
# ============ Move to Jupyter Core =============
4749

50+
# Once the chunk below moves to Jupyter Core
51+
# use the fixtures directly from Jupyter Core.
52+
4853

4954
def mkdir(tmp_path, *parts):
5055
path = tmp_path.joinpath(*parts)
@@ -130,6 +135,55 @@ def jp_environ(
130135
# ================= End: Move to Jupyter core ================
131136

132137

138+
@pytest.fixture
139+
def asyncio_loop():
140+
loop = asyncio.new_event_loop()
141+
asyncio.set_event_loop(loop)
142+
yield loop
143+
loop.close()
144+
145+
146+
@pytest.fixture(autouse=True)
147+
def io_loop(asyncio_loop):
148+
async def get_tornado_loop():
149+
return tornado.ioloop.IOLoop.current()
150+
151+
return asyncio_loop.run_until_complete(get_tornado_loop())
152+
153+
154+
@pytest.fixture
155+
def http_server_client(http_server, io_loop):
156+
"""
157+
Create an asynchronous HTTP client that can fetch from `http_server`.
158+
"""
159+
160+
async def get_client():
161+
return AsyncHTTPServerClient(http_server=http_server)
162+
163+
client = io_loop.run_sync(get_client)
164+
with closing(client) as context:
165+
yield context
166+
167+
168+
@pytest.fixture
169+
def http_server(io_loop, http_server_port, jp_web_app):
170+
"""Start a tornado HTTP server that listens on all available interfaces."""
171+
172+
async def get_server():
173+
server = tornado.httpserver.HTTPServer(jp_web_app)
174+
server.add_socket(http_server_port[0])
175+
return server
176+
177+
server = io_loop.run_sync(get_server)
178+
yield server
179+
server.stop()
180+
181+
if hasattr(server, "close_all_connections"):
182+
io_loop.run_sync(server.close_all_connections)
183+
184+
http_server_port[0].close()
185+
186+
133187
@pytest.fixture
134188
def jp_server_config():
135189
"""Allows tests to setup their specific configuration values."""
@@ -167,7 +221,8 @@ def jp_extension_environ(jp_env_config_path, monkeypatch):
167221
@pytest.fixture
168222
def jp_http_port(http_server_port):
169223
"""Returns the port value from the http_server_port fixture."""
170-
return http_server_port[-1]
224+
yield http_server_port[-1]
225+
http_server_port[0].close()
171226

172227

173228
@pytest.fixture
@@ -216,8 +271,8 @@ def jp_configurable_serverapp(
216271
jp_base_url,
217272
tmp_path,
218273
jp_root_dir,
219-
io_loop,
220274
jp_logging_stream,
275+
asyncio_loop,
221276
):
222277
"""Starts a Jupyter Server instance based on
223278
the provided configuration values.
@@ -254,8 +309,9 @@ def _configurable_serverapp(
254309
):
255310
c = Config(config)
256311
c.NotebookNotary.db_file = ":memory:"
257-
token = hexlify(os.urandom(4)).decode("ascii")
258-
c.IdentityProvider.token = token
312+
if "token" not in c.ServerApp and not c.IdentityProvider.token:
313+
token = hexlify(os.urandom(4)).decode("ascii")
314+
c.IdentityProvider.token = token
259315

260316
# Allow tests to configure root_dir via a file, argv, or its
261317
# default (cwd) by specifying a value of None.
@@ -278,48 +334,29 @@ def _configurable_serverapp(
278334
app.log.propagate = True
279335
app.log.handlers = []
280336
# Initialize app without httpserver
281-
app.initialize(argv=argv, new_httpserver=False)
337+
if asyncio_loop.is_running():
338+
app.initialize(argv=argv, new_httpserver=False)
339+
else:
340+
341+
async def initialize_app():
342+
app.initialize(argv=argv, new_httpserver=False)
343+
344+
asyncio_loop.run_until_complete(initialize_app())
282345
# Reroute all logging StreamHandlers away from stdin/stdout since pytest hijacks
283346
# these streams and closes them at unfortunate times.
284347
stream_handlers = [h for h in app.log.handlers if isinstance(h, logging.StreamHandler)]
285348
for handler in stream_handlers:
286349
handler.setStream(jp_logging_stream)
287350
app.log.propagate = True
288351
app.log.handlers = []
289-
# Start app without ioloop
290352
app.start_app()
291353
return app
292354

293355
return _configurable_serverapp
294356

295357

296-
@pytest.fixture
297-
def jp_ensure_app_fixture(request):
298-
"""Ensures that the 'app' fixture used by pytest-tornasync
299-
is set to `jp_web_app`, the Tornado Web Application returned
300-
by the ServerApp in Jupyter Server, provided by the jp_web_app
301-
fixture in this module.
302-
303-
Note, this hardcodes the `app_fixture` option from
304-
pytest-tornasync to `jp_web_app`. If this value is configured
305-
to something other than the default, it will raise an exception.
306-
"""
307-
app_option = request.config.getoption("app_fixture")
308-
if app_option not in ["app", "jp_web_app"]:
309-
raise Exception(
310-
"jp_serverapp requires the `app-fixture` option "
311-
"to be set to 'jp_web_app`. Try rerunning the "
312-
"current tests with the option `--app-fixture "
313-
"jp_web_app`."
314-
)
315-
elif app_option == "app":
316-
# Manually set the app_fixture to `jp_web_app` if it's
317-
# not set already.
318-
request.config.option.app_fixture = "jp_web_app"
319-
320-
321358
@pytest.fixture(scope="function")
322-
def jp_serverapp(jp_ensure_app_fixture, jp_server_config, jp_argv, jp_configurable_serverapp):
359+
def jp_serverapp(jp_server_config, jp_argv, jp_configurable_serverapp):
323360
"""Starts a Jupyter Server instance based on the established configuration values."""
324361
return jp_configurable_serverapp(config=jp_server_config, argv=jp_argv)
325362

@@ -482,24 +519,13 @@ def inner(nbpath):
482519

483520

484521
@pytest.fixture(autouse=True)
485-
def jp_server_cleanup(io_loop):
522+
def jp_server_cleanup(asyncio_loop):
486523
yield
487524
app: ServerApp = ServerApp.instance()
488-
loop = io_loop.asyncio_loop
489-
loop.run_until_complete(app._cleanup())
525+
asyncio_loop.run_until_complete(app._cleanup())
490526
ServerApp.clear_instance()
491527

492528

493-
@pytest.fixture
494-
def jp_cleanup_subprocesses(jp_serverapp):
495-
"""DEPRECATED: The jp_server_cleanup fixture automatically cleans up the singleton ServerApp class"""
496-
497-
async def _():
498-
pass
499-
500-
return _
501-
502-
503529
@pytest.fixture
504530
def send_request(jp_fetch, jp_ws_fetch):
505531
"""Send to Jupyter Server and return response code."""

0 commit comments

Comments
 (0)