1+ import logging
2+
13import dash
24import os
35import requests
911import sys
1012import inspect
1113import traceback
14+ import threading
1215import warnings
1316import queue
1417
1821from ansi2html import Ansi2HTMLConverter
1922import uuid
2023
24+ from werkzeug .serving import make_server
25+
2126from .comms import _dash_comm , _jupyter_config , _request_jupyter_config
22- from ._stoppable_thread import StoppableThread
2327
2428
2529def _get_skip (error : Exception ):
@@ -50,7 +54,7 @@ class JupyterDash(dash.Dash):
5054 _in_colab = "google.colab" in sys .modules
5155 _token = str (uuid .uuid4 ())
5256
53- _server_threads = {}
57+ _servers = {}
5458
5559 @classmethod
5660 def infer_jupyter_proxy_config (cls ):
@@ -147,6 +151,7 @@ def alive():
147151 return 'Alive'
148152
149153 self .server .logger .disabled = True
154+ self ._exception_handling_added = False
150155
151156 def run (
152157 self ,
@@ -186,11 +191,8 @@ def run(
186191 return
187192
188193 # 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" )))
194196
195197 # Validate / infer display mode
196198 if JupyterDash ._in_colab :
@@ -222,11 +224,10 @@ def run(
222224 inline_exceptions = mode == "inline"
223225
224226 # Terminate any existing server using this port
225- old_server = self ._server_threads .get ((host , port ))
227+ old_server = self ._servers .get ((host , port ))
226228 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 )]
230231
231232 # Configure pathname prefix
232233 requests_pathname_prefix = self .config .get ('requests_pathname_prefix' , None )
@@ -256,11 +257,7 @@ def run(
256257 )
257258
258259 # 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 )
264261
265262 # Enable supported dev tools
266263 if debug :
@@ -283,15 +280,33 @@ def run(
283280 # there is no active kernel.
284281 kwargs ['dev_tools_hot_reload' ] = mode == "external"
285282
286- # suppress warning banner printed to standard out
287- flask .cli .show_server_banner = lambda * args , ** kwargs : None
288-
289283 # Set up custom callback exception handling
290284 self ._config_callback_exception_handling (
291285 dev_tools_prune_errors = kwargs .get ('dev_tools_prune_errors' , True ),
292286 inline_exceptions = inline_exceptions ,
293287 )
294288
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+
295310 # prevent partial import of orjson when it's installed and mode=jupyterlab
296311 # TODO: why do we need this? Why only in this mode? Importing here in
297312 # all modes anyway, in case there's a way it can pop up in another mode
@@ -302,25 +317,32 @@ def run(
302317
303318 err_q = queue .Queue ()
304319
320+ server = make_server (
321+ host , port , self .server ,
322+ threaded = True ,
323+ processes = 0
324+ )
325+ logging .getLogger ("werkzeug" ).setLevel (logging .ERROR )
326+
305327 @retry (
306328 stop_max_attempt_number = 15 ,
307329 wait_exponential_multiplier = 100 ,
308330 wait_exponential_max = 1000
309331 )
310332 def run ():
311333 try :
312- super_run_server ( ** kwargs )
334+ server . serve_forever ( )
313335 except SystemExit :
314336 pass
315337 except Exception as error :
316338 err_q .put (error )
317339 raise error
318340
319- thread = StoppableThread (target = run )
320- thread .setDaemon ( True )
341+ thread = threading . Thread (target = run )
342+ thread .daemon = True
321343 thread .start ()
322344
323- self ._server_threads [(host , port )] = thread
345+ self ._servers [(host , port )] = server
324346
325347 # Wait for server to start up
326348 alive_url = "http://{host}:{port}/_alive_{token}" .format (
0 commit comments