[BUG] Callbacks still double-register with uvicorn `--reload` when an explicit `name` is passed to `Dash()` (incomplete fix for #3818 / #3883)
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 55/100
調査の方向性
dash/_utils.py の alias_main_module から始め、明示的な名前を持つ提供済みの app と uvicorn の reload 設定で問題を再現します。reload worker が server をどのように import して callback を登録するかを追跡します。完了条件は、明示的な名前と app-factory パターンについて dependency output に各 callback が1回ずつ含まれ、既存の main の動作を壊さないことです。
索引モデルが issue の本文から書いたものです。
説明
Summary
Thanks for the fix! This is a follow up issue from #3818 . PR #3883 (released in 4.4.1) fixes the double-registration reported in #3818 but only for the exact idiom app = Dash(__name__) written at the top level of the entry-point file.
The fix (alias_main_module) keys entirely on the app's resolved name being "__main__" or "__mp_main__". As soon as Dash() receives any other name — a literal string, or Dash(__name__) when the app is built inside a package / app-factory (where __name__ is the module name) — the alias no-ops, the module is executed twice in the reload worker, and every callback is registered twice. The renderer then throws:
Duplicate callback outputs
Output 0 (output.children) is already in use.
Environment
- dash 4.4.1 (
dash[fastapi]) - uvicorn (standard), reload=True
- Python 3.10, Linux
Minimal reproduction
# repro.py
from dash import Dash, html, dcc, callback, Output, Input
# BUG TRIGGER: any explicit name that is not "__main__"/"__mp_main__".
# Using Dash(__name__) here (top level) works; Dash("myapp") does not.
app = Dash("myapp", backend="fastapi")
app.layout = html.Div([dcc.Input(id="input", value="Hello"), html.Div(id="output")])
@callback(Output("output", "children"), Input("input", "value"))
def update(value):
return f"You typed: {value}"
app.enable_dev_tools(debug=True, dev_tools_hot_reload=True)
server = app.server
if __name__ == "__main__":
import uvicorn
uvicorn.run("repro:server", host="127.0.0.1", port=8050, reload=True)
python3 repro.py
curl -s http://127.0.0.1:8050/_dash-dependencies | grep -o '"output.children"' | wc -l
# -> 2 (expected 1)
Open the page in a browser and the console shows Duplicate callback outputs.
Constructing the app inside a package with Dash(__name__) also reproduces it, because __name__ is the package name, not __main__, in this case -- __name__ == "app_factory":
- app_factory.py:
from dash import Dash, html, dcc, callback, Output, Input
class AppBuilder:
def __init__(self):
self.app = Dash(__name__, backend="fastapi")
self.app.layout = html.Div([
html.H1("Reload double-register demo (factory)"),
dcc.Input(id="input", value="Hello"),
html.Div(id="output"),
])
@callback(Output("output", "children"), Input("input", "value"))
def update(value):
return f"You typed: {value}"
def setup(self):
self.app.enable_dev_tools(debug=True, dev_tools_hot_reload=True)
return self.app.server
- main.py:
if __name__ != "__main__":
from app_factory import AppBuilder
server = AppBuilder().setup()
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:server", host="127.0.0.1", port=8050, reload=True)
Root Vause
dash/_utils.py:
def alias_main_module(caller_name: str) -> None:
if caller_name not in ("__main__", "__mp_main__"):
return
...
alias_main_module only aliases the running module into sys.modules when caller_name is exactly "__main__"/"__mp_main__". Passing an explicit name (or building the app anywhere other than the entry module's top level) makes caller_name something else, so the alias is skipped, the "main:server" import string re-executes the file, and the callbacks register a second time.
This means the fix effectively only covers the single-file app = Dash(__name__) tutorial layout, and misses the very common app-factory / class-based / explicitly-named app patterns.
Current Workaround
# Skip the reload worker's __mp_main__ spawn re-exec so callbacks are registered exactly once.
if __name__ not in ("__main__", "__mp_main__"):
server = build_app()
Works temporarily but idk if that will break in any future releases.
- 主要言語
- Python
- スター
- 24.4k
- フォーク
- 2.3k
- 平均マージ
- 1日 19時間
- マージ済み PR(30日)
- 19
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
plotly/dash のほかの issue
-
good first issue P3 size: 1 task
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
-
enhancement P2 size: 5
難易度 5/5 1週間以上 初心者へのやさしさ 45/100
-
enhancement P3 size: 10+
-
P2 size: 1 task
-
enhancement P3 size: 1
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
似ている issue
-
essnmx good first issue
難易度 1/5 1時間未満 初心者へのやさしさ 95/100
-
[Feature] 奇物选择添加优先级 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
syfoud/Simulated_Scepter#174 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
Giskard-AI/giskard-oss#2840 · コメント 1 件 ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success オープンarea: repo bug perceived difficulty: 2
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
yeti-platform/yeti#1380 ·