Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

Commit 044acaa

Browse files
authored
Merge pull request #105 from plotly/make-server
Use werkzeug make_server for starting & shutdown
2 parents 86e5746 + 837aa0e commit 044acaa

File tree

3 files changed

+46
-46
lines changed

3 files changed

+46
-46
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
## [UNRELEASED]
66
### Fixed
77
- Propagate start error message. [#94](https://github.com/plotly/jupyter-dash/pull/94)
8+
- Fix rerun server with newer flask/werkzeug. [#105](https://github.com/plotly/jupyter-dash/pull/105)
89

910
### Added
1011

jupyter_dash/_stoppable_thread.py

-23
This file was deleted.

jupyter_dash/jupyter_app.py

+45-23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
import dash
24
import os
35
import requests
@@ -9,6 +11,7 @@
911
import sys
1012
import inspect
1113
import traceback
14+
import threading
1215
import warnings
1316
import queue
1417

@@ -18,8 +21,9 @@
1821
from ansi2html import Ansi2HTMLConverter
1922
import uuid
2023

24+
from werkzeug.serving import make_server
25+
2126
from .comms import _dash_comm, _jupyter_config, _request_jupyter_config
22-
from ._stoppable_thread import StoppableThread
2327

2428

2529
def _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

Comments
 (0)