1
1
# Copyright (c) Jupyter Development Team.
2
2
# Distributed under the terms of the Modified BSD License.
3
+ import asyncio
3
4
import importlib
4
5
import io
5
6
import json
9
10
import sys
10
11
import urllib .parse
11
12
from binascii import hexlify
13
+ from contextlib import closing
12
14
13
15
import jupyter_core .paths
14
16
import nbformat
15
17
import pytest
16
18
import tornado
19
+ import tornado .testing
20
+ from pytest_tornasync .plugin import AsyncHTTPServerClient
17
21
from tornado .escape import url_escape
18
22
from tornado .httpclient import HTTPClientError
19
23
from tornado .websocket import WebSocketHandler
35
39
]
36
40
37
41
38
- import asyncio
39
-
40
- if os .name == "nt" and sys .version_info >= (3 , 7 ):
42
+ if os .name == "nt" :
41
43
asyncio .set_event_loop_policy (
42
44
asyncio .WindowsSelectorEventLoopPolicy () # type:ignore[attr-defined]
43
45
)
44
46
45
47
46
48
# ============ Move to Jupyter Core =============
47
49
50
+ # Once the chunk below moves to Jupyter Core
51
+ # use the fixtures directly from Jupyter Core.
52
+
48
53
49
54
def mkdir (tmp_path , * parts ):
50
55
path = tmp_path .joinpath (* parts )
@@ -130,6 +135,55 @@ def jp_environ(
130
135
# ================= End: Move to Jupyter core ================
131
136
132
137
138
+ @pytest .fixture
139
+ def asyncio_loop ():
140
+ loop = asyncio .new_event_loop ()
141
+ asyncio .set_event_loop (loop )
142
+ yield loop
143
+ loop .close ()
144
+
145
+
146
+ @pytest .fixture (autouse = True )
147
+ def io_loop (asyncio_loop ):
148
+ async def get_tornado_loop ():
149
+ return tornado .ioloop .IOLoop .current ()
150
+
151
+ return asyncio_loop .run_until_complete (get_tornado_loop ())
152
+
153
+
154
+ @pytest .fixture
155
+ def http_server_client (http_server , io_loop ):
156
+ """
157
+ Create an asynchronous HTTP client that can fetch from `http_server`.
158
+ """
159
+
160
+ async def get_client ():
161
+ return AsyncHTTPServerClient (http_server = http_server )
162
+
163
+ client = io_loop .run_sync (get_client )
164
+ with closing (client ) as context :
165
+ yield context
166
+
167
+
168
+ @pytest .fixture
169
+ def http_server (io_loop , http_server_port , jp_web_app ):
170
+ """Start a tornado HTTP server that listens on all available interfaces."""
171
+
172
+ async def get_server ():
173
+ server = tornado .httpserver .HTTPServer (jp_web_app )
174
+ server .add_socket (http_server_port [0 ])
175
+ return server
176
+
177
+ server = io_loop .run_sync (get_server )
178
+ yield server
179
+ server .stop ()
180
+
181
+ if hasattr (server , "close_all_connections" ):
182
+ io_loop .run_sync (server .close_all_connections )
183
+
184
+ http_server_port [0 ].close ()
185
+
186
+
133
187
@pytest .fixture
134
188
def jp_server_config ():
135
189
"""Allows tests to setup their specific configuration values."""
@@ -167,7 +221,8 @@ def jp_extension_environ(jp_env_config_path, monkeypatch):
167
221
@pytest .fixture
168
222
def jp_http_port (http_server_port ):
169
223
"""Returns the port value from the http_server_port fixture."""
170
- return http_server_port [- 1 ]
224
+ yield http_server_port [- 1 ]
225
+ http_server_port [0 ].close ()
171
226
172
227
173
228
@pytest .fixture
@@ -216,8 +271,8 @@ def jp_configurable_serverapp(
216
271
jp_base_url ,
217
272
tmp_path ,
218
273
jp_root_dir ,
219
- io_loop ,
220
274
jp_logging_stream ,
275
+ asyncio_loop ,
221
276
):
222
277
"""Starts a Jupyter Server instance based on
223
278
the provided configuration values.
@@ -254,8 +309,9 @@ def _configurable_serverapp(
254
309
):
255
310
c = Config (config )
256
311
c .NotebookNotary .db_file = ":memory:"
257
- token = hexlify (os .urandom (4 )).decode ("ascii" )
258
- c .IdentityProvider .token = token
312
+ if "token" not in c .ServerApp and not c .IdentityProvider .token :
313
+ token = hexlify (os .urandom (4 )).decode ("ascii" )
314
+ c .IdentityProvider .token = token
259
315
260
316
# Allow tests to configure root_dir via a file, argv, or its
261
317
# default (cwd) by specifying a value of None.
@@ -278,48 +334,29 @@ def _configurable_serverapp(
278
334
app .log .propagate = True
279
335
app .log .handlers = []
280
336
# Initialize app without httpserver
281
- app .initialize (argv = argv , new_httpserver = False )
337
+ if asyncio_loop .is_running ():
338
+ app .initialize (argv = argv , new_httpserver = False )
339
+ else :
340
+
341
+ async def initialize_app ():
342
+ app .initialize (argv = argv , new_httpserver = False )
343
+
344
+ asyncio_loop .run_until_complete (initialize_app ())
282
345
# Reroute all logging StreamHandlers away from stdin/stdout since pytest hijacks
283
346
# these streams and closes them at unfortunate times.
284
347
stream_handlers = [h for h in app .log .handlers if isinstance (h , logging .StreamHandler )]
285
348
for handler in stream_handlers :
286
349
handler .setStream (jp_logging_stream )
287
350
app .log .propagate = True
288
351
app .log .handlers = []
289
- # Start app without ioloop
290
352
app .start_app ()
291
353
return app
292
354
293
355
return _configurable_serverapp
294
356
295
357
296
- @pytest .fixture
297
- def jp_ensure_app_fixture (request ):
298
- """Ensures that the 'app' fixture used by pytest-tornasync
299
- is set to `jp_web_app`, the Tornado Web Application returned
300
- by the ServerApp in Jupyter Server, provided by the jp_web_app
301
- fixture in this module.
302
-
303
- Note, this hardcodes the `app_fixture` option from
304
- pytest-tornasync to `jp_web_app`. If this value is configured
305
- to something other than the default, it will raise an exception.
306
- """
307
- app_option = request .config .getoption ("app_fixture" )
308
- if app_option not in ["app" , "jp_web_app" ]:
309
- raise Exception (
310
- "jp_serverapp requires the `app-fixture` option "
311
- "to be set to 'jp_web_app`. Try rerunning the "
312
- "current tests with the option `--app-fixture "
313
- "jp_web_app`."
314
- )
315
- elif app_option == "app" :
316
- # Manually set the app_fixture to `jp_web_app` if it's
317
- # not set already.
318
- request .config .option .app_fixture = "jp_web_app"
319
-
320
-
321
358
@pytest .fixture (scope = "function" )
322
- def jp_serverapp (jp_ensure_app_fixture , jp_server_config , jp_argv , jp_configurable_serverapp ):
359
+ def jp_serverapp (jp_server_config , jp_argv , jp_configurable_serverapp ):
323
360
"""Starts a Jupyter Server instance based on the established configuration values."""
324
361
return jp_configurable_serverapp (config = jp_server_config , argv = jp_argv )
325
362
@@ -482,24 +519,13 @@ def inner(nbpath):
482
519
483
520
484
521
@pytest .fixture (autouse = True )
485
- def jp_server_cleanup (io_loop ):
522
+ def jp_server_cleanup (asyncio_loop ):
486
523
yield
487
524
app : ServerApp = ServerApp .instance ()
488
- loop = io_loop .asyncio_loop
489
- loop .run_until_complete (app ._cleanup ())
525
+ asyncio_loop .run_until_complete (app ._cleanup ())
490
526
ServerApp .clear_instance ()
491
527
492
528
493
- @pytest .fixture
494
- def jp_cleanup_subprocesses (jp_serverapp ):
495
- """DEPRECATED: The jp_server_cleanup fixture automatically cleans up the singleton ServerApp class"""
496
-
497
- async def _ ():
498
- pass
499
-
500
- return _
501
-
502
-
503
529
@pytest .fixture
504
530
def send_request (jp_fetch , jp_ws_fetch ):
505
531
"""Send to Jupyter Server and return response code."""
0 commit comments