Skip to content

Commit 45742d9

Browse files
Zsailerecharles
authored andcommitted
Expose a jupyter_server pytest plugin (#162)
* make a jupyter server pytest plugin * rollback extension conftest * add a create_notebook fixture * move ioloop into serverapp * use pip to install dependencies * Update jupyter_server/pytest_plugin.py
1 parent 08473f0 commit 45742d9

File tree

17 files changed

+319
-271
lines changed

17 files changed

+319
-271
lines changed

appveyor.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ environment:
2121
platform:
2222
- x64
2323

24-
build: off
24+
build: false
2525

2626
install:
2727
- cmd: call %CONDA_INSTALL_LOCN%\Scripts\activate.bat
@@ -31,11 +31,11 @@ install:
3131
- cmd: conda config --add channels conda-forge
3232
- cmd: conda update --yes --quiet conda
3333
- cmd: conda info -a
34-
- cmd: conda create -y -q -n test-env-%CONDA_PY% python=%CONDA_PY_SPEC% pyzmq tornado jupyter_client nbformat nbconvert ipykernel pip nose
34+
- cmd: conda create -y -q -n test-env-%CONDA_PY% python=%CONDA_PY_SPEC% pip pyzmq tornado jupyter_client nbformat nbconvert nose
3535
- cmd: conda activate test-env-%CONDA_PY%
36-
- cmd: pip install .[test]
36+
- cmd: pip install -e .[test]
3737
# FIXME: Use patch for python 3.8, windows issues (https://github.com/ipython/ipykernel/pull/456) - remove once released
3838
- IF %CONDA_PY% == 38 pip install --upgrade git+https://github.com/ipython/ipykernel.git
3939

4040
test_script:
41-
- pytest
41+
- pytest -s -v

jupyter_server/pytest_plugin.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import os
2+
import sys
3+
import json
4+
import pytest
5+
import asyncio
6+
from binascii import hexlify
7+
8+
import urllib.parse
9+
import tornado
10+
from tornado.escape import url_escape
11+
12+
from traitlets.config import Config
13+
14+
import jupyter_core.paths
15+
from jupyter_server.extension import serverextension
16+
from jupyter_server.serverapp import ServerApp
17+
from jupyter_server.utils import url_path_join
18+
19+
import nbformat
20+
21+
# This shouldn't be needed anymore, since pytest_tornasync is found in entrypoints
22+
pytest_plugins = "pytest_tornasync"
23+
24+
# NOTE: This is a temporary fix for Windows 3.8
25+
# We have to override the io_loop fixture with an
26+
# asyncio patch. This will probably be removed in
27+
# the future.
28+
29+
@pytest.fixture
30+
def asyncio_patch():
31+
ServerApp()._init_asyncio_patch()
32+
33+
@pytest.fixture
34+
def io_loop(asyncio_patch):
35+
loop = tornado.ioloop.IOLoop()
36+
loop.make_current()
37+
yield loop
38+
loop.clear_current()
39+
loop.close(all_fds=True)
40+
41+
42+
def mkdir(tmp_path, *parts):
43+
path = tmp_path.joinpath(*parts)
44+
if not path.exists():
45+
path.mkdir(parents=True)
46+
return path
47+
48+
49+
config = pytest.fixture(lambda: {})
50+
home_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "home"))
51+
data_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "data"))
52+
config_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "config"))
53+
runtime_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "runtime"))
54+
root_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "root_dir"))
55+
system_jupyter_path = pytest.fixture(
56+
lambda tmp_path: mkdir(tmp_path, "share", "jupyter")
57+
)
58+
env_jupyter_path = pytest.fixture(
59+
lambda tmp_path: mkdir(tmp_path, "env", "share", "jupyter")
60+
)
61+
system_config_path = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "etc", "jupyter"))
62+
env_config_path = pytest.fixture(
63+
lambda tmp_path: mkdir(tmp_path, "env", "etc", "jupyter")
64+
)
65+
argv = pytest.fixture(lambda: [])
66+
67+
68+
@pytest.fixture
69+
def environ(
70+
monkeypatch,
71+
tmp_path,
72+
home_dir,
73+
data_dir,
74+
config_dir,
75+
runtime_dir,
76+
root_dir,
77+
system_jupyter_path,
78+
system_config_path,
79+
env_jupyter_path,
80+
env_config_path,
81+
):
82+
monkeypatch.setenv("HOME", str(home_dir))
83+
monkeypatch.setenv("PYTHONPATH", os.pathsep.join(sys.path))
84+
monkeypatch.setenv("JUPYTER_NO_CONFIG", "1")
85+
monkeypatch.setenv("JUPYTER_CONFIG_DIR", str(config_dir))
86+
monkeypatch.setenv("JUPYTER_DATA_DIR", str(data_dir))
87+
monkeypatch.setenv("JUPYTER_RUNTIME_DIR", str(runtime_dir))
88+
monkeypatch.setattr(
89+
jupyter_core.paths, "SYSTEM_JUPYTER_PATH", [str(system_jupyter_path)]
90+
)
91+
monkeypatch.setattr(jupyter_core.paths, "ENV_JUPYTER_PATH", [str(env_jupyter_path)])
92+
monkeypatch.setattr(
93+
jupyter_core.paths, "SYSTEM_CONFIG_PATH", [str(system_config_path)]
94+
)
95+
monkeypatch.setattr(jupyter_core.paths, "ENV_CONFIG_PATH", [str(env_config_path)])
96+
97+
98+
@pytest.fixture
99+
def extension_environ(env_config_path, monkeypatch):
100+
"""Monkeypatch a Jupyter Extension's config path into each test's environment variable"""
101+
monkeypatch.setattr(serverextension, "ENV_CONFIG_PATH", [str(env_config_path)])
102+
monkeypatch.setattr(serverextension, "ENV_CONFIG_PATH", [str(env_config_path)])
103+
104+
105+
@pytest.fixture
106+
def configurable_serverapp(
107+
environ, http_port, tmp_path, home_dir, data_dir, config_dir, runtime_dir, root_dir, io_loop
108+
):
109+
def serverapp(
110+
config={},
111+
argv=[],
112+
environ=environ,
113+
http_port=http_port,
114+
tmp_path=tmp_path,
115+
home_dir=home_dir,
116+
data_dir=data_dir,
117+
config_dir=config_dir,
118+
runtime_dir=runtime_dir,
119+
root_dir=root_dir,
120+
**kwargs
121+
):
122+
c = Config(config)
123+
c.NotebookNotary.db_file = ":memory:"
124+
token = hexlify(os.urandom(4)).decode("ascii")
125+
url_prefix = "/"
126+
app = ServerApp.instance(
127+
port=http_port,
128+
port_retries=0,
129+
open_browser=False,
130+
config_dir=str(config_dir),
131+
data_dir=str(data_dir),
132+
runtime_dir=str(runtime_dir),
133+
root_dir=str(root_dir),
134+
base_url=url_prefix,
135+
config=c,
136+
allow_root=True,
137+
token=token,
138+
**kwargs
139+
)
140+
app.init_signal = lambda: None
141+
app.log.propagate = True
142+
app.log.handlers = []
143+
# Initialize app without httpserver
144+
app.initialize(argv=argv, new_httpserver=False)
145+
app.log.propagate = True
146+
app.log.handlers = []
147+
# Start app without ioloop
148+
app.start_app()
149+
return app
150+
151+
yield serverapp
152+
ServerApp.clear_instance()
153+
154+
155+
@pytest.fixture
156+
def serverapp(configurable_serverapp, config, argv):
157+
app = configurable_serverapp(config=config, argv=argv)
158+
yield app
159+
app.remove_server_info_file()
160+
app.remove_browser_open_file()
161+
app.cleanup_kernels()
162+
163+
164+
@pytest.fixture
165+
def app(serverapp):
166+
return serverapp.web_app
167+
168+
169+
@pytest.fixture
170+
def auth_header(serverapp):
171+
return {"Authorization": "token {token}".format(token=serverapp.token)}
172+
173+
174+
@pytest.fixture
175+
def http_port(http_server_port):
176+
return http_server_port[-1]
177+
178+
179+
@pytest.fixture
180+
def base_url(http_server_port):
181+
return "/"
182+
183+
184+
@pytest.fixture
185+
def fetch(http_server_client, auth_header, base_url):
186+
"""fetch fixture that handles auth, base_url, and path"""
187+
def client_fetch(*parts, headers={}, params={}, **kwargs):
188+
# Handle URL strings
189+
path_url = url_escape(url_path_join(base_url, *parts), plus=False)
190+
params_url = urllib.parse.urlencode(params)
191+
url = path_url + "?" + params_url
192+
# Add auth keys to header
193+
headers.update(auth_header)
194+
# Make request.
195+
return http_server_client.fetch(
196+
url, headers=headers, request_timeout=20, **kwargs
197+
)
198+
return client_fetch
199+
200+
201+
@pytest.fixture
202+
def create_notebook(root_dir):
203+
"""Create a notebook in the test's home directory."""
204+
def inner(nbpath):
205+
nbpath = root_dir.joinpath(nbpath)
206+
# Check that the notebook has the correct file extension.
207+
if nbpath.suffix != '.ipynb':
208+
raise Exception("File extension for notebook must be .ipynb")
209+
# If the notebook path has a parent directory, make sure it's created.
210+
parent = nbpath.parent
211+
parent.mkdir(parents=True, exist_ok=True)
212+
# Create a notebook string and write to file.
213+
nb = nbformat.v4.new_notebook()
214+
nbtext = nbformat.writes(nb, version=4)
215+
nbpath.write_text(nbtext)
216+
return inner

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@
100100
'console_scripts': [
101101
'jupyter-server = jupyter_server.serverapp:main',
102102
'jupyter-bundlerextension = jupyter_server.bundler.bundlerextensions:main',
103+
],
104+
'pytest11': [
105+
'pytest_jupyter_server = jupyter_server.pytest_plugin'
103106
]
104107
},
105108
)

0 commit comments

Comments
 (0)