Skip to content

Commit ecbb00c

Browse files
authored
Update event loop handling (#75)
* Update event loop handling * add docstring * prefer_selector_loop * fix event policy handling on windows * another windows fix * update docstring * fix event loop handling * always prefer selector loop * simplify matrix * fix api usage * fix deps * close event loop after each test
1 parent 25b2154 commit ecbb00c

File tree

8 files changed

+67
-117
lines changed

8 files changed

+67
-117
lines changed

.github/workflows/downstream.yml

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,27 @@ concurrency:
1010
cancel-in-progress: true
1111

1212
jobs:
13-
jupyter_client:
13+
jupyter_server:
1414
runs-on: ubuntu-latest
15+
timeout-minutes: 10
1516
steps:
16-
- name: Checkout
17-
uses: actions/checkout@v4
18-
19-
- name: Base Setup
20-
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
21-
22-
- name: Run Test
23-
uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
17+
- uses: actions/checkout@v4
18+
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
19+
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
2420
with:
25-
package_name: jupyter_client
21+
package_name: jupyter_server
22+
package_download_extra_args: --pre
2623

27-
jupyter_server:
24+
jupyter_client:
2825
runs-on: ubuntu-latest
29-
timeout-minutes: 15
26+
timeout-minutes: 10
3027
steps:
3128
- uses: actions/checkout@v4
3229
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
3330
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
3431
with:
35-
package_name: jupyter_server
32+
package_name: jupyter_client
33+
package_download_extra_args: --pre
3634

3735
downstream_check: # This job does nothing and is only used for the branch protection
3836
if: always()

.github/workflows/test.yml

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -141,28 +141,6 @@ jobs:
141141
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
142142
- uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
143143

144-
jupyter_server_downstream:
145-
runs-on: ubuntu-latest
146-
timeout-minutes: 10
147-
steps:
148-
- uses: actions/checkout@v4
149-
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
150-
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
151-
with:
152-
package_name: jupyter_server
153-
package_download_extra_args: --pre
154-
155-
jupyter_client_downstream:
156-
runs-on: ubuntu-latest
157-
timeout-minutes: 10
158-
steps:
159-
- uses: actions/checkout@v4
160-
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
161-
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
162-
with:
163-
package_name: jupyter_client
164-
package_download_extra_args: --pre
165-
166144
tests_check: # This job does nothing and is only used for the branch protection
167145
if: always()
168146
needs:
@@ -173,8 +151,6 @@ jobs:
173151
- test_prereleases
174152
- check_links
175153
- check_release
176-
- jupyter_server_downstream
177-
- jupyter_client_downstream
178154
- test_sdist
179155
runs-on: ubuntu-latest
180156
steps:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ classifiers = [
2828
]
2929
dependencies = [
3030
"pytest",
31-
"jupyter_core"
31+
"jupyter_core>=5.7"
3232
]
3333
requires-python = ">=3.8"
3434

pytest_jupyter/jupyter_client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
# Bring in local plugins.
2323
from pytest_jupyter.jupyter_core import * # noqa: F403
24-
from pytest_jupyter.pytest_tornasync import * # noqa: F403
2524

2625

2726
@pytest.fixture()

pytest_jupyter/jupyter_core.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""Fixtures for use with jupyter core and downstream."""
22
# Copyright (c) Jupyter Development Team.
33
# Distributed under the terms of the Modified BSD License.
4-
import asyncio
54
import json
65
import os
76
import sys
8-
import typing
7+
from inspect import iscoroutinefunction
98
from pathlib import Path
109

1110
import jupyter_core
1211
import pytest
12+
from jupyter_core.utils import ensure_event_loop
1313

1414
from .utils import mkdir
1515

@@ -35,34 +35,35 @@
3535
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
3636

3737

38-
@pytest.fixture()
38+
@pytest.fixture(autouse=True)
3939
def jp_asyncio_loop():
4040
"""Get an asyncio loop."""
41-
if os.name == "nt":
42-
asyncio.set_event_loop_policy(
43-
asyncio.WindowsSelectorEventLoopPolicy() # type:ignore[attr-defined]
44-
)
45-
loop = asyncio.new_event_loop()
46-
asyncio.set_event_loop(loop)
41+
loop = ensure_event_loop(prefer_selector_loop=True)
4742
yield loop
4843
loop.close()
4944

5045

51-
@pytest.fixture(autouse=True)
52-
def io_loop(jp_asyncio_loop):
53-
"""Override the io_loop for pytest_tornasync. This is a no-op
54-
if tornado is not installed."""
46+
@pytest.hookimpl(tryfirst=True)
47+
def pytest_pycollect_makeitem(collector, name, obj):
48+
"""Custom pytest collection hook."""
49+
if collector.funcnamefilter(name) and iscoroutinefunction(obj):
50+
return list(collector._genfunctions(name, obj))
51+
return None
52+
5553

56-
async def get_tornado_loop() -> typing.Any:
57-
"""Asynchronously get a tornado loop."""
58-
try:
59-
from tornado.ioloop import IOLoop
54+
@pytest.hookimpl(tryfirst=True)
55+
def pytest_pyfunc_call(pyfuncitem):
56+
"""Custom pytest function call hook."""
57+
funcargs = pyfuncitem.funcargs
58+
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
6059

61-
return IOLoop.current()
62-
except ImportError:
63-
pass
60+
if not iscoroutinefunction(pyfuncitem.obj):
61+
pyfuncitem.obj(**testargs)
62+
return True
6463

65-
return jp_asyncio_loop.run_until_complete(get_tornado_loop())
64+
loop = ensure_event_loop(prefer_selector_loop=True)
65+
loop.run_until_complete(pyfuncitem.obj(**testargs))
66+
return True
6667

6768

6869
@pytest.fixture()

pytest_jupyter/jupyter_server.py

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# Distributed under the terms of the Modified BSD License.
44
from __future__ import annotations
55

6-
import asyncio
76
import importlib
87
import io
98
import logging
@@ -53,36 +52,6 @@
5352
from pytest_jupyter.pytest_tornasync import * # noqa: F403
5453
from pytest_jupyter.utils import mkdir
5554

56-
# Override some of the fixtures from pytest_tornasync
57-
# The io_loop fixture is overridden in jupyter_core.py so it
58-
# can be shared by other plugins that need it (e.g. jupyter_client.py).
59-
60-
61-
@pytest.fixture()
62-
def http_server(io_loop, http_server_port, jp_web_app):
63-
"""Start a tornado HTTP server that listens on all available interfaces."""
64-
65-
async def get_server():
66-
"""Get a server asynchronously."""
67-
server = tornado.httpserver.HTTPServer(jp_web_app)
68-
server.add_socket(http_server_port[0])
69-
return server
70-
71-
server = io_loop.run_sync(get_server)
72-
yield server
73-
server.stop()
74-
75-
if hasattr(server, "close_all_connections"):
76-
try:
77-
io_loop.run_sync(server.close_all_connections)
78-
except asyncio.TimeoutError:
79-
pass
80-
81-
http_server_port[0].close()
82-
83-
84-
# End pytest_tornasync overrides
85-
8655

8756
@pytest.fixture()
8857
def jp_server_config():
@@ -177,7 +146,6 @@ def jp_configurable_serverapp(
177146
jp_root_dir,
178147
jp_logging_stream,
179148
jp_asyncio_loop,
180-
io_loop,
181149
):
182150
"""Starts a Jupyter Server instance based on
183151
the provided configuration values.
@@ -207,7 +175,6 @@ def _configurable_serverapp(
207175
environ=jp_environ,
208176
http_port=jp_http_port,
209177
tmp_path=tmp_path,
210-
io_loop=io_loop,
211178
root_dir=jp_root_dir,
212179
**kwargs,
213180
):
@@ -345,7 +312,7 @@ async def my_test(jp_fetch, jp_ws_fetch):
345312
...
346313
"""
347314

348-
def client_fetch(*parts, headers=None, params=None, **kwargs): # noqa: ARG
315+
def client_fetch(*parts, headers=None, params=None, **kwargs):
349316
if not headers:
350317
headers = {}
351318
if not params:
@@ -414,6 +381,7 @@ async def _(url, **fetch_kwargs):
414381
code = r.code
415382
except HTTPClientError as err:
416383
code = err.code
384+
print(f"HTTPClientError ({err.code}): {err}") # noqa: T201
417385
else:
418386
if fetch is jp_ws_fetch:
419387
r.close()

pytest_jupyter/pytest_tornasync.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Vendored fork of pytest_tornasync from
22
https://github.com/eukaryote/pytest-tornasync/blob/9f1bdeec3eb5816e0183f975ca65b5f6f29fbfbb/src/pytest_tornasync/plugin.py
33
"""
4+
import asyncio
45
from contextlib import closing
5-
from inspect import iscoroutinefunction
66

77
try:
88
import tornado.ioloop
@@ -14,33 +14,37 @@
1414
import pytest
1515

1616
# mypy: disable-error-code="no-untyped-call"
17+
# Bring in local plugins.
18+
from pytest_jupyter.jupyter_core import * # noqa: F403
1719

1820

19-
@pytest.hookimpl(tryfirst=True)
20-
def pytest_pycollect_makeitem(collector, name, obj):
21-
"""Custom pytest collection hook."""
22-
if collector.funcnamefilter(name) and iscoroutinefunction(obj):
23-
return list(collector._genfunctions(name, obj))
24-
return None
21+
@pytest.fixture()
22+
def io_loop(jp_asyncio_loop):
23+
"""Get the current tornado event loop."""
24+
return tornado.ioloop.IOLoop.current()
25+
2526

27+
@pytest.fixture()
28+
def http_server(jp_asyncio_loop, http_server_port, jp_web_app):
29+
"""Start a tornado HTTP server that listens on all available interfaces."""
2630

27-
@pytest.hookimpl(tryfirst=True)
28-
def pytest_pyfunc_call(pyfuncitem):
29-
"""Custom pytest function call hook."""
30-
funcargs = pyfuncitem.funcargs
31-
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
31+
async def get_server():
32+
"""Get a server asynchronously."""
33+
server = tornado.httpserver.HTTPServer(jp_web_app)
34+
server.add_socket(http_server_port[0])
35+
return server
3236

33-
if not iscoroutinefunction(pyfuncitem.obj):
34-
pyfuncitem.obj(**testargs)
35-
return True
37+
server = jp_asyncio_loop.run_until_complete(get_server())
38+
yield server
39+
server.stop()
3640

37-
try:
38-
loop = funcargs["io_loop"]
39-
except KeyError:
40-
loop = tornado.ioloop.IOLoop.current()
41+
if hasattr(server, "close_all_connections"):
42+
try:
43+
jp_asyncio_loop.run_until_complete(server.close_all_connections())
44+
except asyncio.TimeoutError:
45+
pass
4146

42-
loop.run_sync(lambda: pyfuncitem.obj(**testargs))
43-
return True
47+
http_server_port[0].close()
4448

4549

4650
@pytest.fixture()
@@ -52,7 +56,7 @@ def http_server_port():
5256

5357

5458
@pytest.fixture()
55-
def http_server_client(http_server, io_loop):
59+
def http_server_client(http_server, jp_asyncio_loop):
5660
"""
5761
Create an asynchronous HTTP client that can fetch from `http_server`.
5862
"""
@@ -61,7 +65,7 @@ async def get_client():
6165
"""Get a client."""
6266
return AsyncHTTPServerClient(http_server=http_server)
6367

64-
client = io_loop.run_sync(get_client)
68+
client = jp_asyncio_loop.run_until_complete(get_client())
6569
with closing(client) as context:
6670
yield context
6771

tests/test_jupyter_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ def test_template_dir(jp_template_dir):
7171

7272
def test_extension_environ(jp_extension_environ):
7373
pass
74+
75+
76+
def test_ioloop_fixture(io_loop):
77+
pass

0 commit comments

Comments
 (0)