Dash 4.4.1: alias_main_module() causes double callback registration when app entrypoint is run as `python <subdir>/<script>.py` from a parent directory
I maintainer di solito rispondono entro 2 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 68/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- python
- Ambito
- backend, data-visualization, web-dev
Direzione di ricerca
Reproduce the duplicate registration with the app/app.py and pages/home.py layout, then inspect dash/_utils.py (alias_main_module), its call in dash/dash.py, and the loader in dash/_pages.py. Verify the change against the shown /_dash-dependencies request: one page callback should be registered when the entrypoint is run from its parent directory, without regressing the reload-import case.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
dash.dash.alias_main_module() (added in #3883, to fix uvicorn.run("app:server", reload=True) double-import, #3818) causes a different double-import when an app's entrypoint file has a directory-name-equals-basename shape (e.g. app/app.py) and is launched with python app/app.py from the parent directory — the layout used by every "run the script directly" tutorial and by many real apps' READMEs. The self-reimport causes Dash's pages loader to execute every page module a second time, double-registering every page-level @callback, which breaks the client with "Duplicate callback outputs" errors.
Confirmed present in 4.4.1 and unchanged in 4.5.0rc0. Not present in 4.4.0 (no alias_main_module in that wheel) or 4.3.0.
Minimal repro
mkdir -p repro/app/pages
cat > repro/app/app.py <<'EOF'
from dash import Dash, html, dcc, callback, Output, Input
app = Dash(__name__, use_pages=True, pages_folder="pages")
app.layout = html.Div([dcc.Location(id="url"), dash.page_container])
if __name__ == "__main__":
print("callbacks registered:", len(dash._callback.GLOBAL_CALLBACK_LIST))
app.run(debug=False)
EOF
cat > repro/app/pages/home.py <<'EOF'
import dash
from dash import html, callback, Output, Input, dcc
dash.register_page(__name__, path="/")
layout = html.Div([html.Button("go", id="btn"), html.Div(id="out")])
@callback(Output("out", "children"), Input("btn", "n_clicks"))
def show(n):
return f"clicked {n}"
EOF
cd repro
python app/app.py
Then, in a second terminal:
curl -s http://127.0.0.1:8050/_dash-dependencies | python3 -m json.tool
Expected
One Output("out", "children") callback registered — a single entry in /_dash-dependencies for that output.
Actual
The Output("out", "children") callback (and every other page-level callback) is registered twice, byte-identical, because pages/home.py is exec_module'd twice by Dash's pages loader (dash/_pages.py, unconditional spec.loader.exec_module(), no sys.modules check). In a full app this manifests client-side as "Duplicate callback outputs".
Root cause
alias_main_module() (dash/_utils.py, called from dash/dash.py:511, first statement in Dash.__init__) does, roughly:
import_name = canonical_import_name(module_file) # relpath(app/app.py, cwd) -> "app.app"
if import_name is None or import_name in sys.modules:
return
spec = importlib.util.find_spec(import_name) # side effect: imports parent package "app" first
When the script is run as python app/app.py from the parent directory, sys.path[0] is set to app/ (the script's own directory), not the parent. importlib.util.find_spec("app.app") must first import the parent package app to resolve the dotted name — and because app/ is on sys.path, Python resolves app to app/app.py itself (there's no app/__init__.py; a top-level module named app shadows the script). That reimport executes app/app.py again, top-to-bottom, under module name app (not __main__), which calls Dash(use_pages=True) a second time and — because Dash's pages loader doesn't consult sys.modules before exec_module-ing page files — every page-level @callback is appended to dash._callback.GLOBAL_CALLBACK_LIST a second time. find_spec then raises (app "is not a package"), which alias_main_module swallows (except (ImportError, ValueError, OSError): pass), so the bug is silent until the client renders and finds duplicate outputs.
This is the mirror-image of #3818: that fix special-cased uvicorn --reload's dual-process import; this path is a single-process python <subdir>/<same-name>.py launch, which is common enough (app/app.py, src/app.py, any Flask/Dash tutorial's "python app.py" instruction run from one directory up) that it's likely to recur for other users bumping past 4.4.0.
Suggested fix
alias_main_module could avoid the side-effectful find_spec on the dotted name entirely by resolving only the top-level package with importlib.machinery.PathFinder.find_spec(top_level, path=[the parent of module_file's directory]) and comparing paths, without ever importing anything — or by checking top_level not in sys.modules and, if the resulting module's __file__ matches module_file, skip re-registration instead of relying on the find_spec exception path. Alternatively, guard early: skip aliasing entirely when sys.modules[import_name.split(".")[0]] would resolve to the currently-executing __main__ file (i.e., directory name equals script basename).
Versions
- dash 4.4.1 (also reproduced on 4.5.0rc0 wheel inspection —
alias_main_moduleunchanged) - Python 3.12
- macOS / Linux (path-shadowing mechanism is OS-independent; not tested on Windows)
Workaround
In the entrypoint file, before constructing Dash(...):
import sys
if __name__ == "__main__":
sys.modules.setdefault("app", sys.modules[__name__])
This pre-seeds sys.modules["app"] so alias_main_module's find_spec returns immediately (first line: if import_name in sys.modules: return — wait, that guard checks import_name i.e. "app.app", not "app"; the workaround instead makes the nested find_spec("app.app") short-circuit because Python's import machinery sees app already in sys.modules and does not re-execute it). No effect under the production gunicorn app:server launch shape (there, caller_name is app, not __main__, so alias_main_module returns at its very first line).
- Lingua principale
- Python
- Stelle
- 24.4k
- Fork
- 2.3k
- Merge medio
- 1g 20h
- PR unite (30g)
- 21
Preparare l'ambiente
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di plotly/dash
-
good first issue P3 size: 1 task
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
plotly/dash#3735 · 3 commenti ·
I maintainer di solito rispondono entro 2 giorni
-
bug P2 plotly-internal size: 3
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
plotly/dash#4013 · 1 commento ·
I maintainer di solito rispondono entro 2 giorni
-
Shared-storage topics are never freed: streaming leaks ~32 frames per page loadForse già presa @T4rk1n l’ha presa 4 giorni fa. Apertabug P1 size: 3
plotly/dash#4010 · 1 assegnatario ·
I maintainer di solito rispondono entro 2 giorni
-
[BUG] disable autocomplete in the date pickers inputForse già presa @AnnMarieW l’ha presa 4 giorni fa. Apertabug P2 size: 1
plotly/dash#4009 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 2 giorni
-
[BUG] Plotly Cloud devtool route crashes with FastAPI and Quart backendsForse già presa @T4rk1n l’ha presa 4 giorni fa. Apertabug P1 size: 1
Difficoltà 3/5 1-2 giorni Idoneità per principianti 65/100
plotly/dash#4008 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 2 giorni
Issue simili
-
[Bug] @deck.gl/arcgis dist import resolves to unpublished @deck.gl/core source path (9.3.11, 9.4.0)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
workflow: a tick's dispatch counts as 'only this step', and no review self-grants a round unattendedApertaworkflow
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
kristofdegrave/homeassistant-smart-charging#1505 ·
I maintainer di solito rispondono entro 1 giorno
-
New Submission: TropWATERApertametadata submission
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
canonical/content-cache-operator#163 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
[submission]Apertasubmission
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 65/100
leanprover/lean-eval-submissions#1852 ·
I maintainer di solito rispondono entro 1 giorno