From 9af721d5f199771e6a3a61353b7a181b5792091c Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 12 Sep 2022 14:18:34 -0400 Subject: [PATCH 1/6] Replace bare exception with PageError --- dash/_validate.py | 5 +++-- dash/exceptions.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dash/_validate.py b/dash/_validate.py index a4eb61cf1e..8bee40594d 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -8,6 +8,7 @@ from .development.base_component import Component from . import exceptions from ._utils import patch_collections_abc, stringify_id, to_json, coerce_to_list +from .exceptions import PageError def validate_callback(outputs, inputs, state, extra_args, types): @@ -462,10 +463,10 @@ def validate_pages_layout(module, page): def validate_use_pages(config): if not config.get("assets_folder", None): - raise Exception("`dash.register_page()` must be called after app instantiation") + raise PageError("`dash.register_page()` must be called after app instantiation") if flask.has_request_context(): - raise Exception( + raise PageError( """ dash.register_page() can’t be called within a callback as it updates dash.page_registry, which is a global variable. For more details, see https://dash.plotly.com/sharing-data-between-callbacks#why-global-variables-will-break-your-app diff --git a/dash/exceptions.py b/dash/exceptions.py index d2fa911a85..4db779aa6b 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -93,3 +93,7 @@ class LongCallbackError(DashException): class MissingLongCallbackManagerError(DashException): pass + + +class PageError(DashException): + pass From ed38d3bac369aaf2701dee514823556b13a12dc9 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 12 Sep 2022 14:30:41 -0400 Subject: [PATCH 2/6] Ignore page register from callback context value. --- dash/_pages.py | 4 ++++ dash/dash.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/dash/_pages.py b/dash/_pages.py index bf09dd2a4c..4a15d44702 100644 --- a/dash/_pages.py +++ b/dash/_pages.py @@ -9,6 +9,7 @@ from . import _validate from ._utils import AttributeDict from ._get_paths import get_relative_path +from ._callback_context import context_value CONFIG = AttributeDict() @@ -250,6 +251,9 @@ def register_page( ]) ``` """ + if context_value.get().get("ignore_register_page"): + return + _validate.validate_use_pages(CONFIG) page = dict( diff --git a/dash/dash.py b/dash/dash.py index b51e804220..d201d8efa5 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1208,6 +1208,8 @@ def dispatch(self): cb = self.callback_map[output] func = cb["callback"] + g.ignore_register_page = cb.get("long", False) + # Add args_grouping inputs_state_indices = cb["inputs_state_indices"] inputs_state = inputs + state From ba9876aa8a560da41d33ab12a6c5f4747e7f8e4d Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 12 Sep 2022 14:45:53 -0400 Subject: [PATCH 3/6] Add dash.testing.ignore_register_page contest manager. --- dash/testing/__init__.py | 17 +++++++++++++++++ tests/unit/test_testing.py | 7 +++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/unit/test_testing.py diff --git a/dash/testing/__init__.py b/dash/testing/__init__.py index e69de29bb2..8d33331a5f 100644 --- a/dash/testing/__init__.py +++ b/dash/testing/__init__.py @@ -0,0 +1,17 @@ +from contextlib import contextmanager + +from .._callback_context import context_value as _ctx +from .._utils import AttributeDict as _AD + + +@contextmanager +def ignore_register_page(): + previous = _ctx.get() + copied = _AD(previous) + copied.ignore_register_page = True + _ctx.set(copied) + + try: + yield + finally: + _ctx.set(previous) diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py new file mode 100644 index 0000000000..b9c1cca344 --- /dev/null +++ b/tests/unit/test_testing.py @@ -0,0 +1,7 @@ +from dash.testing import ignore_register_page +from dash import register_page + + +def test_tst001_ignore_register_page(): + with ignore_register_page(): + register_page("/") From 68866708345273d9675e0ac657a44e393acdb227 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 12 Sep 2022 14:52:27 -0400 Subject: [PATCH 4/6] Rename tests.unit.dash to tests.unit.library (messes with IDE imports) --- tests/unit/{dash => library}/fixtures.py | 0 tests/unit/{dash => library}/test_async_resources.py | 0 tests/unit/{dash => library}/test_grouped_callbacks.py | 0 tests/unit/{dash => library}/test_grouping.py | 0 tests/unit/{dash => library}/test_long_callback_validation.py | 0 tests/unit/{dash => library}/test_utils.py | 0 tests/unit/{dash => library}/test_validate.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{dash => library}/fixtures.py (100%) rename tests/unit/{dash => library}/test_async_resources.py (100%) rename tests/unit/{dash => library}/test_grouped_callbacks.py (100%) rename tests/unit/{dash => library}/test_grouping.py (100%) rename tests/unit/{dash => library}/test_long_callback_validation.py (100%) rename tests/unit/{dash => library}/test_utils.py (100%) rename tests/unit/{dash => library}/test_validate.py (100%) diff --git a/tests/unit/dash/fixtures.py b/tests/unit/library/fixtures.py similarity index 100% rename from tests/unit/dash/fixtures.py rename to tests/unit/library/fixtures.py diff --git a/tests/unit/dash/test_async_resources.py b/tests/unit/library/test_async_resources.py similarity index 100% rename from tests/unit/dash/test_async_resources.py rename to tests/unit/library/test_async_resources.py diff --git a/tests/unit/dash/test_grouped_callbacks.py b/tests/unit/library/test_grouped_callbacks.py similarity index 100% rename from tests/unit/dash/test_grouped_callbacks.py rename to tests/unit/library/test_grouped_callbacks.py diff --git a/tests/unit/dash/test_grouping.py b/tests/unit/library/test_grouping.py similarity index 100% rename from tests/unit/dash/test_grouping.py rename to tests/unit/library/test_grouping.py diff --git a/tests/unit/dash/test_long_callback_validation.py b/tests/unit/library/test_long_callback_validation.py similarity index 100% rename from tests/unit/dash/test_long_callback_validation.py rename to tests/unit/library/test_long_callback_validation.py diff --git a/tests/unit/dash/test_utils.py b/tests/unit/library/test_utils.py similarity index 100% rename from tests/unit/dash/test_utils.py rename to tests/unit/library/test_utils.py diff --git a/tests/unit/dash/test_validate.py b/tests/unit/library/test_validate.py similarity index 100% rename from tests/unit/dash/test_validate.py rename to tests/unit/library/test_validate.py From 58273874921a223e2864a8fffe867c1b04bbfba3 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 14 Sep 2022 12:38:51 -0400 Subject: [PATCH 5/6] Remove ignore register page flag when calling background callbacks. --- dash/_callback.py | 1 + dash/long_callback/managers/celery_manager.py | 4 +- .../managers/diskcache_manager.py | 56 +++++++++++-------- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index da09369389..9d58fe6edd 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -374,6 +374,7 @@ def add_context(*args, **kwargs): input_values=callback_ctx.input_values, state_values=callback_ctx.state_values, triggered_inputs=callback_ctx.triggered_inputs, + ignore_register_page=True, ), ) diff --git a/dash/long_callback/managers/celery_manager.py b/dash/long_callback/managers/celery_manager.py index cc69a2bc27..fba09af65c 100644 --- a/dash/long_callback/managers/celery_manager.py +++ b/dash/long_callback/managers/celery_manager.py @@ -147,7 +147,9 @@ def _set_progress(progress_value): ctx = copy_context() def run(): - context_value.set(AttributeDict(**context)) + c = AttributeDict(**context) + c.ignore_register_page = False + context_value.set(c) try: if isinstance(user_callback_args, dict): user_callback_output = fn(*maybe_progress, **user_callback_args) diff --git a/dash/long_callback/managers/diskcache_manager.py b/dash/long_callback/managers/diskcache_manager.py index 7ddf8feb73..a767aa4fd9 100644 --- a/dash/long_callback/managers/diskcache_manager.py +++ b/dash/long_callback/managers/diskcache_manager.py @@ -1,6 +1,9 @@ import traceback +from contextvars import copy_context from . import BaseLongCallbackManager +from ..._callback_context import context_value +from ..._utils import AttributeDict from ...exceptions import PreventUpdate _pending_value = "__$pending__" @@ -110,12 +113,13 @@ def make_job_fn(self, fn, progress): def clear_cache_entry(self, key): self.handle.delete(key) + # noinspection PyUnresolvedReferences def call_job_fn(self, key, job_fn, args, context): # pylint: disable-next=import-outside-toplevel,no-name-in-module,import-error from multiprocess import Process # pylint: disable-next=not-callable - proc = Process(target=job_fn, args=(key, self._make_progress_key(key), args)) + proc = Process(target=job_fn, args=(key, self._make_progress_key(key), args, context)) proc.start() return proc.pid @@ -147,7 +151,7 @@ def get_result(self, key, job): def _make_job_fn(fn, cache, progress): - def job_fn(result_key, progress_key, user_callback_args): + def job_fn(result_key, progress_key, user_callback_args, context): def _set_progress(progress_value): if not isinstance(progress_value, (list, tuple)): progress_value = [progress_value] @@ -156,27 +160,35 @@ def _set_progress(progress_value): maybe_progress = [_set_progress] if progress else [] - try: - if isinstance(user_callback_args, dict): - user_callback_output = fn(*maybe_progress, **user_callback_args) - elif isinstance(user_callback_args, (list, tuple)): - user_callback_output = fn(*maybe_progress, *user_callback_args) + ctx = copy_context() + + def run(): + c = AttributeDict(**context) + c.ignore_register_page = False + context_value.set(c) + try: + if isinstance(user_callback_args, dict): + user_callback_output = fn(*maybe_progress, **user_callback_args) + elif isinstance(user_callback_args, (list, tuple)): + user_callback_output = fn(*maybe_progress, *user_callback_args) + else: + user_callback_output = fn(*maybe_progress, user_callback_args) + except PreventUpdate: + cache.set(result_key, {"_dash_no_update": "_dash_no_update"}) + except Exception as err: # pylint: disable=broad-except + cache.set( + result_key, + { + "long_callback_error": { + "msg": str(err), + "tb": traceback.format_exc(), + } + }, + ) else: - user_callback_output = fn(*maybe_progress, user_callback_args) - except PreventUpdate: - cache.set(result_key, {"_dash_no_update": "_dash_no_update"}) - except Exception as err: # pylint: disable=broad-except - cache.set( - result_key, - { - "long_callback_error": { - "msg": str(err), - "tb": traceback.format_exc(), - } - }, - ) - else: - cache.set(result_key, user_callback_output) + cache.set(result_key, user_callback_output) + + ctx.run(run) return job_fn From e8467cdcca28cbf4bdeb2937aa192079952e21af Mon Sep 17 00:00:00 2001 From: philippe Date: Fri, 16 Sep 2022 09:02:02 -0400 Subject: [PATCH 6/6] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad18b27287..f3c8e6421e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#2152](https://github.com/plotly/dash/pull/2152) Fix bug [#2128](https://github.com/plotly/dash/issues/2128) preventing rendering of multiple components inside a dictionary. - [#2187](https://github.com/plotly/dash/pull/2187) Fix confusing error message when trying to use pytest fixtures but `dash[testing]` is not installed. - [#2202](https://github.com/plotly/dash/pull/2202) Fix bug [#2185](https://github.com/plotly/dash/issues/2185) when you copy text with multiple quotes into a table +- [#2226](https://github.com/plotly/dash/pull/2226) Fix [#2219](https://github.com/plotly/dash/issues/2219) pages register & background callbacks. ## [2.6.1] - 2022-08-01