Skip to content

Commit 1a91510

Browse files
committed
Add initial plugins for core and server
1 parent 60896b3 commit 1a91510

File tree

6 files changed

+336
-0
lines changed

6 files changed

+336
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
__pycache__/
33
*.py[cod]
44
*$py.class
5+
*.vscode
56

67
# C extensions
78
*.so
@@ -114,6 +115,9 @@ venv.bak/
114115
.spyderproject
115116
.spyproject
116117

118+
# Pycharm settings
119+
.idea/
120+
117121
# Rope project settings
118122
.ropeproject
119123

pytest_jupyter/__init__.py

Whitespace-only changes.

pytest_jupyter/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.0.1"

pytest_jupyter/jupyter_core.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
import os
5+
import pytest
6+
import shutil
7+
import sys
8+
9+
import jupyter_core.paths
10+
11+
12+
def mkdir(tmp_path, *parts):
13+
path = tmp_path.joinpath(*parts)
14+
if not path.exists():
15+
path.mkdir(parents=True)
16+
return path
17+
18+
19+
jp_home_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "home"))
20+
jp_data_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "data"))
21+
jp_config_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "config"))
22+
jp_runtime_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "runtime"))
23+
jp_root_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "root_dir"))
24+
jp_template_dir = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "templates"))
25+
jp_system_jupyter_path = pytest.fixture(
26+
lambda tmp_path: mkdir(tmp_path, "share", "jupyter")
27+
)
28+
jp_env_jupyter_path = pytest.fixture(
29+
lambda tmp_path: mkdir(tmp_path, "env", "share", "jupyter")
30+
)
31+
jp_system_config_path = pytest.fixture(lambda tmp_path: mkdir(tmp_path, "etc", "jupyter"))
32+
jp_env_config_path = pytest.fixture(
33+
lambda tmp_path: mkdir(tmp_path, "env", "etc", "jupyter")
34+
)
35+
36+
37+
@pytest.fixture
38+
def jp_environ(
39+
monkeypatch,
40+
tmp_path,
41+
jp_home_dir,
42+
jp_data_dir,
43+
jp_config_dir,
44+
jp_runtime_dir,
45+
jp_root_dir,
46+
jp_system_jupyter_path,
47+
jp_system_config_path,
48+
jp_env_jupyter_path,
49+
jp_env_config_path,
50+
):
51+
monkeypatch.setenv("HOME", str(jp_home_dir))
52+
monkeypatch.setenv("PYTHONPATH", os.pathsep.join(sys.path))
53+
54+
# Get path to nbconvert template directory *before*
55+
# monkeypatching the paths env variable.
56+
possible_paths = jupyter_core.paths.jupyter_path('nbconvert', 'templates')
57+
nbconvert_path = None
58+
for path in possible_paths:
59+
if os.path.exists(path):
60+
nbconvert_path = path
61+
break
62+
63+
nbconvert_target = jp_data_dir / 'nbconvert' / 'templates'
64+
65+
# monkeypatch.setenv("JUPYTER_NO_CONFIG", "1")
66+
monkeypatch.setenv("JUPYTER_CONFIG_DIR", str(jp_config_dir))
67+
monkeypatch.setenv("JUPYTER_DATA_DIR", str(jp_data_dir))
68+
monkeypatch.setenv("JUPYTER_RUNTIME_DIR", str(jp_runtime_dir))
69+
monkeypatch.setattr(
70+
jupyter_core.paths, "SYSTEM_JUPYTER_PATH", [str(jp_system_jupyter_path)]
71+
)
72+
monkeypatch.setattr(jupyter_core.paths, "ENV_JUPYTER_PATH", [str(jp_env_jupyter_path)])
73+
monkeypatch.setattr(
74+
jupyter_core.paths, "SYSTEM_CONFIG_PATH", [str(jp_system_config_path)]
75+
)
76+
monkeypatch.setattr(jupyter_core.paths, "ENV_CONFIG_PATH", [str(jp_env_config_path)])
77+
78+
# copy nbconvert templates to new tmp data_dir.
79+
if nbconvert_path:
80+
shutil.copytree(nbconvert_path, str(nbconvert_target))

pytest_jupyter/jupyter_server.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Copyright (c) Jupyter Development Team.
2+
# Distributed under the terms of the Modified BSD License.
3+
4+
import os
5+
import json
6+
import pytest
7+
8+
from binascii import hexlify
9+
10+
import urllib.parse
11+
import tornado
12+
from tornado.escape import url_escape
13+
14+
from traitlets.config import Config
15+
16+
from jupyter_server.extension import serverextension
17+
from jupyter_server.serverapp import ServerApp
18+
from jupyter_server.utils import url_path_join
19+
from jupyter_server.services.contents.filemanager import FileContentsManager
20+
21+
import nbformat
22+
23+
# This shouldn't be needed anymore, since pytest_tornasync is found in entrypoints
24+
pytest_plugins = "pytest_tornasync"
25+
26+
# NOTE: This is a temporary fix for Windows 3.8
27+
# We have to override the io_loop fixture with an
28+
# asyncio patch. This will probably be removed in
29+
# the future.
30+
31+
@pytest.fixture
32+
def jp_asyncio_patch():
33+
ServerApp()._init_asyncio_patch()
34+
35+
@pytest.fixture
36+
def io_loop(jp_asyncio_patch):
37+
loop = tornado.ioloop.IOLoop()
38+
loop.make_current()
39+
yield loop
40+
loop.clear_current()
41+
loop.close(all_fds=True)
42+
43+
jp_server_config = pytest.fixture(lambda: {})
44+
45+
some_resource = u"The very model of a modern major general"
46+
sample_kernel_json = {
47+
'argv':['cat', '{connection_file}'],
48+
'display_name': 'Test kernel',
49+
}
50+
jp_argv = pytest.fixture(lambda: [])
51+
52+
53+
54+
@pytest.fixture
55+
def jp_extension_environ(jp_env_config_path, monkeypatch):
56+
"""Monkeypatch a Jupyter Extension's config path into each test's environment variable"""
57+
monkeypatch.setattr(serverextension, "ENV_CONFIG_PATH", [str(jp_env_config_path)])
58+
59+
60+
@pytest.fixture
61+
def jp_http_port(http_server_port):
62+
return http_server_port[-1]
63+
64+
65+
@pytest.fixture(scope='function')
66+
def jp_configurable_serverapp(
67+
jp_environ,
68+
jp_server_config,
69+
jp_argv,
70+
jp_http_port,
71+
tmp_path,
72+
jp_root_dir,
73+
io_loop,
74+
):
75+
ServerApp.clear_instance()
76+
77+
def _configurable_serverapp(
78+
config=jp_server_config,
79+
argv=jp_argv,
80+
environ=jp_environ,
81+
http_port=jp_http_port,
82+
tmp_path=tmp_path,
83+
root_dir=jp_root_dir,
84+
**kwargs
85+
):
86+
c = Config(config)
87+
c.NotebookNotary.db_file = ":memory:"
88+
token = hexlify(os.urandom(4)).decode("ascii")
89+
url_prefix = "/"
90+
app = ServerApp.instance(
91+
# Set the log level to debug for testing purposes
92+
log_level='DEBUG',
93+
port=http_port,
94+
port_retries=0,
95+
open_browser=False,
96+
root_dir=str(root_dir),
97+
base_url=url_prefix,
98+
config=c,
99+
allow_root=True,
100+
token=token,
101+
**kwargs
102+
)
103+
104+
app.init_signal = lambda: None
105+
app.log.propagate = True
106+
app.log.handlers = []
107+
# Initialize app without httpserver
108+
app.initialize(argv=argv, new_httpserver=False)
109+
app.log.propagate = True
110+
app.log.handlers = []
111+
# Start app without ioloop
112+
app.start_app()
113+
return app
114+
115+
return _configurable_serverapp
116+
117+
118+
@pytest.fixture(scope="function")
119+
def jp_serverapp(jp_server_config, jp_argv, jp_configurable_serverapp):
120+
app = jp_configurable_serverapp(config=jp_server_config, argv=jp_argv)
121+
yield app
122+
app.remove_server_info_file()
123+
app.remove_browser_open_file()
124+
app.cleanup_kernels()
125+
126+
127+
@pytest.fixture
128+
def app(jp_serverapp):
129+
"""app fixture is needed by pytest_tornasync plugin"""
130+
return jp_serverapp.web_app
131+
132+
133+
@pytest.fixture
134+
def jp_auth_header(jp_serverapp):
135+
return {"Authorization": "token {token}".format(token=jp_serverapp.token)}
136+
137+
138+
@pytest.fixture
139+
def jp_base_url():
140+
return "/"
141+
142+
143+
@pytest.fixture
144+
def jp_fetch(http_server_client, jp_auth_header, jp_base_url):
145+
"""fetch fixture that handles auth, base_url, and path"""
146+
def client_fetch(*parts, headers={}, params={}, **kwargs):
147+
# Handle URL strings
148+
path_url = url_escape(url_path_join(jp_base_url, *parts), plus=False)
149+
params_url = urllib.parse.urlencode(params)
150+
url = path_url + "?" + params_url
151+
# Add auth keys to header
152+
headers.update(jp_auth_header)
153+
# Make request.
154+
return http_server_client.fetch(
155+
url, headers=headers, request_timeout=20, **kwargs
156+
)
157+
return client_fetch
158+
159+
160+
@pytest.fixture
161+
def jp_ws_fetch(jp_auth_header, jp_http_port):
162+
"""websocket fetch fixture that handles auth, base_url, and path"""
163+
def client_fetch(*parts, headers={}, params={}, **kwargs):
164+
# Handle URL strings
165+
path = url_escape(url_path_join(*parts), plus=False)
166+
urlparts = urllib.parse.urlparse('ws://localhost:{}'.format(jp_http_port))
167+
urlparts = urlparts._replace(
168+
path=path,
169+
query=urllib.parse.urlencode(params)
170+
)
171+
url = urlparts.geturl()
172+
# Add auth keys to header
173+
headers.update(jp_auth_header)
174+
# Make request.
175+
req = tornado.httpclient.HTTPRequest(
176+
url,
177+
headers=jp_auth_header,
178+
connect_timeout=120
179+
)
180+
return tornado.websocket.websocket_connect(req)
181+
return client_fetch
182+
183+
184+
@pytest.fixture
185+
def jp_kernelspecs(jp_data_dir):
186+
spec_names = ['sample', 'sample 2']
187+
for name in spec_names:
188+
sample_kernel_dir = jp_data_dir.joinpath('kernels', name)
189+
sample_kernel_dir.mkdir(parents=True)
190+
# Create kernel json file
191+
sample_kernel_file = sample_kernel_dir.joinpath('kernel.json')
192+
sample_kernel_file.write_text(json.dumps(sample_kernel_json))
193+
# Create resources text
194+
sample_kernel_resources = sample_kernel_dir.joinpath('resource.txt')
195+
sample_kernel_resources.write_text(some_resource)
196+
197+
198+
@pytest.fixture(params=[True, False])
199+
def jp_contents_manager(request, tmp_path):
200+
return FileContentsManager(root_dir=str(tmp_path), use_atomic_writing=request.param)
201+
202+
203+
@pytest.fixture
204+
def jp_create_notebook(jp_root_dir):
205+
"""Create a notebook in the test's home directory."""
206+
def inner(nbpath):
207+
nbpath = jp_root_dir.joinpath(nbpath)
208+
# Check that the notebook has the correct file extension.
209+
if nbpath.suffix != '.ipynb':
210+
raise Exception("File extension for notebook must be .ipynb")
211+
# If the notebook path has a parent directory, make sure it's created.
212+
parent = nbpath.parent
213+
parent.mkdir(parents=True, exist_ok=True)
214+
# Create a notebook string and write to file.
215+
nb = nbformat.v4.new_notebook()
216+
nbtext = nbformat.writes(nb, version=4)
217+
nbpath.write_text(nbtext)
218+
return inner

setup.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import pathlib
3+
from setuptools import setup, find_packages
4+
5+
here = pathlib.Path('.')
6+
7+
readme_path = here.joinpath('README.md')
8+
README = readme_path.read_text()
9+
VERSION = "0.0.1"
10+
11+
setup(
12+
name="pytest-jupyter",
13+
packages=find_packages(),
14+
version=VERSION,
15+
description="A pytest plugin for testing Jupyter core libraries and extensions.",
16+
long_description=README,
17+
long_description_content_type='text/markdown',
18+
author='Jupyter Development Team',
19+
author_email='[email protected]',
20+
url='http://jupyter.org',
21+
license='BSD',
22+
platforms="Linux, Mac OS X, Windows",
23+
keywords=['Jupyter', 'pytest'],
24+
# the following makes a plugin available to pytest
25+
entry_points={
26+
"pytest11": [
27+
"pytest-jupyterserver = pytest_jupyter.jupyter_server",
28+
"pytest-jupytercore = pytest_jupyter.jupyter_core"
29+
]
30+
},
31+
# custom PyPI classifier for pytest plugins
32+
classifiers=["Framework :: Pytest"],
33+
)

0 commit comments

Comments
 (0)