Skip to content

Commit

Permalink
[Tidy] Improve validation error message if CapturedCallable is dire…
Browse files Browse the repository at this point in the history
…ctly provided (#590)

Co-authored-by: Antony Milne <antony.milne@quantumblack.com>
  • Loading branch information
huong-li-nguyen and antonymilne authored Jul 19, 2024
1 parent 02781cc commit bf65329
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 87 deletions.
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

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

<!--
### 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))
-->
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
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_min_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_min_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_min_length = validator("components", allow_reuse=True, always=True)(validate_min_length)
_validate_layout = validator("layout", allow_reuse=True, always=True)(set_layout)

@root_validator(pre=True)
Expand Down
15 changes: 14 additions & 1 deletion vizro-core/src/vizro/models/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ def __init__(self, function, /, *args, **kwargs):
self.__bound_arguments.update(self.__bound_arguments[var_keyword_param])
del self.__bound_arguments[var_keyword_param]

# This is used to check that the mode of the capture decorator matches the inserted captured callable.
# Used in later validations of the captured callable.
self._mode = None
self._model_example = None

def __call__(self, *args, **kwargs):
"""Run the `function` with the initially bound arguments overridden by `**kwargs`.
Expand Down Expand Up @@ -283,7 +284,16 @@ class capture:

def __init__(self, mode: Literal["graph", "action", "table", "ag_grid", "figure"]):
"""Decorator to capture a function call."""
# mode and model_example 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 +317,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 +345,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 +358,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

0 comments on commit bf65329

Please sign in to comment.