Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

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

未關閉
#4,011 1 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 2 天內回覆

還沒有人認領這個 Issue。

評估

難度
4/5
預估耗時
3-5 天
新手友好度
68/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
python

研究方向

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.

由索引模型根據 Issue 內容生成。

描述

bug P3 size: 1

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_module unchanged)
  • 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).

主要語言
Python
星號
24.4k
分支
2.3k
平均合併
3 天 10 小時
30 天內合併 PR
24

環境準備

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

plotly/dash 的其他 Issue

查看 plotly/dash 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。