Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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

Đã đóng
#4,011 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
68/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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).

Ngôn ngữ chính
Python
Star
24.4k
Fork
2.3k
Merge trung bình
3 ngày 11 giờ
Pull request đã merge (30 ngày)
23

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của plotly/dash

Tất cả issue của plotly/dash

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.