diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 12b3bd6a56..1a48a305ed 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -974,6 +974,7 @@ jobs: build-windows: name: Build on Windows runs-on: windows-latest + needs: build timeout-minutes: 30 steps: @@ -1012,6 +1013,15 @@ jobs: npm ci npm run build + - name: Download built component folders + uses: actions/download-artifact@v4 + with: + name: dash-components + path: ${{ github.workspace }}/dash + + - name: Run Windows-specific unit tests + run: python -m pytest tests/unit/test_resources.py + dcc-lint: name: DCC Lint Tests (Python ${{ matrix.python-version }}) needs: [build, changes_filter] diff --git a/CHANGELOG.md b/CHANGELOG.md index f31991146e..d7d555d30b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3881](https://github.com/plotly/dash/pull/3881) Fix components rendered as props (eg. `dcc.Dropdown` option labels, `dcc.Tab` labels) crashing or failing to update when the host subtree was replaced by a callback; out-of-tree `ExternalWrapper` components now re-insert themselves and update in place. - [#3929](https://github.com/plotly/dash/issues/3929) Fix components that set their own initial state on mount (eg. `dash-bootstrap-components` `Tabs`) not applying it on first render, because descendant layout hashes were reset on the first fresh render (regression introduced in 4.2.0 by [#3570](https://github.com/plotly/dash/pull/3570)). - [#3948](https://github.com/plotly/dash/issues/3948) Fix page getting progressively slower as callbacks append children +- [#3002](https://github.com/plotly/dash/issues/3002) Fix page loads failing with a 500 error on Windows when Dash is imported from an extended-length (`\\?\`) path, as in JupyterLab Desktop. Package resource paths are now joined with the OS separator instead of `/`. ## [4.4.1] - 2026-07-21 diff --git a/dash/dash.py b/dash/dash.py index ed1ed5ebc8..cb0d849938 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1261,7 +1261,7 @@ def _get_worker_url( # Build fingerprinted URL (same pattern as _collect_and_register_resources) module_path = os.path.join( os.path.dirname(sys.modules[namespace].__file__), # type: ignore - relative_path, + *relative_path.split("/"), ) # Use a fallback if the file doesn't exist yet (during development) @@ -1302,9 +1302,11 @@ def _relative_url_path(relative_package_path="", namespace=""): else: version = importlib.import_module(namespace).__version__ + # Split on "/" so the file path uses the OS separator: Windows + # extended-length paths (\\?\ prefix) reject forward slashes. module_path = os.path.join( # type: ignore[reportCallIssue] os.path.dirname(sys.modules[namespace].__file__), # type: ignore[reportCallIssue] - relative_package_path, + *relative_package_path.split("/"), ) modified = int(os.stat(module_path).st_mtime) @@ -1489,7 +1491,7 @@ def index(self, *_args, **_kwargs): if self._favicon: favicon_mod_time = os.path.getmtime( - os.path.join(self.config.assets_folder, self._favicon) + os.path.join(self.config.assets_folder, *self._favicon.split("/")) ) favicon_url = f"{self.get_asset_url(self._favicon)}?m={favicon_mod_time}" else: diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 413514de18..cbbf52964b 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,4 +1,8 @@ +import os +import sys + import mock +import pytest import dash from dash import dcc, html # noqa: F401 @@ -253,3 +257,28 @@ def test_multiple_external_urls_with_attributes(): {"src": "https://example.com/script1.js", "type": "module"}, {"src": "https://example.com/script2.js", "type": "module"}, ] + + +@pytest.mark.skipif( + sys.platform != "win32", reason="extended-length paths only exist on Windows" +) +def test_index_with_windows_extended_length_paths(tmp_path, monkeypatch): + """Dash and the assets folder under extended-length paths (JupyterLab Desktop). + + Windows uses these paths verbatim, so a "/" inside one is an invalid name. + """ + monkeypatch.setattr(dash, "__file__", "\\\\?\\" + os.path.abspath(dash.__file__)) + icons = tmp_path / "assets" / "icons" + icons.mkdir(parents=True) + (icons / "favicon.ico").write_bytes(b"") + + app = dash.Dash(__name__, assets_folder="\\\\?\\" + str(tmp_path / "assets")) + app.layout = html.Div() + + response = app.server.test_client().get("/") + + assert response.status_code == 200 + body = response.get_data(as_text=True) + assert "/_dash-component-suites/dash/deps/polyfill@" in body + assert "dash-stream-worker.v" in body + assert "/assets/icons/favicon.ico?m=" in body