Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tidy] Improve validation error message if CapturedCallable is directly provided #590

Merged
merged 14 commits into from
Jul 19, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
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
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

- Improve validation error message if `CapturedCallable` is directly provided. ([#590](https://github.com/mckinsey/vizro/pull/590))

<!--
### 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))

-->
69 changes: 18 additions & 51 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,34 @@
"""Example app to show all features of Vizro."""

# check out https://github.com/mckinsey/vizro for more info about Vizro
# and checkout https://vizro.readthedocs.io/en/stable/ for documentation

import pandas as pd
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.figures import kpi_card

df = px.data.iris()

page = vm.Page(
title="Vizro on PyCafe",
layout=vm.Layout(
grid=[[0, 0, 0, 1, 2, 3], [4, 4, 4, 4, 4, 4], [4, 4, 4, 4, 4, 4], [5, 5, 5, 5, 5, 5], [5, 5, 5, 5, 5, 5]],
row_min_height="175px",
),
components=[
vm.Card(
text="""
### What is Vizro?
gapminder = px.data.gapminder()

Vizro is a toolkit for creating modular data visualization applications.
"""
),
vm.Card(
text="""
### Github

Checkout Vizro's github page.
""",
href="https://github.com/mckinsey/vizro",
),
vm.Card(
text="""
### Docs
# data from the demo app
df_kpi = pd.DataFrame(
{
"Actual": [100, 200, 700],
"Reference": [100, 300, 500],
"Category": ["A", "B", "C"],
}
)

Visit the documentation for codes examples, tutorials and API reference.
""",
href="https://vizro.readthedocs.io/",
),
vm.Card(
text="""
### Nav Link

Click this for page 2.
""",
href="/page2",
),
vm.Graph(id="scatter_chart", figure=px.scatter(df, x="sepal_length", y="petal_width", color="species")),
vm.Graph(id="hist_chart", figure=px.histogram(df, x="sepal_width", color="species")),
],
controls=[
vm.Filter(column="species", selector=vm.Dropdown(value=["ALL"])),
vm.Filter(column="petal_length"),
vm.Filter(column="sepal_width"),
home = vm.Page(
title="Page Title",
components=[
# kpi_card(data_frame=df_kpi, value_column="Actual", title="KPI with value"),
vm.Figure(figure=kpi_card(data_frame=df_kpi, value_column="Actual", title="KPI with value")),
],
)

page2 = vm.Page(
title="Page2", components=[vm.Graph(id="hist_chart2", figure=px.histogram(df, x="sepal_width", color="species"))]
)

dashboard = vm.Dashboard(pages=[page, page2])
dashboard = vm.Dashboard(pages=[home])


if __name__ == "__main__":
Vizro().build(dashboard).run()
12 changes: 12 additions & 0 deletions vizro-core/src/vizro/figures/kpi_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def kpi_card( # noqa: PLR0913
Returns:
A Dash Bootstrap Components card (`dbc.Card`) containing the formatted KPI value.

Examples:
Wrap inside `vm.Figure` to use as a component inside `vm.Page` or `vm.Container`.
>>> import vizro.models as vm
>>> from vizro.figures import kpi_card
>>> vm.Page(title="Page", components=[vm.Figure(figure=kpi_card(...))])

"""
title = title or f"{agg_func} {value_column}".title()
value = data_frame[value_column].agg(agg_func)
Expand Down Expand Up @@ -119,6 +125,12 @@ def kpi_card_reference( # noqa: PLR0913
Returns:
A Dash Bootstrap Components card (`dbc.Card`) containing the formatted KPI value and reference.

Examples:
Wrap inside `vm.Figure` to use as a component inside `vm.Page` or `vm.Container`.
>>> import vizro.models as vm
>>> from vizro.figures import kpi_card_reference
>>> vm.Page(title="Page", components=[vm.Figure(figure=kpi_card_reference(...))])

"""
title = title or f"{agg_func} {value_column}".title()
value, reference = data_frame[[value_column, reference_column]].agg(agg_func)
Expand Down
7 changes: 5 additions & 2 deletions vizro-core/src/vizro/models/_components/_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from vizro.models import VizroBaseModel
from vizro.models._components.form import Checklist, Dropdown, RadioItems, RangeSlider, Slider
from vizro.models._layout import set_layout
from vizro.models._models_utils import _log_call, _validate_min_length
from vizro.models._models_utils import _log_call, check_captured_callable, validate_min_length
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
from vizro.models.types import _FormComponentType

if TYPE_CHECKING:
Expand All @@ -34,7 +34,10 @@ class Form(VizroBaseModel):
layout: Layout = None # type: ignore[assignment]

# Re-used validators
_validate_components = validator("components", allow_reuse=True, always=True)(_validate_min_length)
_check_captured_callable = validator("components", allow_reuse=True, each_item=True, pre=True)(
check_captured_callable
)
_validate_components_length = validator("components", allow_reuse=True, always=True)(validate_min_length)
_validate_layout = validator("layout", allow_reuse=True, always=True)(set_layout)

@_log_call
Expand Down
7 changes: 5 additions & 2 deletions vizro-core/src/vizro/models/_components/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from vizro.models import VizroBaseModel
from vizro.models._layout import set_layout
from vizro.models._models_utils import _log_call, _validate_min_length
from vizro.models._models_utils import _log_call, check_captured_callable, validate_min_length
from vizro.models.types import ComponentType

if TYPE_CHECKING:
Expand All @@ -36,7 +36,10 @@ class Container(VizroBaseModel):
layout: Layout = None # type: ignore[assignment]

# Re-used validators
_validate_components = validator("components", allow_reuse=True, always=True)(_validate_min_length)
_check_captured_callable = validator("components", allow_reuse=True, each_item=True, pre=True)(
check_captured_callable
)
_validate_components_length = validator("components", allow_reuse=True, always=True)(validate_min_length)
_validate_layout = validator("layout", allow_reuse=True, always=True)(set_layout)

@_log_call
Expand Down
4 changes: 2 additions & 2 deletions vizro-core/src/vizro/models/_components/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pydantic import validator

from vizro.models import VizroBaseModel
from vizro.models._models_utils import _log_call, _validate_min_length
from vizro.models._models_utils import _log_call, validate_min_length

if TYPE_CHECKING:
from vizro.models._components import Container
Expand All @@ -29,7 +29,7 @@ class Tabs(VizroBaseModel):
type: Literal["tabs"] = "tabs"
tabs: List[Container]

_validate_tabs = validator("tabs", allow_reuse=True, always=True)(_validate_min_length)
_validate_tabs = validator("tabs", allow_reuse=True, always=True)(validate_min_length)

@_log_call
def build(self):
Expand Down
22 changes: 19 additions & 3 deletions vizro-core/src/vizro/models/_models_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from functools import wraps

from vizro.models.types import CapturedCallable, _SupportsCapturedCallable

logger = logging.getLogger(__name__)


Expand All @@ -16,7 +18,21 @@ def _wrapper(self, *args, **kwargs):


# Validators for reuse
def _validate_min_length(cls, field):
if not field:
def validate_min_length(cls, value):
if not value:
raise ValueError("Ensure this value has at least 1 item.")
return field
return value


def check_captured_callable(cls, value):
if isinstance(value, CapturedCallable):
captured_callable = value
elif isinstance(value, _SupportsCapturedCallable):
captured_callable = value._captured_callable
else:
return value

raise ValueError(
f"A callable of mode `{captured_callable._mode}` has been provided. Please wrap it inside "
f"`{captured_callable._model_example}`."
)
7 changes: 5 additions & 2 deletions vizro-core/src/vizro/models/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from vizro.models import Action, Layout, VizroBaseModel
from vizro.models._action._actions_chain import ActionsChain, Trigger
from vizro.models._layout import set_layout
from vizro.models._models_utils import _log_call, _validate_min_length
from vizro.models._models_utils import _log_call, check_captured_callable, validate_min_length

from .types import ComponentType, ControlType

Expand Down Expand Up @@ -52,7 +52,10 @@ class Page(VizroBaseModel):
actions: List[ActionsChain] = []

# Re-used validators
_validate_components = validator("components", allow_reuse=True, always=True)(_validate_min_length)
_check_captured_callable = validator("components", allow_reuse=True, each_item=True, pre=True)(
check_captured_callable
)
_validate_components_length = validator("components", allow_reuse=True, always=True)(validate_min_length)
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
_validate_layout = validator("layout", allow_reuse=True, always=True)(set_layout)

@root_validator(pre=True)
Expand Down
12 changes: 12 additions & 0 deletions vizro-core/src/vizro/models/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,16 @@ class capture:

def __init__(self, mode: Literal["graph", "action", "table", "ag_grid", "figure"]):
"""Decorator to capture a function call."""
# mode and model are used in later validations of the captured callable.
self._mode = mode
model_examples = {
"graph": "vm.Graph(figure=...)",
"action": "vm.Action(function=...)",
"table": "vm.Table(figure=...)",
"ag_grid": "vm.AgGrid(figure=...)",
"figure": "vm.Figure(figure=...)",
}
self._model_example = model_examples[mode]

def __call__(self, func, /):
"""Produces a CapturedCallable or _DashboardReadyFigure.
Expand All @@ -307,6 +316,7 @@ def wrapped(*args, **kwargs) -> _DashboardReadyFigure:
# positional or keyword, this is much more robust than trying to get it out of arg or kwargs ourselves.
captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs)
captured_callable._mode = self._mode
captured_callable._model_example = self._model_example

try:
captured_callable["data_frame"]
Expand Down Expand Up @@ -334,6 +344,7 @@ def wrapped(*args, **kwargs):
# Note this is basically the same as partial(func, *args, **kwargs)
captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs)
captured_callable._mode = self._mode
captured_callable._model_example = self._model_example
return captured_callable

return wrapped
Expand All @@ -346,6 +357,7 @@ def wrapped(*args, **kwargs):

captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs)
captured_callable._mode = self._mode
captured_callable._model_example = self._model_example

try:
captured_callable["data_frame"]
Expand Down
10 changes: 9 additions & 1 deletion vizro-core/src/vizro/tables/_dash_ag_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@

@capture("ag_grid")
def dash_ag_grid(data_frame: pd.DataFrame, **kwargs) -> dag.AgGrid:
"""Implementation of `dash_ag_grid.AgGrid` with sensible defaults to be used in [`AgGrid`][vizro.models.AgGrid]."""
"""Implementation of `dash_ag_grid.AgGrid` with sensible defaults to be used in [`AgGrid`][vizro.models.AgGrid].

Examples
Wrap inside `vm.AgGrid` to use as a component inside `vm.Page` or `vm.Container`.
>>> import vizro.models as vm
>>> from vizro.tables import dash_ag_grid
>>> vm.Page(title="Page", components=[vm.AgGrid(figure=dash_ag_grid(...))])

"""
defaults = {
"className": "ag-theme-quartz-dark ag-theme-vizro",
"columnDefs": [{"field": col} for col in data_frame.columns],
Expand Down
10 changes: 9 additions & 1 deletion vizro-core/src/vizro/tables/_dash_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@

@capture("table")
def dash_data_table(data_frame: pd.DataFrame, **kwargs) -> dash_table.DataTable:
"""Standard `dash_table.DataTable` with sensible defaults to be used in [`Table`][vizro.models.Table]."""
"""Standard `dash_table.DataTable` with sensible defaults to be used in [`Table`][vizro.models.Table].

Examples
Wrap inside `vm.Table` to use as a component inside `vm.Page` or `vm.Container`.
>>> import vizro.models as vm
>>> from vizro.table import dash_data_table
>>> vm.Page(title="Page", components=[vm.Table(figure=dash_data_table(...))])

"""
defaults = {
"columns": [{"name": col, "id": col} for col in data_frame.columns],
"style_as_list_view": True,
Expand Down
11 changes: 11 additions & 0 deletions vizro-core/tests/unit/vizro/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.figures import kpi_card
from vizro.tables import dash_ag_grid, dash_data_table


Expand Down Expand Up @@ -61,6 +62,16 @@ def standard_go_chart(gapminder):
return go.Figure(data=go.Scatter(x=gapminder["gdpPercap"], y=gapminder["lifeExp"], mode="markers"))


@pytest.fixture
def standard_kpi_card(gapminder):
return kpi_card(
data_frame=gapminder,
value_column="lifeExp",
agg_func="mean",
value_format="{value:.3f}",
)


@pytest.fixture
def chart_with_temporal_data(stocks):
return go.Figure(data=go.Scatter(x=stocks["Date"], y=stocks["AAPL.High"], mode="markers"))
Expand Down
Loading
Loading