Skip to content

Commit

Permalink
Merge branch 'main' into docs/improve_custom_chart_docs
Browse files Browse the repository at this point in the history
  • Loading branch information
maxschulz-COL authored Dec 20, 2023
2 parents 9bb92d2 + a30b50b commit 5fcd680
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Added
- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Changed
- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Fixed
- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
8 changes: 1 addition & 7 deletions vizro-core/src/vizro/models/_components/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@ class Button(VizroBaseModel):
@_log_call
def build(self):
return html.Div(
[
dbc.Button(
id=self.id,
children=self.text,
className="button_primary",
),
],
dbc.Button(id=self.id, children=self.text, className="button_primary"),
className="button_container",
id=f"{self.id}_outer",
)
2 changes: 1 addition & 1 deletion vizro-core/tests/integration/test_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ def label_cases(cases, label):
def test_navigation_build(dashboard_result, dashboard_expected):
result = dashboard_result.navigation.build()
expected = dashboard_expected.navigation.build()
assert_component_equal(result, expected)
assert_component_equal(result, expected, keys_to_strip={"id"})
41 changes: 27 additions & 14 deletions vizro-core/tests/tests_utils/asserts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,41 @@
import dash.development
import plotly

STRIP_ALL = object()

def strip_keys(object, keys):
"""Strips all entries with key "id" from a dictionary, regardless of how deeply it's nested.

This makes it easy to compare dictionaries generated from Dash components we've created that contain random IDs.
"""
def _strip_keys(object, keys):
"""Strips from a JSON object all entries where the key is in keys, regardless of how deeply it's nested."""
if isinstance(object, dict):
object = {key: strip_keys(value, keys) for key, value in object.items() if key not in keys}
object = {key: _strip_keys(value, keys) for key, value in object.items() if key not in keys}
elif isinstance(object, list):
object = [strip_keys(item, keys) for item in object]
object = [_strip_keys(item, keys) for item in object]
return object


def component_to_dict(component: dash.development.base_component.Component) -> dict:
def _component_to_dict(component: dash.development.base_component.Component) -> dict:
"""Prepares a Dash component for comparison by conversion to JSON object."""
return json.loads(json.dumps(component, cls=plotly.utils.PlotlyJSONEncoder))


# TODO: implement some sort of depth limit to comparison so can use in high level tests, roll out more widely across
# tests
def assert_component_equal(left, right, keys_to_strip=None):
# Note we check for None explicitly because {} is a valid value for keys_to_strip.
keys_to_strip = keys_to_strip if keys_to_strip is not None else {"id", "class_name", "className"}
left = strip_keys(component_to_dict(left), keys_to_strip)
right = strip_keys(component_to_dict(right), keys_to_strip)
def assert_component_equal(left, right, *, keys_to_strip=None):
"""Checks that the left and right Dash components are equal, ignoring keys_to_strip.
If keys_to_strip is set to STRIP_ALL then only the type and namespace of component
will be compared, similar to doing isinstance.
Examples:
>>> from dash import html
>>> assert_component_equal(html.Div(), html.Div())
>>> assert_component_equal(html.Div(id="a"), html.Div(), keys_to_strip={"id"})
>>> assert_component_equal(html.Div([html.P(), html.P()], id="a"), html.Div(id="a"), keys_to_strip={"children"})
>>> assert_component_equal(html.Div(html.P(), className="blah", id="a"), html.Div(), keys_to_strip=STRIP_ALL)
"""
keys_to_strip = keys_to_strip or {}
if keys_to_strip is STRIP_ALL:
# Remove all properties from the component dictionary, leaving just "type" and "namespace" behind.
keys_to_strip = {"props"}

left = _strip_keys(_component_to_dict(left), keys_to_strip)
right = _strip_keys(_component_to_dict(right), keys_to_strip)
assert left == right
48 changes: 48 additions & 0 deletions vizro-core/tests/tests_utils/demo_asserts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Demo to show how to use asserts. These are not real tests that are run as part of testing, just a teaching aid."""
from typing import List

from asserts import STRIP_ALL, assert_component_equal
from dash import html

from vizro.models import VizroBaseModel


class X(VizroBaseModel):
# Low-level contents model.
text: str

def build(self):
return html.Div(
[html.H1("Heading"), html.P(self.text, id=self.id), html.Hr(), html.H2("Something")], className="inner"
)


class Y(VizroBaseModel):
# Higher-level container model.
children: List[X]

def build(self):
return html.Div([child.build() for child in self.children], id=self.id, className="container")


def test_X_build():
# Test for low-level contents: compare the whole component tree.
# Sometimes setting keys_to_strip={"className"} is useful here.
result = X(id="x", text="Hello world").build()
expected = html.Div(
[html.H1("Heading"), html.P("Hello world", id="x"), html.Hr(), html.H2("Something")], className="inner"
)
assert_component_equal(result, expected)


def test_Y_build():
# Test for higher-level container.
many_x = [X(text="Hello world") for _ in range(4)]
result = Y(id="y", children=many_x).build()
# We don't want to compare the whole component tree here. Instead compare the bit that's specifically in Y and
# ignore the children:
assert_component_equal(result, html.Div(id="y", className="container"), keys_to_strip={"children"})
# And also compare the "interface" between X and Y. Use STRIP_ALL to not look at any properties of the html.Div.
# This is basically the same as doing:
# assert all(isinstance(child, html.Div) for child in result.children) and len(result.children) == 4
assert_component_equal(result.children, [html.Div()] * 4, keys_to_strip=STRIP_ALL)
32 changes: 9 additions & 23 deletions vizro-core/tests/unit/vizro/models/_components/test_button.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
"""Unit tests for vizro.models.Button."""
import json

import dash_bootstrap_components as dbc
import plotly
import pytest
from asserts import assert_component_equal
from dash import html

import vizro.models as vm
from vizro.actions import export_data


@pytest.fixture
def expected_button():
return html.Div(
[
dbc.Button(
id="button_id",
children="Click me!",
className="button_primary",
),
],
className="button_container",
id="button_id_outer",
)


class TestButtonInstantiation:
"""Tests model instantiation and the validators run at that time."""

Expand Down Expand Up @@ -53,9 +37,11 @@ def test_set_action_via_validator(self):


class TestBuildMethod:
def test_button_build(self, expected_button):
button = vm.Button(id="button_id", text="Click me!").build()
result = json.loads(json.dumps(button, cls=plotly.utils.PlotlyJSONEncoder))
expected = json.loads(json.dumps(expected_button, cls=plotly.utils.PlotlyJSONEncoder))

assert result == expected
def test_button_build(self):
button = vm.Button(id="button_id", text="My text").build()
expected = html.Div(
dbc.Button(id="button_id", children="My text", className="button_primary"),
className="button_container",
id="button_id_outer",
)
assert_component_equal(button, expected)
10 changes: 6 additions & 4 deletions vizro-core/tests/unit/vizro/models/_navigation/test_accordion.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_invalid_page(self, pages):
class TestAccordionBuild:
"""Tests accordion build method."""

common_args = {"always_open": True, "persistence": True, "persistence_type": "session"}
common_args = {"always_open": True, "persistence": True, "persistence_type": "session", "id": "accordion"}

test_cases = [
(
Expand Down Expand Up @@ -113,9 +113,11 @@ class TestAccordionBuild:
@pytest.mark.parametrize("pages, expected", test_cases)
def test_accordion(self, pages, expected):
accordion = vm.Accordion(id="accordion", pages=pages).build(active_page_id="Page 1")
assert_component_equal(accordion, html.Div(id="nav_panel_outer"), keys_to_strip={"children", "className"})
assert_component_equal(accordion["accordion"], expected)
assert_component_equal(
accordion, html.Div(id="nav_panel_outer", className="nav_panel"), keys_to_strip={"children"}
)
assert_component_equal(accordion["accordion"], expected, keys_to_strip={"class_name", "className"})

def test_accordion_one_page(self):
accordion = vm.Accordion(pages={"Group": ["Page 1"]}).build(active_page_id="Page 1")
assert_component_equal(accordion, html.Div(hidden=True, id="nav_panel_outer"), keys_to_strip={})
assert_component_equal(accordion, html.Div(hidden=True, id="nav_panel_outer"))
22 changes: 10 additions & 12 deletions vizro-core/tests/unit/vizro/models/_navigation/test_nav_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
import pytest
from asserts import assert_component_equal
from asserts import STRIP_ALL, assert_component_equal
from dash import html

try:
Expand Down Expand Up @@ -93,11 +93,13 @@ def test_nav_bar_active_pages_as_dict(self, pages_as_dict):
)
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button, keys_to_strip={"id", "className"})
assert_component_equal(
built_nav_bar["nav_panel_outer"], html.Div(id="nav_panel_outer"), keys_to_strip={"children", "className"}
built_nav_bar["nav_panel_outer"],
html.Div(id="nav_panel_outer", className="nav_panel"),
keys_to_strip={"children"},
)
assert all(isinstance(child, dbc.Accordion) for child in built_nav_bar["nav_panel_outer"].children)
assert_component_equal(built_nav_bar["nav_panel_outer"].children, [dbc.Accordion()], keys_to_strip=STRIP_ALL)

def test_nav_bar_active_pages_as_list(self, pages_as_list):
nav_bar = vm.NavBar(pages=pages_as_list)
Expand All @@ -117,11 +119,10 @@ def test_nav_bar_active_pages_as_list(self, pages_as_list):
),
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons, keys_to_strip={"id", "className"})
assert_component_equal(
built_nav_bar["nav_panel_outer"],
html.Div(id="nav_panel_outer", hidden=True),
keys_to_strip={"children", "className"},
)

def test_nav_bar_not_active_pages_as_dict(self, pages_as_dict):
Expand All @@ -137,10 +138,8 @@ def test_nav_bar_not_active_pages_as_dict(self, pages_as_dict):
)
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button)
assert_component_equal(
built_nav_bar["nav_panel_outer"], html.Div(hidden=True, id="nav_panel_outer"), keys_to_strip={}
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button, keys_to_strip={"id", "className"})
assert_component_equal(built_nav_bar["nav_panel_outer"], html.Div(hidden=True, id="nav_panel_outer"))

def test_nav_bar_not_active_pages_as_list(self, pages_as_list):
nav_bar = vm.NavBar(pages=pages_as_list)
Expand All @@ -160,9 +159,8 @@ def test_nav_bar_not_active_pages_as_list(self, pages_as_list):
),
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons, keys_to_strip={"id", "className"})
assert_component_equal(
built_nav_bar["nav_panel_outer"],
html.Div(id="nav_panel_outer", hidden=True),
keys_to_strip={},
)
24 changes: 20 additions & 4 deletions vizro-core/tests/unit/vizro/models/_navigation/test_nav_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
import pytest
from asserts import assert_component_equal
from asserts import STRIP_ALL, assert_component_equal
from dash import html

try:
Expand Down Expand Up @@ -86,22 +86,38 @@ def test_nav_link_active(self, pages, request):
nav_link.pre_build()
built_nav_link = nav_link.build(active_page_id="Page 1")
expected_button = dbc.Button(
children=[dmc.Tooltip(label="Label", children=[html.Span("icon")], **self.common_args)],
children=[
dmc.Tooltip(
label="Label",
children=[html.Span("icon", className="material-symbols-outlined")],
**self.common_args,
)
],
active=True,
href="/",
className="icon-button",
id="nav_link",
)
assert_component_equal(built_nav_link["nav_link"], expected_button)
assert all(isinstance(child, dbc.Accordion) for child in built_nav_link["nav_panel_outer"].children)
assert_component_equal(built_nav_link["nav_panel_outer"].children, [dbc.Accordion()], keys_to_strip=STRIP_ALL)

def test_nav_link_not_active(self, pages, request):
pages = request.getfixturevalue(pages)
nav_link = vm.NavLink(id="nav_link", label="Label", icon="icon", pages=pages)
nav_link.pre_build()
built_nav_link = nav_link.build(active_page_id="Page 3")
expected_button = dbc.Button(
children=[dmc.Tooltip(label="Label", children=[html.Span("icon")], **self.common_args)],
children=[
dmc.Tooltip(
label="Label",
children=[html.Span("icon", className="material-symbols-outlined")],
**self.common_args,
)
],
active=False,
href="/",
className="icon-button",
id="nav_link",
)
assert_component_equal(built_nav_link["nav_link"], expected_button)
assert "nav_panel_outer" not in built_nav_link
Loading

0 comments on commit 5fcd680

Please sign in to comment.