Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update event loop handling #75

Merged
merged 13 commits into from
Feb 21, 2024
24 changes: 11 additions & 13 deletions .github/workflows/downstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,27 @@ concurrency:
cancel-in-progress: true

jobs:
jupyter_client:
jupyter_server:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

- name: Run Test
uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
with:
package_name: jupyter_client
package_name: jupyter_server
package_download_extra_args: --pre

jupyter_server:
jupyter_client:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
with:
package_name: jupyter_server
package_name: jupyter_client
package_download_extra_args: --pre

downstream_check: # This job does nothing and is only used for the branch protection
if: always()
Expand Down
24 changes: 0 additions & 24 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,28 +141,6 @@ jobs:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1

jupyter_server_downstream:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
with:
package_name: jupyter_server
package_download_extra_args: --pre

jupyter_client_downstream:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
with:
package_name: jupyter_client
package_download_extra_args: --pre

tests_check: # This job does nothing and is only used for the branch protection
if: always()
needs:
Expand All @@ -173,8 +151,6 @@ jobs:
- test_prereleases
- check_links
- check_release
- jupyter_server_downstream
- jupyter_client_downstream
- test_sdist
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
]
dependencies = [
"pytest",
"jupyter_core"
"jupyter_core>=5.7"
]
requires-python = ">=3.8"

Expand Down
1 change: 0 additions & 1 deletion pytest_jupyter/jupyter_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

# Bring in local plugins.
from pytest_jupyter.jupyter_core import * # noqa: F403
from pytest_jupyter.pytest_tornasync import * # noqa: F403


@pytest.fixture()
Expand Down
43 changes: 22 additions & 21 deletions pytest_jupyter/jupyter_core.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""Fixtures for use with jupyter core and downstream."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import asyncio
import json
import os
import sys
import typing
from inspect import iscoroutinefunction
from pathlib import Path

import jupyter_core
import pytest
from jupyter_core.utils import ensure_event_loop

from .utils import mkdir

Expand All @@ -35,34 +35,35 @@
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))


@pytest.fixture()
@pytest.fixture(autouse=True)
def jp_asyncio_loop():
"""Get an asyncio loop."""
if os.name == "nt":
asyncio.set_event_loop_policy(
asyncio.WindowsSelectorEventLoopPolicy() # type:ignore[attr-defined]
)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop = ensure_event_loop(prefer_selector_loop=True)
yield loop
loop.close()


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


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

return IOLoop.current()
except ImportError:
pass
if not iscoroutinefunction(pyfuncitem.obj):
pyfuncitem.obj(**testargs)
return True

return jp_asyncio_loop.run_until_complete(get_tornado_loop())
loop = ensure_event_loop(prefer_selector_loop=True)
loop.run_until_complete(pyfuncitem.obj(**testargs))
return True


@pytest.fixture()
Expand Down
36 changes: 2 additions & 34 deletions pytest_jupyter/jupyter_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations

import asyncio
import importlib
import io
import logging
Expand Down Expand Up @@ -53,36 +52,6 @@
from pytest_jupyter.pytest_tornasync import * # noqa: F403
from pytest_jupyter.utils import mkdir

# Override some of the fixtures from pytest_tornasync
# The io_loop fixture is overridden in jupyter_core.py so it
# can be shared by other plugins that need it (e.g. jupyter_client.py).


@pytest.fixture()
def http_server(io_loop, http_server_port, jp_web_app):
"""Start a tornado HTTP server that listens on all available interfaces."""

async def get_server():
"""Get a server asynchronously."""
server = tornado.httpserver.HTTPServer(jp_web_app)
server.add_socket(http_server_port[0])
return server

server = io_loop.run_sync(get_server)
yield server
server.stop()

if hasattr(server, "close_all_connections"):
try:
io_loop.run_sync(server.close_all_connections)
except asyncio.TimeoutError:
pass

http_server_port[0].close()


# End pytest_tornasync overrides


@pytest.fixture()
def jp_server_config():
Expand Down Expand Up @@ -177,7 +146,6 @@ def jp_configurable_serverapp(
jp_root_dir,
jp_logging_stream,
jp_asyncio_loop,
io_loop,
):
"""Starts a Jupyter Server instance based on
the provided configuration values.
Expand Down Expand Up @@ -207,7 +175,6 @@ def _configurable_serverapp(
environ=jp_environ,
http_port=jp_http_port,
tmp_path=tmp_path,
io_loop=io_loop,
root_dir=jp_root_dir,
**kwargs,
):
Expand Down Expand Up @@ -345,7 +312,7 @@ async def my_test(jp_fetch, jp_ws_fetch):
...
"""

def client_fetch(*parts, headers=None, params=None, **kwargs): # noqa: ARG
def client_fetch(*parts, headers=None, params=None, **kwargs):
if not headers:
headers = {}
if not params:
Expand Down Expand Up @@ -414,6 +381,7 @@ async def _(url, **fetch_kwargs):
code = r.code
except HTTPClientError as err:
code = err.code
print(f"HTTPClientError ({err.code}): {err}") # noqa: T201
else:
if fetch is jp_ws_fetch:
r.close()
Expand Down
50 changes: 27 additions & 23 deletions pytest_jupyter/pytest_tornasync.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Vendored fork of pytest_tornasync from
https://github.com/eukaryote/pytest-tornasync/blob/9f1bdeec3eb5816e0183f975ca65b5f6f29fbfbb/src/pytest_tornasync/plugin.py
"""
import asyncio
from contextlib import closing
from inspect import iscoroutinefunction

try:
import tornado.ioloop
Expand All @@ -14,33 +14,37 @@
import pytest

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


@pytest.hookimpl(tryfirst=True)
def pytest_pycollect_makeitem(collector, name, obj):
"""Custom pytest collection hook."""
if collector.funcnamefilter(name) and iscoroutinefunction(obj):
return list(collector._genfunctions(name, obj))
return None
@pytest.fixture()
def io_loop(jp_asyncio_loop):
"""Get the current tornado event loop."""
return tornado.ioloop.IOLoop.current()


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

@pytest.hookimpl(tryfirst=True)
def pytest_pyfunc_call(pyfuncitem):
"""Custom pytest function call hook."""
funcargs = pyfuncitem.funcargs
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
async def get_server():
"""Get a server asynchronously."""
server = tornado.httpserver.HTTPServer(jp_web_app)
server.add_socket(http_server_port[0])
return server

if not iscoroutinefunction(pyfuncitem.obj):
pyfuncitem.obj(**testargs)
return True
server = jp_asyncio_loop.run_until_complete(get_server())
yield server
server.stop()

try:
loop = funcargs["io_loop"]
except KeyError:
loop = tornado.ioloop.IOLoop.current()
if hasattr(server, "close_all_connections"):
try:
jp_asyncio_loop.run_until_complete(server.close_all_connections())
except asyncio.TimeoutError:
pass

loop.run_sync(lambda: pyfuncitem.obj(**testargs))
return True
http_server_port[0].close()


@pytest.fixture()
Expand All @@ -52,7 +56,7 @@ def http_server_port():


@pytest.fixture()
def http_server_client(http_server, io_loop):
def http_server_client(http_server, jp_asyncio_loop):
"""
Create an asynchronous HTTP client that can fetch from `http_server`.
"""
Expand All @@ -61,7 +65,7 @@ async def get_client():
"""Get a client."""
return AsyncHTTPServerClient(http_server=http_server)

client = io_loop.run_sync(get_client)
client = jp_asyncio_loop.run_until_complete(get_client())
with closing(client) as context:
yield context

Expand Down
4 changes: 4 additions & 0 deletions tests/test_jupyter_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ def test_template_dir(jp_template_dir):

def test_extension_environ(jp_extension_environ):
pass


def test_ioloop_fixture(io_loop):
pass
Loading