1
+ import logging
2
+
1
3
import dash
2
4
import os
3
5
import requests
9
11
import sys
10
12
import inspect
11
13
import traceback
14
+ import threading
12
15
import warnings
13
16
import queue
14
17
18
21
from ansi2html import Ansi2HTMLConverter
19
22
import uuid
20
23
24
+ from werkzeug .serving import make_server
25
+
21
26
from .comms import _dash_comm , _jupyter_config , _request_jupyter_config
22
- from ._stoppable_thread import StoppableThread
23
27
24
28
25
29
def _get_skip (error : Exception ):
@@ -50,7 +54,7 @@ class JupyterDash(dash.Dash):
50
54
_in_colab = "google.colab" in sys .modules
51
55
_token = str (uuid .uuid4 ())
52
56
53
- _server_threads = {}
57
+ _servers = {}
54
58
55
59
@classmethod
56
60
def infer_jupyter_proxy_config (cls ):
@@ -147,6 +151,7 @@ def alive():
147
151
return 'Alive'
148
152
149
153
self .server .logger .disabled = True
154
+ self ._exception_handling_added = False
150
155
151
156
def run (
152
157
self ,
@@ -186,11 +191,8 @@ def run(
186
191
return
187
192
188
193
# Get host and port
189
- host = kwargs .get ("host" , os .getenv ("HOST" , "127.0.0.1" ))
190
- port = kwargs .get ("port" , os .getenv ("PORT" , "8050" ))
191
-
192
- kwargs ['host' ] = host
193
- kwargs ['port' ] = port
194
+ host = kwargs .pop ("host" , os .getenv ("HOST" , "127.0.0.1" ))
195
+ port = int (kwargs .pop ("port" , os .getenv ("PORT" , "8050" )))
194
196
195
197
# Validate / infer display mode
196
198
if JupyterDash ._in_colab :
@@ -222,11 +224,10 @@ def run(
222
224
inline_exceptions = mode == "inline"
223
225
224
226
# Terminate any existing server using this port
225
- old_server = self ._server_threads .get ((host , port ))
227
+ old_server = self ._servers .get ((host , port ))
226
228
if old_server :
227
- old_server .kill ()
228
- old_server .join ()
229
- del self ._server_threads [(host , port )]
229
+ old_server .shutdown ()
230
+ del self ._servers [(host , port )]
230
231
231
232
# Configure pathname prefix
232
233
requests_pathname_prefix = self .config .get ('requests_pathname_prefix' , None )
@@ -256,11 +257,7 @@ def run(
256
257
)
257
258
258
259
# Default the global "debug" flag to True
259
- debug = kwargs .get ('debug' , True )
260
-
261
- # Disable debug flag when calling superclass because it doesn't work
262
- # in notebook
263
- kwargs ['debug' ] = False
260
+ debug = kwargs .pop ('debug' , True )
264
261
265
262
# Enable supported dev tools
266
263
if debug :
@@ -283,15 +280,33 @@ def run(
283
280
# there is no active kernel.
284
281
kwargs ['dev_tools_hot_reload' ] = mode == "external"
285
282
286
- # suppress warning banner printed to standard out
287
- flask .cli .show_server_banner = lambda * args , ** kwargs : None
288
-
289
283
# Set up custom callback exception handling
290
284
self ._config_callback_exception_handling (
291
285
dev_tools_prune_errors = kwargs .get ('dev_tools_prune_errors' , True ),
292
286
inline_exceptions = inline_exceptions ,
293
287
)
294
288
289
+ dev_tools_args = dict (
290
+ debug = debug ,
291
+ dev_tools_ui = kwargs .pop ("dev_tools_ui" , None ),
292
+ dev_tools_props_check = kwargs .pop ("dev_tools_props_check" , None ),
293
+ dev_tools_serve_dev_bundles = kwargs .pop ("dev_tools_serve_dev_bundles" , None ),
294
+ dev_tools_hot_reload = kwargs .pop ("dev_tools_hot_reload" , None ),
295
+ dev_tools_hot_reload_interval = kwargs .pop ("dev_tools_hot_reload_interval" , None ),
296
+ dev_tools_hot_reload_watch_interval = kwargs .pop ("dev_tools_hot_reload_watch_interval" , None ),
297
+ dev_tools_hot_reload_max_retry = kwargs .pop ("dev_tools_hot_reload_max_retry" , None ),
298
+ dev_tools_silence_routes_logging = kwargs .pop ("dev_tools_silence_routes_logging" , None ),
299
+ dev_tools_prune_errors = kwargs .pop ("dev_tools_prune_errors" , None ),
300
+ )
301
+
302
+ if len (kwargs ):
303
+ raise Exception (f"Invalid keyword argument: { list (kwargs .keys ())} " )
304
+
305
+ self .enable_dev_tools (** dev_tools_args )
306
+
307
+ # suppress warning banner printed to standard out
308
+ flask .cli .show_server_banner = lambda * args , ** kw : None
309
+
295
310
# prevent partial import of orjson when it's installed and mode=jupyterlab
296
311
# TODO: why do we need this? Why only in this mode? Importing here in
297
312
# all modes anyway, in case there's a way it can pop up in another mode
@@ -302,25 +317,32 @@ def run(
302
317
303
318
err_q = queue .Queue ()
304
319
320
+ server = make_server (
321
+ host , port , self .server ,
322
+ threaded = True ,
323
+ processes = 0
324
+ )
325
+ logging .getLogger ("werkzeug" ).setLevel (logging .ERROR )
326
+
305
327
@retry (
306
328
stop_max_attempt_number = 15 ,
307
329
wait_exponential_multiplier = 100 ,
308
330
wait_exponential_max = 1000
309
331
)
310
332
def run ():
311
333
try :
312
- super_run_server ( ** kwargs )
334
+ server . serve_forever ( )
313
335
except SystemExit :
314
336
pass
315
337
except Exception as error :
316
338
err_q .put (error )
317
339
raise error
318
340
319
- thread = StoppableThread (target = run )
320
- thread .setDaemon ( True )
341
+ thread = threading . Thread (target = run )
342
+ thread .daemon = True
321
343
thread .start ()
322
344
323
- self ._server_threads [(host , port )] = thread
345
+ self ._servers [(host , port )] = server
324
346
325
347
# Wait for server to start up
326
348
alive_url = "http://{host}:{port}/_alive_{token}" .format (
0 commit comments