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