diff --git a/CHANGELOG.md b/CHANGELOG.md index 806a9c33a2..8a08239826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Added +- [#2832](https://github.com/plotly/dash/pull/2832) Add dash startup route setup on Dash init. - [#2819](https://github.com/plotly/dash/pull/2819) Add dash subcomponents receive additional parameters passed by the parent component. Fixes [#2814](https://github.com/plotly/dash/issues/2814). - [#2826](https://github.com/plotly/dash/pull/2826) When using Pages, allows for `app.title` and (new) `app.description` to be used as defaults for the page title and description. Fixes [#2811](https://github.com/plotly/dash/issues/2811). - [#2795](https://github.com/plotly/dash/pull/2795) Allow list of components to be passed as layout. diff --git a/dash/dash.py b/dash/dash.py index 985fb01c4f..9309762c9e 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -372,6 +372,7 @@ class Dash: """ _plotlyjs_url: str + STARTUP_ROUTES: list = [] def __init__( # pylint: disable=too-many-statements self, @@ -556,6 +557,7 @@ def __init__( # pylint: disable=too-many-statements "JupyterDash is deprecated, use Dash instead.\n" "See https://dash.plotly.com/dash-in-jupyter for more details." ) + self.setup_startup_routes() def init_app(self, app=None, **kwargs): """Initialize the parts of Dash that require a flask app.""" @@ -1626,6 +1628,39 @@ def display_content(path): self.config.requests_pathname_prefix, path ) + @staticmethod + def add_startup_route(name, view_func, methods): + """ + Add a route to the app to be initialized at the end of Dash initialization. + Use this if the package requires a route to be added to the app, and you will not need to worry about at what point to add it. + + :param name: The name of the route. eg "my-new-url/path". + :param view_func: The function to call when the route is requested. The function should return a JSON serializable object. + :param methods: The HTTP methods that the route should respond to. eg ["GET", "POST"] or either one. + """ + if not isinstance(name, str) or name.startswith("/"): + raise ValueError("name must be a string and should not start with '/'") + + if not callable(view_func): + raise ValueError("view_func must be callable") + + valid_methods = {"POST", "GET"} + if not set(methods).issubset(valid_methods): + raise ValueError(f"methods should only contain {valid_methods}") + + if any(route[0] == name for route in Dash.STARTUP_ROUTES): + raise ValueError(f"Route name '{name}' is already in use.") + + Dash.STARTUP_ROUTES.append((name, view_func, methods)) + + def setup_startup_routes(self): + """ + Initialize the startup routes stored in STARTUP_ROUTES. + """ + for _name, _view_func, _methods in self.STARTUP_ROUTES: + self._add_url(f"_dash_startup_route/{_name}", _view_func, _methods) + self.STARTUP_ROUTES = [] + def _setup_dev_tools(self, **kwargs): debug = kwargs.get("debug", False) dev_tools = self._dev_tools = AttributeDict() diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 4108a33b7b..14bd018027 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -511,3 +511,24 @@ def on_nested_click(n_clicks): dash_duo.wait_for_element("#nested").click() dash_duo.wait_for_text_to_equal("#nested-output", "Clicked 1 times") + + +def test_inin030_add_startup_route(dash_duo): + url = "my-new-route" + + def my_route_f(): + return "hello" + + Dash.add_startup_route(url, my_route_f, ["POST"]) + + import requests + + app = Dash(__name__) + Dash.STARTUP_ROUTES = [] + app.layout = html.Div("Hello World") + dash_duo.start_server(app) + + url = f"{dash_duo.server_url}{app.config.requests_pathname_prefix}_dash_startup_route/{url}" + response = requests.post(url) + assert response.status_code == 200 + assert response.text == "hello"