1
+ import os
2
+ import sys
3
+ import json
4
+ import shutil
5
+ import pytest
6
+ import asyncio
7
+ from binascii import hexlify
8
+
9
+ import urllib .parse
10
+ import tornado
11
+ from tornado .escape import url_escape
12
+
13
+ from traitlets .config import Config
14
+
15
+ import jupyter_core .paths
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 asyncio_patch ():
33
+ ServerApp ()._init_asyncio_patch ()
34
+
35
+ @pytest .fixture
36
+ def io_loop (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
+
44
+ def mkdir (tmp_path , * parts ):
45
+ path = tmp_path .joinpath (* parts )
46
+ if not path .exists ():
47
+ path .mkdir (parents = True )
48
+ return path
49
+
50
+
51
+ server_config = pytest .fixture (lambda : {})
52
+ home_dir = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "home" ))
53
+ data_dir = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "data" ))
54
+ config_dir = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "config" ))
55
+ runtime_dir = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "runtime" ))
56
+ root_dir = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "root_dir" ))
57
+ template_dir = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "templates" ))
58
+ system_jupyter_path = pytest .fixture (
59
+ lambda tmp_path : mkdir (tmp_path , "share" , "jupyter" )
60
+ )
61
+ env_jupyter_path = pytest .fixture (
62
+ lambda tmp_path : mkdir (tmp_path , "env" , "share" , "jupyter" )
63
+ )
64
+ system_config_path = pytest .fixture (lambda tmp_path : mkdir (tmp_path , "etc" , "jupyter" ))
65
+ env_config_path = pytest .fixture (
66
+ lambda tmp_path : mkdir (tmp_path , "env" , "etc" , "jupyter" )
67
+ )
68
+ some_resource = u"The very model of a modern major general"
69
+ sample_kernel_json = {
70
+ 'argv' :['cat' , '{connection_file}' ],
71
+ 'display_name' : 'Test kernel' ,
72
+ }
73
+ argv = pytest .fixture (lambda : [])
74
+
75
+ @pytest .fixture
76
+ def environ (
77
+ monkeypatch ,
78
+ tmp_path ,
79
+ home_dir ,
80
+ data_dir ,
81
+ config_dir ,
82
+ runtime_dir ,
83
+ root_dir ,
84
+ system_jupyter_path ,
85
+ system_config_path ,
86
+ env_jupyter_path ,
87
+ env_config_path ,
88
+ ):
89
+ monkeypatch .setenv ("HOME" , str (home_dir ))
90
+ monkeypatch .setenv ("PYTHONPATH" , os .pathsep .join (sys .path ))
91
+
92
+ # Get path to nbconvert template directory *before*
93
+ # monkeypatching the paths env variable.
94
+ possible_paths = jupyter_core .paths .jupyter_path ('nbconvert' , 'templates' )
95
+ nbconvert_path = None
96
+ for path in possible_paths :
97
+ if os .path .exists (path ):
98
+ nbconvert_path = path
99
+ break
100
+
101
+ nbconvert_target = data_dir / 'nbconvert' / 'templates'
102
+
103
+ # monkeypatch.setenv("JUPYTER_NO_CONFIG", "1")
104
+ monkeypatch .setenv ("JUPYTER_CONFIG_DIR" , str (config_dir ))
105
+ monkeypatch .setenv ("JUPYTER_DATA_DIR" , str (data_dir ))
106
+ monkeypatch .setenv ("JUPYTER_RUNTIME_DIR" , str (runtime_dir ))
107
+ monkeypatch .setattr (
108
+ jupyter_core .paths , "SYSTEM_JUPYTER_PATH" , [str (system_jupyter_path )]
109
+ )
110
+ monkeypatch .setattr (jupyter_core .paths , "ENV_JUPYTER_PATH" , [str (env_jupyter_path )])
111
+ monkeypatch .setattr (
112
+ jupyter_core .paths , "SYSTEM_CONFIG_PATH" , [str (system_config_path )]
113
+ )
114
+ monkeypatch .setattr (jupyter_core .paths , "ENV_CONFIG_PATH" , [str (env_config_path )])
115
+
116
+ # copy nbconvert templates to new tmp data_dir.
117
+ if nbconvert_path :
118
+ shutil .copytree (nbconvert_path , str (nbconvert_target ))
119
+
120
+
121
+ @pytest .fixture
122
+ def extension_environ (env_config_path , monkeypatch ):
123
+ """Monkeypatch a Jupyter Extension's config path into each test's environment variable"""
124
+ monkeypatch .setattr (serverextension , "ENV_CONFIG_PATH" , [str (env_config_path )])
125
+
126
+
127
+ @pytest .fixture (scope = 'function' )
128
+ def configurable_serverapp (
129
+ environ ,
130
+ server_config ,
131
+ argv ,
132
+ http_port ,
133
+ tmp_path ,
134
+ root_dir ,
135
+ io_loop ,
136
+ ):
137
+ ServerApp .clear_instance ()
138
+
139
+ def _configurable_serverapp (
140
+ config = server_config ,
141
+ argv = argv ,
142
+ environ = environ ,
143
+ http_port = http_port ,
144
+ tmp_path = tmp_path ,
145
+ root_dir = root_dir ,
146
+ ** kwargs
147
+ ):
148
+ c = Config (config )
149
+ c .NotebookNotary .db_file = ":memory:"
150
+ token = hexlify (os .urandom (4 )).decode ("ascii" )
151
+ url_prefix = "/"
152
+ app = ServerApp .instance (
153
+ # Set the log level to debug for testing purposes
154
+ log_level = 'DEBUG' ,
155
+ port = http_port ,
156
+ port_retries = 0 ,
157
+ open_browser = False ,
158
+ root_dir = str (root_dir ),
159
+ base_url = url_prefix ,
160
+ config = c ,
161
+ allow_root = True ,
162
+ token = token ,
163
+ ** kwargs
164
+ )
165
+
166
+ app .init_signal = lambda : None
167
+ app .log .propagate = True
168
+ app .log .handlers = []
169
+ # Initialize app without httpserver
170
+ app .initialize (argv = argv , new_httpserver = False )
171
+ app .log .propagate = True
172
+ app .log .handlers = []
173
+ # Start app without ioloop
174
+ app .start_app ()
175
+ return app
176
+
177
+ return _configurable_serverapp
178
+
179
+
180
+ @pytest .fixture (scope = "function" )
181
+ def serverapp (server_config , argv , configurable_serverapp ):
182
+ app = configurable_serverapp (config = server_config , argv = argv )
183
+ yield app
184
+ app .remove_server_info_file ()
185
+ app .remove_browser_open_file ()
186
+ app .cleanup_kernels ()
187
+
188
+
189
+ @pytest .fixture
190
+ def app (serverapp ):
191
+ """app fixture is needed by pytest_tornasync plugin"""
192
+ return serverapp .web_app
193
+
194
+
195
+ @pytest .fixture
196
+ def auth_header (serverapp ):
197
+ return {"Authorization" : "token {token}" .format (token = serverapp .token )}
198
+
199
+
200
+ @pytest .fixture
201
+ def http_port (http_server_port ):
202
+ return http_server_port [- 1 ]
203
+
204
+
205
+ @pytest .fixture
206
+ def base_url ():
207
+ return "/"
208
+
209
+
210
+ @pytest .fixture
211
+ def fetch (http_server_client , auth_header , base_url ):
212
+ """fetch fixture that handles auth, base_url, and path"""
213
+ def client_fetch (* parts , headers = {}, params = {}, ** kwargs ):
214
+ # Handle URL strings
215
+ path_url = url_escape (url_path_join (base_url , * parts ), plus = False )
216
+ params_url = urllib .parse .urlencode (params )
217
+ url = path_url + "?" + params_url
218
+ # Add auth keys to header
219
+ headers .update (auth_header )
220
+ # Make request.
221
+ return http_server_client .fetch (
222
+ url , headers = headers , request_timeout = 20 , ** kwargs
223
+ )
224
+ return client_fetch
225
+
226
+
227
+ @pytest .fixture
228
+ def ws_fetch (auth_header , http_port ):
229
+ """websocket fetch fixture that handles auth, base_url, and path"""
230
+ def client_fetch (* parts , headers = {}, params = {}, ** kwargs ):
231
+ # Handle URL strings
232
+ path = url_escape (url_path_join (* parts ), plus = False )
233
+ urlparts = urllib .parse .urlparse ('ws://localhost:{}' .format (http_port ))
234
+ urlparts = urlparts ._replace (
235
+ path = path ,
236
+ query = urllib .parse .urlencode (params )
237
+ )
238
+ url = urlparts .geturl ()
239
+ # Add auth keys to header
240
+ headers .update (auth_header )
241
+ # Make request.
242
+ req = tornado .httpclient .HTTPRequest (
243
+ url ,
244
+ headers = auth_header ,
245
+ connect_timeout = 120
246
+ )
247
+ return tornado .websocket .websocket_connect (req )
248
+ return client_fetch
249
+
250
+
251
+ @pytest .fixture
252
+ def kernelspecs (data_dir ):
253
+ spec_names = ['sample' , 'sample 2' ]
254
+ for name in spec_names :
255
+ sample_kernel_dir = data_dir .joinpath ('kernels' , name )
256
+ sample_kernel_dir .mkdir (parents = True )
257
+ # Create kernel json file
258
+ sample_kernel_file = sample_kernel_dir .joinpath ('kernel.json' )
259
+ sample_kernel_file .write_text (json .dumps (sample_kernel_json ))
260
+ # Create resources text
261
+ sample_kernel_resources = sample_kernel_dir .joinpath ('resource.txt' )
262
+ sample_kernel_resources .write_text (some_resource )
263
+
264
+
265
+ @pytest .fixture (params = [True , False ])
266
+ def contents_manager (request , tmp_path ):
267
+ return FileContentsManager (root_dir = str (tmp_path ), use_atomic_writing = request .param )
268
+
269
+
270
+ @pytest .fixture
271
+ def create_notebook (root_dir ):
272
+ """Create a notebook in the test's home directory."""
273
+ def inner (nbpath ):
274
+ nbpath = root_dir .joinpath (nbpath )
275
+ # Check that the notebook has the correct file extension.
276
+ if nbpath .suffix != '.ipynb' :
277
+ raise Exception ("File extension for notebook must be .ipynb" )
278
+ # If the notebook path has a parent directory, make sure it's created.
279
+ parent = nbpath .parent
280
+ parent .mkdir (parents = True , exist_ok = True )
281
+ # Create a notebook string and write to file.
282
+ nb = nbformat .v4 .new_notebook ()
283
+ nbtext = nbformat .writes (nb , version = 4 )
284
+ nbpath .write_text (nbtext )
285
+ return inner
0 commit comments