-
- Notifications
You must be signed in to change notification settings - Fork 2.2k
Add hooks #3029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add hooks #3029
Changes from 2 commits
54d338d 1b4d1a3 8b68e62 fde59e8 59234db 89d6a93 d405c72 d26c9d3 ee1ddea d69a87c bd1c8f2 d3d88c1 998171a b405e1b 7e2f37e File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import typing as _t | ||
| | ||
| _ns = { | ||
| "setup": [], | ||
| "layout": [], | ||
| "routes": [], | ||
| "error": [], | ||
| "callback": [], | ||
| } | ||
| | ||
| | ||
| def layout(func): | ||
| """ | ||
| Run a function when serving the layout, the return value | ||
| ||
| will be used as the layout. | ||
| """ | ||
| _ns["layout"].append(func) | ||
| return func | ||
| | ||
| | ||
| def setup(func): | ||
| """ | ||
| Can be used to get a reference to the app after it is instantiated. | ||
| """ | ||
| _ns["setup"].append(func) | ||
| return func | ||
| | ||
| | ||
| def route(name=None, methods=("GET",)): | ||
| """ | ||
| Add a route to the Dash server. | ||
| """ | ||
| | ||
| def wrap(func): | ||
| _name = name or func.__name__ | ||
| _ns["routes"].append((_name, func, methods)) | ||
| ||
| return func | ||
| | ||
| return wrap | ||
| | ||
| | ||
| def error(func: _t.Callable[[Exception], _t.Any]): | ||
| ||
| """Automatically add an error handler to the dash app.""" | ||
| _ns["error"].append(func) | ||
| return func | ||
| | ||
| | ||
| def callback(*args, **kwargs): | ||
| """ | ||
| Add a callback to all the apps with the hook installed. | ||
| """ | ||
| | ||
| def wrap(func): | ||
| _ns["callback"].append((list(args), dict(kwargs), func)) | ||
| ||
| return func | ||
| | ||
| return wrap | ||
| | ||
| | ||
| class HooksManager: | ||
| _registered = False | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add a comment explaining what Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above | ||
| | ||
| # pylint: disable=too-few-public-methods | ||
| class HookErrorHandler: | ||
| def __init__(self, original, hooks): | ||
| self.original = original | ||
| self.hooks = hooks | ||
| | ||
| def __call__(self, err: Exception): | ||
| result = None | ||
| if self.original: | ||
| result = self.original(err) | ||
| hook_result = None | ||
| for hook in HooksManager.get_hooks("error"): | ||
| hook_result = hook(err) | ||
| return result or hook_result | ||
| | ||
| @staticmethod | ||
| def get_hooks(hook: str): | ||
| return _ns.get(hook, []).copy() | ||
| | ||
| @classmethod | ||
| def register_setuptools(cls): | ||
| if cls._registered: | ||
| return | ||
| | ||
| import importlib.metadata # pylint: disable=import-outside-toplevel | ||
T4rk1n marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| | ||
| for dist in importlib.metadata.distributions(): | ||
| for entry in dist.entry_points: | ||
| if entry.group != "dash": | ||
| ||
| continue | ||
| entry.load() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| from flask import jsonify | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice | ||
| import requests | ||
| import pytest | ||
| | ||
| from dash import Dash, Input, Output, html, hooks, set_props | ||
| | ||
| | ||
| @pytest.fixture(scope="module", autouse=True) | ||
| def hook_cleanup(): | ||
| hooks._ns["layout"] = [] | ||
| hooks._ns["setup"] = [] | ||
| hooks._ns["route"] = [] | ||
| hooks._ns["error"] = [] | ||
| hooks._ns["callback"] = [] | ||
| | ||
| | ||
| def test_hook001_layout(dash_duo): | ||
| @hooks.layout | ||
| def on_layout(layout): | ||
| return [html.Div("Header", id="header")] + layout | ||
| | ||
| app = Dash() | ||
| app.layout = [html.Div("Body", id="body")] | ||
| | ||
| dash_duo.start_server(app) | ||
| | ||
| dash_duo.wait_for_text_to_equal("#header", "Header") | ||
| dash_duo.wait_for_text_to_equal("#body", "Body") | ||
| | ||
| | ||
| def test_hook002_setup(): | ||
| setup_title = None | ||
| | ||
| @hooks.setup | ||
| def on_setup(app: Dash): | ||
| nonlocal setup_title | ||
| setup_title = app.title | ||
| | ||
| app = Dash(title="setup-test") | ||
| app.layout = html.Div("setup") | ||
| | ||
| assert setup_title == "setup-test" | ||
| | ||
| | ||
| def test_hook003_route(dash_duo): | ||
| @hooks.route(methods=("POST",)) | ||
| def hook_route(): | ||
| return jsonify({"success": True}) | ||
| | ||
| app = Dash() | ||
| app.layout = html.Div("hook route") | ||
| | ||
| dash_duo.start_server(app) | ||
| response = requests.post(f"{dash_duo.server_url}/hook_route") | ||
| data = response.json() | ||
| assert data["success"] | ||
| | ||
| | ||
| def test_hook004_error(dash_duo): | ||
| @hooks.error | ||
| def on_error(error): | ||
| set_props("error", {"children": str(error)}) | ||
| | ||
| app = Dash() | ||
| app.layout = [html.Button("start", id="start"), html.Div(id="error")] | ||
| | ||
| @app.callback(Input("start", "n_clicks"), prevent_initial_call=True) | ||
| def on_click(_): | ||
| raise Exception("hook error") | ||
| | ||
| dash_duo.start_server(app) | ||
| dash_duo.wait_for_element("#start").click() | ||
| dash_duo.wait_for_text_to_equal("#error", "hook error") | ||
| | ||
| | ||
| def test_hook005_callback(dash_duo): | ||
| @hooks.callback( | ||
| Output("output", "children"), | ||
| Input("start", "n_clicks"), | ||
| prevent_initial_call=True, | ||
| ) | ||
| def on_hook_cb(n_clicks): | ||
| return f"clicked {n_clicks}" | ||
| | ||
| app = Dash() | ||
| app.layout = [ | ||
| html.Button("start", id="start"), | ||
| html.Div(id="output"), | ||
| ] | ||
| | ||
| dash_duo.start_server(app) | ||
| dash_duo.wait_for_element("#start").click() | ||
| dash_duo.wait_for_text_to_equal("#output", "clicked 1") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just curious: why this rather than importing the specific things you need from
typing(e.g.,from typing import TypeVar)?