diff --git a/vizro-core/src/vizro/_vizro.py b/vizro-core/src/vizro/_vizro.py index fd5eb9473..1a46d36a1 100644 --- a/vizro-core/src/vizro/_vizro.py +++ b/vizro-core/src/vizro/_vizro.py @@ -90,8 +90,9 @@ def build(self, dashboard: Dashboard): # everything else gets overridden in the post-fig creation layout.template update in Graph.__call__ and the # clientside theme selector callback. # Note this setting of global template isn't undone anywhere. If we really wanted to then we could try and - # put in some teardown code, but it would probably never be 100% reliable. Remember this template setting - # can't go in run() though since it's needed even in deployment. + # put in some teardown code, but it would probably never be 100% reliable. Vizro._reset can't do this well + # either because it's a staticmethod. Remember this template setting can't go in run() though since it's needed + # even in deployment. pio.templates.default = dashboard.theme # Note that model instantiation and pre_build are independent of Dash. diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py index d085a7fbc..184a68da7 100644 --- a/vizro-core/src/vizro/models/types.py +++ b/vizro-core/src/vizro/models/types.py @@ -252,8 +252,8 @@ def _check_type(cls, captured_callable: CapturedCallable, field: ModelField) -> @contextmanager -def _pio_templates_default(default: Literal["vizro_light", "vizro_dark"]): - """Sets pio.templates.default and then reverts it. +def _pio_templates_default(): + """Sets pio.templates.default to "vizro_dark" and then reverts it. This is to ensure that in a Jupyter notebook captured charts look the same as when they're in the dashboard. When the context manager exits the global theme is reverted just to keep things clean (e.g. if you really wanted to, @@ -270,7 +270,7 @@ def _pio_templates_default(default: Literal["vizro_light", "vizro_dark"]): # If the user has set pio.templates.default to a vizro theme already, no need to change it. if old_default not in ["vizro_dark", "vizro_light"]: template_changed = True - pio.templates.default = default + pio.templates.default = "vizro_dark" # Revert the template. This is done in a try/finally so that if the code wrapped inside the context manager (i.e. # plotting functions) raises an exception, pio.templates.default is still reverted. This is not very important @@ -374,7 +374,7 @@ def wrapped(*args, **kwargs) -> _DashboardReadyFigure: # We don't want to update the captured_callable in the same way, since it's only used inside the # dashboard, at which point the global pio.templates.default is always set anyway according to # the dashboard theme and then updated according to the theme selector. - with _pio_templates_default("vizro_dark") as default_template: + with _pio_templates_default() as default_template: fig = func(*args, **kwargs) # Update the fig.layout.template just to ensure absolute consistency with how the dashboard # works. diff --git a/vizro-core/tests/conftest.py b/vizro-core/tests/conftest.py index 2994ab8e2..0bac5ed1e 100644 --- a/vizro-core/tests/conftest.py +++ b/vizro-core/tests/conftest.py @@ -1,5 +1,11 @@ import pytest from vizro import Vizro +import plotly.io as pio + +# Setting pio.templates.default here is a bit of a hack. This is executed on Vizro.build, but some tests +# that don't run Vizro.build still expect it to be set. Ideally these tests would set the theme themselves or not +# pay attention to the template in the test if it's not relevant. +pio.templates.default = "vizro_dark" # Allow our custom assert functions in tests_utils/asserts.py to do introspection nicely still. # See https://pytest.org/en/7.4.x/how-to/assert.html#assertion-introspection-details diff --git a/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py b/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py index 5f59cf9c3..4243278ec 100644 --- a/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py +++ b/vizro-core/tests/unit/vizro/actions/test_on_page_load_action.py @@ -18,9 +18,7 @@ def target_scatter_filtered_continent_and_pop_parameter_y_and_x(request, gapmind ] scatter_params["y"] = y scatter_params["x"] = x - return px.scatter(data, template=dark if template == "vizro_dark" else light, **scatter_params).update_layout( - margin_t=24 - ) + return px.scatter(data, template=template, **scatter_params).update_layout(margin_t=24) @pytest.fixture @@ -32,7 +30,7 @@ def target_box_filtered_continent_and_pop_parameter_y_and_x(request, gapminder_2 ] box_params["y"] = y box_params["x"] = x - return px.box(data, template=dark if template == "vizro_dark" else light, **box_params).update_layout(margin_t=24) + return px.box(data, template=template, **box_params).update_layout(margin_t=24) @pytest.fixture diff --git a/vizro-core/tests/unit/vizro/conftest.py b/vizro-core/tests/unit/vizro/conftest.py index 67136ead9..e4cc384e3 100644 --- a/vizro-core/tests/unit/vizro/conftest.py +++ b/vizro-core/tests/unit/vizro/conftest.py @@ -7,6 +7,7 @@ from vizro import Vizro from vizro.figures import kpi_card from vizro.tables import dash_ag_grid, dash_data_table +import plotly.io as pio @pytest.fixture diff --git a/vizro-core/tests/unit/vizro/models/test_types.py b/vizro-core/tests/unit/vizro/models/test_types.py index baa977c7f..980c615e8 100644 --- a/vizro-core/tests/unit/vizro/models/test_types.py +++ b/vizro-core/tests/unit/vizro/models/test_types.py @@ -3,6 +3,8 @@ import plotly.graph_objects as go import pytest +import plotly.io as pio + try: from pydantic.v1 import Field, ValidationError except ImportError: # pragma: no cov @@ -281,3 +283,121 @@ class ModelWithInvalidModule(VizroBaseModel): ValueError, match="_target_=decorated_graph_function cannot be imported from invalid.module." ): ModelWithInvalidModule(function=config) + + +@capture("graph") +def decorated_graph_function(data_frame): + return go.Figure() + + +@capture("graph") +def decorated_graph_function_themed(data_frame, template): + fig = go.Figure() + fig.layout.template = template + return fig + + +@capture("graph") +def decorated_graph_function_crash(data_frame): + raise RuntimeError("Crash") + + +# @capture("graph") +# def decorated_graph_function(data_frame): +# return px. + +# @pytest.fixture(params=["vizro_dark", "vizro_light", "plotly"]) +# def template(request): +# return request.param + + +@pytest.fixture +def set_pio_default_template_dark(): + old_default = pio.templates.default + pio.templates.default = "vizro_dark" + yield + pio.templates.default = old_default + + +@pytest.fixture +def set_pio_default_template_light(): + old_default = pio.templates.default + pio.templates.default = "vizro_light" + yield + pio.templates.default = old_default + + +@pytest.fixture +def set_pio_default_template_plotly(): + old_default = pio.templates.default + pio.templates.default = "plotly" + yield + pio.templates.default = old_default + + +class TestGraphTemplate: + def test(self, set_pio_default_template_dark): + graph = decorated_graph_function(None) + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "vizro_dark" + + def test2(self, set_pio_default_template_light): + graph = decorated_graph_function(None) + assert graph.layout.template == pio.templates["vizro_light"] + assert pio.templates.default == "vizro_light" + + def test3(self, set_pio_default_template_plotly): + graph = decorated_graph_function(None) + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "plotly" + + def test4(self, set_pio_default_template_dark): + graph = decorated_graph_function_themed(None, "vizro_dark") + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "vizro_dark" + + def test5(self, set_pio_default_template_dark): + graph = decorated_graph_function_themed(None, "vizro_light") + assert graph.layout.template == pio.templates["vizro_light"] + assert pio.templates.default == "vizro_dark" + + def test6(self, set_pio_default_template_dark): + graph = decorated_graph_function_themed(None, "plotly") + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "vizro_dark" + + def test7(self, set_pio_default_template_light): + graph = decorated_graph_function_themed(None, "vizro_dark") + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "vizro_light" + + def test8(self, set_pio_default_template_light): + graph = decorated_graph_function_themed(None, "vizro_light") + assert graph.layout.template == pio.templates["vizro_light"] + assert pio.templates.default == "vizro_light" + + def test9(self, set_pio_default_template_light): + graph = decorated_graph_function_themed(None, "plotly") + assert graph.layout.template == pio.templates["vizro_light"] + assert pio.templates.default == "vizro_light" + + def test10(self, set_pio_default_template_plotly): + graph = decorated_graph_function_themed(None, "vizro_dark") + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "plotly" + + def test11(self, set_pio_default_template_plotly): + graph = decorated_graph_function_themed(None, "vizro_light") + assert graph.layout.template == pio.templates["vizro_light"] + assert pio.templates.default == "plotly" + + def test12(self, set_pio_default_template_plotly): + graph = decorated_graph_function_themed(None, "plotly") + assert graph.layout.template == pio.templates["vizro_dark"] + assert pio.templates.default == "plotly" + + # Could parametrise this 3 times too if can be bothered... + def test13(self, set_pio_default_template_plotly): + with pytest.raises(RuntimeError, match="Crash"): + decorated_graph_function_crash(None) + assert pio.templates.default == "plotly"