Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- [#3986](https://github.com/plotly/dash/pull/3986) Adjust `_run_before_hooks` in the `fastapi` backend to honor a response returned by a `before_request` function, matching the `flask` backend's behavior.

### Fixed
- [#4053](https://github.com/plotly/dash/pull/4053) Fix callbacks being registered twice under a reloader (e.g. `uvicorn --reload`) when `Dash()` is given an explicit `name` or is built by an app factory called from the main module. Fixes [#3912](https://github.com/plotly/dash/issues/3912).
- [#3980](https://github.com/plotly/dash/pull/3980) Fix the three `before_request` hooks (`Dash._setup_server` and the pages `router_sync` / `router_async`) publishing their "already done" guard flag before the setup work behind it had run. Under a multi-threaded WSGI worker such as `gunicorn -k gthread` (or under an ASGI worker for the async router), a second request arriving mid-setup could observe the flag already set, skip setup, then read `registered_paths` / `callback_map` / the pages router callback while they were still being registered - causing the first burst of component bundle requests after a restart to 500 with `Error loading dependency. "<lib>" is not a registered library`, or the pages router to hit `DuplicateCallback` when two workers raced past the guard. Each hook body now runs under a lock (`threading.Lock` for the two sync hooks, an `asyncio.Lock` bound to the running loop for the async router) and only publishes the flag after all work completes. Fixes [#3971](https://github.com/plotly/dash/issues/3971).
- [#3944](https://github.com/plotly/dash/pull/3944) Fix `dash.testing` runner backend detection for wrapped FastAPI/Quart servers so threaded Flask-only options are not passed to ASGI runners.
- [#3955](https://github.com/plotly/dash/pull/3955) Unpin `selenium` in the testing requirements (was capped at `<=4.2.0` from 2022) and require `>=4.11.0`, so it can drive current stable Chrome via Selenium Manager and stop the widespread CI flakiness.
Expand Down
7 changes: 5 additions & 2 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,12 @@ def __init__( # pylint: disable=too-many-statements, too-many-branches
if not csrf_header_name or not csrf_header_name.strip():
raise ValueError("csrf_header_name must be a non-empty string")

caller_name: str = name if name is not None else get_caller_name()
module_name = get_caller_name()
caller_name: str = name if name is not None else module_name

alias_main_module(caller_name)
# Alias based on the module that is running, not the app name: an
# explicit ``name`` or an app factory still runs from the main module.
alias_main_module(module_name)

# Determine backend
if backend is None:
Expand Down
119 changes: 119 additions & 0 deletions tests/unit/test_main_module_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,122 @@ def test_no_sibling_import_when_script_dir_has_same_name_module(tmp_path, monkey
for spec in _callback.GLOBAL_CALLBACK_LIST
if spec["output"] not in ("alias-out.children", "sibling-out.children")
]


NAMED_APP_SOURCE = """
from dash import Dash, html, dcc, callback, Output, Input
app = Dash("myapp")
app.layout = html.Div([
dcc.Input(id="named-alias-in", value="hello"),
html.Div(id="named-alias-out"),
])
@callback(Output("named-alias-out", "children"), Input("named-alias-in", "value"))
def update(value):
return value
server = app.server
"""

NAMED_MODULE_NAME = "dash_test_alias_named"


def test_main_module_alias_with_explicit_name(tmp_path, monkeypatch):
"""Passing an explicit ``name`` to ``Dash()`` must not disable the alias:
the running main module is still re-imported by its import string. See
issue #3912."""
from dash import _callback

app_file = tmp_path / f"{NAMED_MODULE_NAME}.py"
app_file.write_text(NAMED_APP_SOURCE)
monkeypatch.syspath_prepend(str(tmp_path))

try:
main_module = _run_as(app_file, "__mp_main__")

imported = importlib.import_module(NAMED_MODULE_NAME)
assert imported is main_module

specs = [
spec
for spec in _callback.GLOBAL_CALLBACK_LIST
if spec["output"] == "named-alias-out.children"
]
assert len(specs) == 1
finally:
sys.modules.pop("__mp_main__", None)
sys.modules.pop(NAMED_MODULE_NAME, None)
_callback.GLOBAL_CALLBACK_MAP.pop("named-alias-out.children", None)
_callback.GLOBAL_CALLBACK_LIST[:] = [
spec
for spec in _callback.GLOBAL_CALLBACK_LIST
if spec["output"] != "named-alias-out.children"
]


FACTORY_SOURCE = """
from dash import Dash, html, dcc, callback, Output, Input
def build():
app = Dash(__name__)
app.layout = html.Div([
dcc.Input(id="factory-alias-in", value="hello"),
html.Div(id="factory-alias-out"),
])
@callback(
Output("factory-alias-out", "children"), Input("factory-alias-in", "value")
)
def update(value):
return value
return app.server
"""

FACTORY_MAIN_SOURCE = """
from dash_test_alias_factory import build
server = build()
"""

FACTORY_MAIN_NAME = "dash_test_alias_factory_main"


def test_main_module_alias_with_app_factory(tmp_path, monkeypatch):
"""An app built by a factory in another module, called from the top level
of the main module, gets that module's ``__name__``. The running main
module must still be aliased so the import string does not build the app
a second time. See issue #3912."""
from dash import _callback

(tmp_path / "dash_test_alias_factory.py").write_text(FACTORY_SOURCE)
app_file = tmp_path / f"{FACTORY_MAIN_NAME}.py"
app_file.write_text(FACTORY_MAIN_SOURCE)
monkeypatch.syspath_prepend(str(tmp_path))

try:
main_module = _run_as(app_file, "__mp_main__")

imported = importlib.import_module(FACTORY_MAIN_NAME)
assert imported is main_module

specs = [
spec
for spec in _callback.GLOBAL_CALLBACK_LIST
if spec["output"] == "factory-alias-out.children"
]
assert len(specs) == 1
finally:
sys.modules.pop("__mp_main__", None)
sys.modules.pop(FACTORY_MAIN_NAME, None)
sys.modules.pop("dash_test_alias_factory", None)
_callback.GLOBAL_CALLBACK_MAP.pop("factory-alias-out.children", None)
_callback.GLOBAL_CALLBACK_LIST[:] = [
spec
for spec in _callback.GLOBAL_CALLBACK_LIST
if spec["output"] != "factory-alias-out.children"
]
Loading