Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- [#3986](https://github.com/plotly/dash/pull/3986) Adjust `_run_before_hooks` in the `fastapi` backend to honor a response returned by a `before_request` function, matching the `flask` backend's behavior.

### Fixed
- [#4078](https://github.com/plotly/dash/pull/4078) Components are now always truthy, so `if component:` and `component or default` no longer treat a component with no children (or no `children` prop, like `DataTable`) as false. With `use_pages=True`, an empty layout such as `html.Div()` still counts as unset and is replaced by `page_container`. Fixes [#2906](https://github.com/plotly/dash/issues/2906).
- [#4075](https://github.com/plotly/dash/pull/4075) `fastapi` backend: run `before_request` hooks on every route of the Dash app (assets, hook routes, `api_endpoint`, pages, when mounted) and send a non-None hook result as the response, as on Flask. `fastapi` and `quart` also run them on the WebSocket handshake.
- [#4075](https://github.com/plotly/dash/pull/4075) Background callbacks without `cache_by` no longer share a result between requests with equal inputs (unread Diskcache results now expire after a day, or after `expire` when set, even with `cache_by`), and callbacks with the same source no longer share a cache key. `build_cache_key` takes a new `callback_key` argument.
- [#4075](https://github.com/plotly/dash/pull/4075) `dcc.Location(refresh=True).href`, `html.Area.href` and `html.Base.href` now block `javascript:` URLs like `html.A`.
Expand Down
15 changes: 13 additions & 2 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
Output,
State,
)
from .development.base_component import ComponentRegistry
from .development.base_component import Component, ComponentRegistry
from .exceptions import (
ProxyError,
DuplicateCallback,
Expand Down Expand Up @@ -247,6 +247,17 @@ async def execute_async_function(func, *args, **kwargs):
return func(*args, **kwargs)


def _layout_is_unset(layout):
"""True when ``use_pages`` should fill in ``page_container`` for ``layout``.

Components are always truthy, so an empty component such as
``html.Div()`` is checked by its length to keep it counting as unset.
"""
if isinstance(layout, Component):
return len(layout) == 0
return not layout


# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
class Dash(ObsoleteChecker):
Expand Down Expand Up @@ -1891,7 +1902,7 @@ def _setup_server(self):
if self.config.include_assets_files:
self._walk_assets_directory()

if not self.layout and self.use_pages:
if self.use_pages and _layout_is_unset(self.layout):
self.layout = page_container

_validate.validate_layout(self.layout, self._layout_value())
Expand Down
5 changes: 5 additions & 0 deletions dash/development/base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,11 @@ def __iter__(self):
for t in self._traverse_ids():
yield t.id # type: ignore[reportAttributeAccessIssue]

def __bool__(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also changes the result of if not self.layout in dash.py:1894. An empty layout with use_pages=True is no longer replaced with page_container, and every request then errors. Can you update that check so an empty component still counts as unset there?

# Without this, truthiness falls back to __len__, so a component
# with no children (or no children prop, like DataTable) is falsy.
return True

def __len__(self):
"""Return the number of items in the tree."""
# TODO - Should we return the number of items that have IDs
Expand Down
18 changes: 18 additions & 0 deletions tests/integration/multi_page/test_pages_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,21 @@ def test_pala007_app_title_discription(dash_duo, clear_pages_state):
assert dash.page_registry["page1"]["title"] == "Page 1 Title"
assert dash.page_registry["home"]["description"] == "App Description"
assert dash.page_registry["page1"]["description"] == "Page 1 Description"


def test_pala008_empty_layout_uses_page_container(dash_duo, clear_pages_state):
# An empty placeholder layout still counts as unset with use_pages, so
# Dash fills in page_container and the registered pages render.
# The pages folder has callbacks on ids that only exist in other tests'
# layouts, so callback exceptions are suppressed here.
app = Dash(__name__, use_pages=True, suppress_callback_exceptions=True)
dash.register_page(
"empty_layout_home",
layout=html.Div("Home for empty layout", id="contents"),
path="/",
)
app.layout = html.Div()

dash_duo.start_server(app)
dash_duo.wait_for_text_to_equal("#contents", "Home for empty layout")
assert dash_duo.get_logs() == []
10 changes: 9 additions & 1 deletion tests/unit/development/test_base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dash import __version__, Dash
from dash import html
from dash.development.base_component import Component
from dash import dcc, Input, Output
from dash import dcc, dash_table, Input, Output

Component._prop_names = ("id", "a", "children", "style")
Component._type = "TestComponent"
Expand Down Expand Up @@ -539,3 +539,11 @@ def update2(v):
def test_debc030_invalid_children_args():
with pytest.raises(TypeError):
dcc.Input(children="invalid children")


def test_debc031_bool_is_always_true():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers bool() itself but not the case that breaks. Can you add a dash_duo test under tests/integration/multi_page/: use_pages=True, app.layout = html.Div(), check that the home page renders? It fails with the current change, so it guards the fix.

assert bool(Component())
assert bool(Component(children=[]))
assert bool(html.Div())
assert bool(dash_table.DataTable(data=[{"a": 1}]))
assert (html.Div() or None) is not None