Skip to content

Commit 9b58f0b

Browse files
authored
Clean up server fixtures and increase coverage (#19)
1 parent 391ef23 commit 9b58f0b

File tree

4 files changed

+99
-73
lines changed

4 files changed

+99
-73
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
dependency_type: minimum
5454
- name: Run the unit tests
5555
run: |
56-
hatch run test:nowarn || hatch run test:nowarn --lf
56+
hatch -v run test:nowarn || hatch run test:nowarn --lf
5757
5858
test_prereleases:
5959
name: Test Prereleases
@@ -85,6 +85,8 @@ jobs:
8585
steps:
8686
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
8787
- uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
88+
with:
89+
test_command: hatch run test:test
8890

8991
pre_commit:
9092
name: pre-commit

pyproject.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ docs = [
3737
"Sphinx",
3838
]
3939
client = [
40-
"jupyter_client"
40+
"jupyter_client>=7.4.0"
4141
]
4242
server = [
43-
"jupyter_server",
44-
"nbformat",
45-
"pytest-tornasync"
43+
"jupyter_server>=1.21",
44+
"ipykernel>=6.14",
45+
"nbformat>=5.3",
46+
"pytest-tornasync>=0.6"
4647
]
4748
test = [
48-
"pytest-jupyter[server]",
4949
"pytest-timeout"
5050
]
5151

@@ -67,13 +67,13 @@ features = ["docs"]
6767
build = "make -C docs html SPHINXOPTS='-W'"
6868

6969
[tool.hatch.envs.test]
70-
features = ["test"]
70+
features = ["test", "server", "client"]
7171
[tool.hatch.envs.test.scripts]
7272
test = "python -m pytest -vv {args}"
7373
nowarn = "test -W default {args}"
7474

7575
[tool.hatch.envs.cov]
76-
features = ["test"]
76+
features = ["test", "server", "client"]
7777
dependencies = ["coverage", "pytest-cov"]
7878
[tool.hatch.envs.cov.scripts]
7979
test = "python -m pytest -vv --cov pytest_jupyter --cov-branch --cov-report term-missing:skip-covered {args}"

pytest_jupyter/jupyter_server.py

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import asyncio
55
import importlib
66
import io
7-
import json
87
import logging
98
import os
109
import re
@@ -22,18 +21,19 @@
2221
import nbformat
2322
import tornado
2423
import tornado.testing
24+
from jupyter_server._version import version_info
2525
from jupyter_server.auth import Authorizer
2626
from jupyter_server.extension import serverextension
2727
from jupyter_server.serverapp import JUPYTER_SERVICE_HANDLERS, ServerApp
28-
from jupyter_server.services.contents.filemanager import AsyncFileContentsManager
29-
from jupyter_server.services.contents.largefilemanager import AsyncLargeFileManager
3028
from jupyter_server.utils import url_path_join
3129
from pytest_tornasync.plugin import AsyncHTTPServerClient
3230
from tornado.escape import url_escape
3331
from tornado.httpclient import HTTPClientError
3432
from tornado.websocket import WebSocketHandler
3533
from traitlets.config import Config
3634

35+
is_v2 = version_info[0] == 2
36+
3737
except ImportError:
3838
import warnings
3939

@@ -51,22 +51,9 @@
5151
pytest_plugins = ["pytest_tornasync", "pytest_jupyter"]
5252

5353

54-
# NOTE: This is a temporary fix for Windows 3.8
55-
# We have to override the io_loop fixture with an
56-
# asyncio patch. This will probably be removed in
57-
# the future.
5854
@pytest.fixture
59-
def jp_asyncio_patch():
60-
"""Appropriately configures the event loop policy if running on Windows w/ Python >= 3.8."""
61-
ServerApp()._init_asyncio_patch()
62-
63-
64-
@pytest.fixture
65-
def asyncio_loop():
66-
loop = asyncio.new_event_loop()
67-
asyncio.set_event_loop(loop)
68-
yield loop
69-
loop.close()
55+
async def asyncio_loop():
56+
return asyncio.get_running_loop()
7057

7158

7259
@pytest.fixture(autouse=True)
@@ -113,11 +100,14 @@ async def get_server():
113100
@pytest.fixture
114101
def jp_server_config():
115102
"""Allows tests to setup their specific configuration values."""
116-
return Config(
117-
{
118-
"jpserver_extensions": {"jupyter_server_terminals": True},
119-
}
120-
)
103+
if is_v2:
104+
return Config(
105+
{
106+
"jpserver_extensions": {"jupyter_server_terminals": True},
107+
}
108+
)
109+
else:
110+
return Config({})
121111

122112

123113
@pytest.fixture
@@ -199,6 +189,7 @@ def jp_configurable_serverapp(
199189
jp_root_dir,
200190
jp_logging_stream,
201191
asyncio_loop,
192+
io_loop,
202193
):
203194
"""Starts a Jupyter Server instance based on
204195
the provided configuration values.
@@ -218,7 +209,7 @@ def my_test(jp_configurable_serverapp):
218209
# explicitly put in config.
219210
serverapp_config = jp_server_config.setdefault("ServerApp", {})
220211
exts = serverapp_config.setdefault("jpserver_extensions", {})
221-
if "jupyter_server_terminals" not in exts:
212+
if "jupyter_server_terminals" not in exts and is_v2:
222213
exts["jupyter_server_terminals"] = True
223214

224215
def _configurable_serverapp(
@@ -228,14 +219,19 @@ def _configurable_serverapp(
228219
environ=jp_environ,
229220
http_port=jp_http_port,
230221
tmp_path=tmp_path,
222+
io_loop=io_loop,
231223
root_dir=jp_root_dir,
232224
**kwargs,
233225
):
234226
c = Config(config)
235227
c.NotebookNotary.db_file = ":memory:"
236-
if "token" not in c.ServerApp and not c.IdentityProvider.token:
237-
token = hexlify(os.urandom(4)).decode("ascii")
238-
c.IdentityProvider.token = token
228+
229+
default_token = hexlify(os.urandom(4)).decode("ascii")
230+
if not is_v2:
231+
kwargs["token"] = default_token
232+
233+
elif "token" not in c.ServerApp and not c.IdentityProvider.token:
234+
c.IdentityProvider.token = default_token
239235

240236
# Allow tests to configure root_dir via a file, argv, or its
241237
# default (cwd) by specifying a value of None.
@@ -294,6 +290,8 @@ def jp_web_app(jp_serverapp):
294290
@pytest.fixture
295291
def jp_auth_header(jp_serverapp):
296292
"""Configures an authorization header using the token from the serverapp fixture."""
293+
if not is_v2:
294+
return {"Authorization": f"token {jp_serverapp.token}"}
297295
return {"Authorization": f"token {jp_serverapp.identity_provider.token}"}
298296

299297

@@ -331,6 +329,7 @@ def client_fetch(*parts, headers=None, params=None, **kwargs):
331329
for key, value in jp_auth_header.items():
332330
headers.setdefault(key, value)
333331
# Make request.
332+
print(id(http_server_client.io_loop.asyncio_loop))
334333
return http_server_client.fetch(url, headers=headers, request_timeout=20, **kwargs)
335334

336335
return client_fetch
@@ -382,43 +381,6 @@ def client_fetch(*parts, headers=None, params=None, **kwargs):
382381
return client_fetch
383382

384383

385-
some_resource = "The very model of a modern major general"
386-
sample_kernel_json = {
387-
"argv": ["cat", "{connection_file}"],
388-
"display_name": "Test kernel",
389-
}
390-
391-
392-
@pytest.fixture
393-
def jp_kernelspecs(jp_data_dir):
394-
"""Configures some sample kernelspecs in the Jupyter data directory."""
395-
spec_names = ["sample", "sample2", "bad"]
396-
for name in spec_names:
397-
sample_kernel_dir = jp_data_dir.joinpath("kernels", name)
398-
sample_kernel_dir.mkdir(parents=True)
399-
# Create kernel json file
400-
sample_kernel_file = sample_kernel_dir.joinpath("kernel.json")
401-
kernel_json = sample_kernel_json.copy()
402-
if name == "bad":
403-
kernel_json["argv"] = ["non_existent_path"]
404-
sample_kernel_file.write_text(json.dumps(kernel_json))
405-
# Create resources text
406-
sample_kernel_resources = sample_kernel_dir.joinpath("resource.txt")
407-
sample_kernel_resources.write_text(some_resource)
408-
409-
410-
@pytest.fixture(params=[True, False])
411-
def jp_contents_manager(request, tmp_path):
412-
"""Returns an AsyncFileContentsManager instance based on the use_atomic_writing parameter value."""
413-
return AsyncFileContentsManager(root_dir=str(tmp_path), use_atomic_writing=request.param)
414-
415-
416-
@pytest.fixture
417-
def jp_large_contents_manager(tmp_path):
418-
"""Returns an AsyncLargeFileManager instance."""
419-
return AsyncLargeFileManager(root_dir=str(tmp_path))
420-
421-
422384
@pytest.fixture
423385
def jp_create_notebook(jp_root_dir):
424386
"""Creates a notebook in the test's home directory."""
@@ -435,6 +397,7 @@ def inner(nbpath):
435397
nb = nbformat.v4.new_notebook()
436398
nbtext = nbformat.writes(nb, version=4)
437399
nbpath.write_text(nbtext)
400+
return nb
438401

439402
return inner
440403

tests/test_jupyter_server.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
1+
import json
2+
import os
3+
from unittest.mock import MagicMock
4+
5+
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
6+
from jupyter_server.auth import Authorizer
17
from jupyter_server.serverapp import ServerApp
8+
from tornado.websocket import WebSocketHandler
29

310

4-
def test_serverapp(jp_serverapp):
11+
async def test_serverapp(jp_serverapp):
512
assert isinstance(jp_serverapp, ServerApp)
13+
14+
15+
async def test_get_api_spec(jp_fetch):
16+
response = await jp_fetch("api", "spec.yaml", method="GET")
17+
assert response.code == 200
18+
19+
20+
async def test_send_request(send_request):
21+
code = await send_request("api/spec.yaml", method="GET")
22+
assert code == 200
23+
24+
25+
async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
26+
# Create kernel
27+
r = await jp_fetch(
28+
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
29+
)
30+
kid = json.loads(r.body.decode())["id"]
31+
32+
# Get kernel info
33+
r = await jp_fetch("api", "kernels", kid, method="GET")
34+
model = json.loads(r.body.decode())
35+
assert model["connections"] == 0
36+
37+
# Open a websocket connection.
38+
ws = await jp_ws_fetch("api", "kernels", kid, "channels")
39+
ws.close()
40+
41+
42+
async def test_authorizer(jp_server_authorizer, jp_serverapp, jp_base_url):
43+
auth: Authorizer = jp_server_authorizer(parent=jp_serverapp)
44+
assert isinstance(auth, Authorizer)
45+
assert auth.normalize_url("foo") == "/foo"
46+
assert auth.normalize_url(f"{jp_base_url}/foo") == "/foo"
47+
request = MagicMock()
48+
request.method = "GET"
49+
request.path = "foo"
50+
handler = WebSocketHandler(jp_serverapp.web_app, request=request)
51+
assert not auth.is_authorized(handler, {}, "execute", None)
52+
assert auth.match_url_to_resource("/api/kernels") == "kernels"
53+
assert auth.match_url_to_resource("/api/shutdown") == "server"
54+
55+
56+
async def test_create_notebook(jp_create_notebook):
57+
nb = jp_create_notebook("foo.ipynb")
58+
assert "nbformat" in nb
59+
60+
61+
def test_template_dir(jp_template_dir):
62+
assert os.path.exists(jp_template_dir)
63+
64+
65+
def test_extension_environ(jp_extension_environ):
66+
pass

0 commit comments

Comments
 (0)