|
| 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 |
0 commit comments