diff --git a/tests/test_example_container.py b/tests/test_example_plugin.py similarity index 73% rename from tests/test_example_container.py rename to tests/test_example_plugin.py index a36bab10..d6eec85b 100644 --- a/tests/test_example_container.py +++ b/tests/test_example_plugin.py @@ -13,12 +13,12 @@ def test_example_plugin(dash_duo): page = _example_plugin.ExamplePlugin(app, title) app.layout = page.layout dash_duo.start_server(app) - btn = dash_duo.find_element(f"#{page.button_id}") + btn = dash_duo.find_element("#" + page.uuid("submit-button")) assert btn.text == "Submit" - text = dash_duo.find_element(f"#{page.div_id}") + text = dash_duo.find_element("#" + page.uuid("output-state")) assert text.text == "Button has been pressed 0 times." btn.click() dash_duo.wait_for_contains_text( - f"#{page.div_id}", "Button has been pressed 1 times", timeout=2 + "#" + page.uuid("output-state"), "Button has been pressed 1 times", timeout=2 ) assert text.text == "Button has been pressed 1 times." diff --git a/tests/test_table_plotter.py b/tests/test_table_plotter.py index ad35a072..aec2213f 100644 --- a/tests/test_table_plotter.py +++ b/tests/test_table_plotter.py @@ -25,19 +25,13 @@ def test_table_plotter(dash_duo): # Check that filter is not active assert not page.use_filter - # Checking that the selectors are not hidden - selector_row = dash_duo.find_element(f"#{page.selector_row}") - assert selector_row.get_attribute("style") == "" - # Checking that the correct plot type is initialized - plot_dd = dash_duo.find_element(f"#{page.plot_option_id}-plottype") + plot_dd = dash_duo.find_element("#" + page.uuid("plottype")) assert plot_dd.text == "scatter" # Checking that only the relevant options are shown for plot_option in page.plot_args.keys(): - plot_option_dd = dash_duo.find_element( - f"#{page.plot_option_id}-div-{plot_option}" - ) + plot_option_dd = dash_duo.find_element("#" + page.uuid(f"div-{plot_option}")) if plot_option not in page.plots["scatter"]: assert plot_option_dd.get_attribute("style") == "display: none;" else: @@ -45,7 +39,7 @@ def test_table_plotter(dash_duo): # Checking that options are initialized correctly for option in ["x", "y"]: - plot_option_dd = dash_duo.find_element(f"#{page.plot_option_id}-{option}") + plot_option_dd = dash_duo.find_element("#" + page.uuid(f"dropdown-{option}")) assert plot_option_dd.text == "Well" @@ -69,18 +63,16 @@ def test_table_plotter_filter(dash_duo): assert page.use_filter assert page.filter_cols == ["Well"] # Checking that the selectors are not hidden - selector_row = dash_duo.find_element(f"#{page.selector_row}") + selector_row = dash_duo.find_element("#" + page.uuid("selector-row")) assert selector_row.get_attribute("style") == "" # Checking that the correct plot type is initialized - plot_dd = dash_duo.find_element(f"#{page.plot_option_id}-plottype") + plot_dd = dash_duo.find_element("#" + page.uuid("plottype")) assert plot_dd.text == "scatter" # Checking that only the relevant options are shown for plot_option in page.plot_args.keys(): - plot_option_dd = dash_duo.find_element( - f"#{page.plot_option_id}-div-{plot_option}" - ) + plot_option_dd = dash_duo.find_element("#" + page.uuid(f"div-{plot_option}")) if plot_option in page.plots["scatter"]: assert plot_option_dd.get_attribute("style") == "display: grid;" else: @@ -88,7 +80,7 @@ def test_table_plotter_filter(dash_duo): # Checking that options are initialized correctly for option in ["x", "y"]: - plot_option_dd = dash_duo.find_element(f"#{page.plot_option_id}-{option}") + plot_option_dd = dash_duo.find_element("#" + page.uuid(f"dropdown-{option}")) assert plot_option_dd.text == "Well" @@ -121,5 +113,5 @@ def test_initialized_table_plotter(dash_duo): assert page.lock # Checking that the selectors are hidden - selector_row = dash_duo.find_element(f"#{page.selector_row}") + selector_row = dash_duo.find_element("#" + page.uuid("selector-row")) assert selector_row.get_attribute("style") == "display: none;" diff --git a/webviz_config/_plugin_abc.py b/webviz_config/_plugin_abc.py index a045531a..4d74033a 100644 --- a/webviz_config/_plugin_abc.py +++ b/webviz_config/_plugin_abc.py @@ -53,10 +53,7 @@ def layout(self): ASSETS = [] def __init__(self): - """This function will later be used for e.g. setting a unique ID at - initialization, which then subclasses can use further. - - If a plugin/subclass defines its own `__init__` function + """If a plugin/subclass defines its own `__init__` function (which they usually do), they should remember to call ```python super().__init__() @@ -64,6 +61,25 @@ def __init__(self): in its own `__init__` function in order to also run the parent `__init__`. """ + self._plugin_uuid = uuid4() + + def uuid(self, element: str): + """Typically used to get a unique ID for some given element/component in + a plugins layout. If the element string is unique within the plugin, this + function returns a string which is guaranteed to be unique also across the + application (even when multiple instances of the same plugin is added). + + Within the same plugin instance, the returned uuid is the same for the same + element string. I.e. storing the returned value in the plugin is not necessary. + + Main benefit of using this function instead of creating a UUID directly, + is that the abstract base class can in the future provide IDs that + are consistent across application restarts (i.e. when the webviz configuration + file changes in a non-portable setting). + """ + + return f"{element}-{self._plugin_uuid}" + @property @abc.abstractmethod def layout(self): @@ -76,9 +92,9 @@ def layout(self): def _plugin_wrapper_id(self): # pylint: disable=attribute-defined-outside-init # We do not have a __init__ method in this abstract base class - if not hasattr(self, "_plugin_wrapper_uuid"): - self._plugin_wrapper_uuid = uuid4() - return f"plugin-wrapper-{self._plugin_wrapper_uuid}" + if not hasattr(self, "_plugin_uuid"): + self._plugin_uuid = uuid4() + return f"plugin-wrapper-{self._plugin_uuid}" @property def plugin_data_output(self): diff --git a/webviz_config/plugins/_data_table.py b/webviz_config/plugins/_data_table.py index 975351ad..4722d1da 100644 --- a/webviz_config/plugins/_data_table.py +++ b/webviz_config/plugins/_data_table.py @@ -1,4 +1,3 @@ -from uuid import uuid4 from pathlib import Path import pandas as pd @@ -40,7 +39,6 @@ def __init__( self.sorting = sorting self.filtering = filtering self.pagination = pagination - self.data_table_id = f"data-table-{uuid4()}" def add_webvizstore(self): return [(get_data, [{"csv_file": self.csv_file}])] @@ -48,7 +46,6 @@ def add_webvizstore(self): @property def layout(self): return dash_table.DataTable( - id=self.data_table_id, columns=[{"name": i, "id": i} for i in self.df.columns], data=self.df.to_dict("records"), sort_action="native" if self.sorting else "none", diff --git a/webviz_config/plugins/_example_plugin.py b/webviz_config/plugins/_example_plugin.py index 2f736571..8255fb0c 100644 --- a/webviz_config/plugins/_example_plugin.py +++ b/webviz_config/plugins/_example_plugin.py @@ -1,5 +1,3 @@ -from uuid import uuid4 - import dash_html_components as html from dash.dependencies import Input, Output @@ -12,9 +10,6 @@ def __init__(self, app, title: str): self.title = title - self.button_id = f"submit-button-{uuid4()}" - self.div_id = f"output-state-{uuid4()}" - self.set_callbacks(app) @property @@ -22,14 +17,17 @@ def layout(self): return html.Div( [ html.H1(self.title), - html.Button(id=self.button_id, n_clicks=0, children="Submit"), - html.Div(id=self.div_id), + html.Button( + id=self.uuid("submit-button"), n_clicks=0, children="Submit" + ), + html.Div(id=self.uuid("output-state")), ] ) def set_callbacks(self, app): @app.callback( - Output(self.div_id, "children"), [Input(self.button_id, "n_clicks")] + Output(self.uuid("output-state"), "children"), + [Input(self.uuid("submit-button"), "n_clicks")], ) def _update_output(n_clicks): return f"Button has been pressed {n_clicks} times." diff --git a/webviz_config/plugins/_example_tour.py b/webviz_config/plugins/_example_tour.py index dd55a864..0057dde7 100644 --- a/webviz_config/plugins/_example_tour.py +++ b/webviz_config/plugins/_example_tour.py @@ -1,22 +1,14 @@ -from uuid import uuid4 - import dash_html_components as html from .. import WebvizPluginABC class ExampleTour(WebvizPluginABC): - def __init__(self): - super().__init__() - - self.blue_text_id = f"element-{uuid4()}" - self.red_text_id = f"element-{uuid4()}" - @property def tour_steps(self): return [ - {"id": self.blue_text_id, "content": "This is the first step"}, - {"id": self.red_text_id, "content": "This is the second step"}, + {"id": self.uuid("blue_text"), "content": "This is the first step"}, + {"id": self.uuid("red_text"), "content": "This is the second step"}, ] @property @@ -25,12 +17,12 @@ def layout(self): children=[ html.Span( "Here is some blue text to explain... ", - id=self.blue_text_id, + id=self.uuid("blue_text"), style={"color": "blue"}, ), html.Span( " ...and here is some red text that also needs an explanation.", - id=self.red_text_id, + id=self.uuid("red_text"), style={"color": "red"}, ), ] diff --git a/webviz_config/plugins/_table_plotter.py b/webviz_config/plugins/_table_plotter.py index 0ad418f6..729b90d7 100644 --- a/webviz_config/plugins/_table_plotter.py +++ b/webviz_config/plugins/_table_plotter.py @@ -1,4 +1,3 @@ -from uuid import uuid4 from pathlib import Path from collections import OrderedDict @@ -41,7 +40,6 @@ def __init__( super().__init__() self.plot_options = plot_options if plot_options else {} - self.graph_id = f"graph-id{uuid4()}" self.lock = lock self.csv_file = csv_file self.data = get_data(self.csv_file) @@ -50,14 +48,11 @@ def __init__( self.numeric_columns = list( self.data.select_dtypes(include=[np.number]).columns ) - self.selector_row = f"selector-row{uuid4()}" - self.plot_option_id = f"plot-option{uuid4()}" self.plotly_theme = app.webviz_settings["theme"].plotly_theme self.set_callbacks(app) def set_filters(self, filter_cols): self.filter_cols = [] - self.filter_ids = {} self.use_filter = False if filter_cols: for col in filter_cols: @@ -66,9 +61,6 @@ def set_filters(self, filter_cols): self.filter_cols.append(col) if self.filter_cols: self.use_filter = True - self.filter_ids = { - col: f"{col}-{str(uuid4())}" for col in self.filter_cols - } def add_webvizstore(self): return [(get_data, [{"csv_file": self.csv_file}])] @@ -191,7 +183,7 @@ def filter_layout(self): children=[ html.Summary(col.lower().capitalize()), dcc.RangeSlider( - id=self.filter_ids[col], + id=self.uuid(f"filter-{col}"), min=min_val, max=max_val, step=(max_val - min_val) / 10, @@ -217,7 +209,7 @@ def filter_layout(self): children=[ html.Summary(col.lower().capitalize()), dcc.Dropdown( - id=self.filter_ids[col], + id=self.uuid(f"filter-{col}"), options=[ {"label": i, "value": i} for i in elements ], @@ -242,7 +234,7 @@ def plot_option_layout(self): html.H4("Set plot options"), html.P("Plot type"), dcc.Dropdown( - id=f"{self.plot_option_id}-plottype", + id=self.uuid("plottype"), clearable=False, options=[{"label": i, "value": i} for i in self.plots], value=self.plot_options.get("type", "scatter"), @@ -256,11 +248,11 @@ def plot_option_layout(self): divs.append( html.Div( style=self.style_options_div, - id=f"{self.plot_option_id}-div-{key}", + id=self.uuid(f"div-{key}"), children=[ html.P(key), dcc.Dropdown( - id=f"{self.plot_option_id}-{key}", + id=self.uuid(f"dropdown-{key}"), clearable=False, options=[{"label": i, "value": i} for i in arg["options"]], value=arg["value"], @@ -305,13 +297,13 @@ def layout(self): style=self.style_page_layout, children=[ html.Div( - id=self.selector_row, + id=self.uuid("selector-row"), style=self.style_selectors, children=self.plot_option_layout(), ), html.Div( style={"height": "100%"}, - children=wcc.Graph(id=self.graph_id), + children=wcc.Graph(id=self.uuid("graph-id")), ), html.Div(children=self.filter_layout()), ], @@ -324,9 +316,9 @@ def plot_output_callbacks(self): """Creates list of output dependencies for callback The outputs are the graph, and the style of the plot options""" outputs = [] - outputs.append(Output(self.graph_id, "figure")) + outputs.append(Output(self.uuid("graph-id"), "figure")) for plot_arg in self.plot_args.keys(): - outputs.append(Output(f"{self.plot_option_id}-div-{plot_arg}", "style")) + outputs.append(Output(self.uuid(f"div-{plot_arg}"), "style")) return outputs @property @@ -336,11 +328,11 @@ def plot_input_callbacks(self): for each plot option """ inputs = [] - inputs.append(Input(f"{self.plot_option_id}-plottype", "value")) + inputs.append(Input(self.uuid("plottype"), "value")) for plot_arg in self.plot_args.keys(): - inputs.append(Input(f"{self.plot_option_id}-{plot_arg}", "value")) + inputs.append(Input(self.uuid(f"dropdown-{plot_arg}"), "value")) for filtcol in self.filter_cols: - inputs.append(Input(self.filter_ids[filtcol], "value")) + inputs.append(Input(self.uuid(f"filter-{filtcol}"), "value")) return inputs def set_callbacks(self, app):