diff --git a/bec_widgets/applications/launch_window.py b/bec_widgets/applications/launch_window.py index 311a3af4..1aad9f5c 100644 --- a/bec_widgets/applications/launch_window.py +++ b/bec_widgets/applications/launch_window.py @@ -231,6 +231,10 @@ class LaunchWindow(BECMainWindow): raise ValueError( f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}." ) + if not WidgetContainerUtils.has_name_valid_chars(name): + raise ValueError( + f"Name {name} contains invalid characters. Only alphanumeric characters, underscores, and dashes are allowed." + ) else: name = "dock_area" name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas) diff --git a/bec_widgets/utils/generate_designer_plugin.py b/bec_widgets/utils/generate_designer_plugin.py index 2e912cd3..53d6b712 100644 --- a/bec_widgets/utils/generate_designer_plugin.py +++ b/bec_widgets/utils/generate_designer_plugin.py @@ -5,6 +5,8 @@ from typing import NamedTuple from qtpy.QtCore import QObject +from bec_widgets.utils.name_utils import pascal_to_snake + EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"] @@ -22,7 +24,7 @@ class DesignerPluginInfo: def __init__(self, plugin_class): self.plugin_class = plugin_class self.plugin_name_pascal = plugin_class.__name__ - self.plugin_name_snake = self.pascal_to_snake(self.plugin_name_pascal) + self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal) self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}" plugin_module = ( ".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin" @@ -38,21 +40,6 @@ class DesignerPluginInfo: self.base_path = os.path.dirname(inspect.getfile(plugin_class)) - @staticmethod - def pascal_to_snake(name: str) -> str: - """ - Convert PascalCase to snake_case. - - Args: - name (str): The name to be converted. - - Returns: - str: The converted name. - """ - s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) - s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1) - return s2.lower() - class DesignerPluginGenerator: def __init__(self, widget: type): diff --git a/bec_widgets/utils/name_utils.py b/bec_widgets/utils/name_utils.py new file mode 100644 index 00000000..02fb7b2d --- /dev/null +++ b/bec_widgets/utils/name_utils.py @@ -0,0 +1,16 @@ +import re + + +def pascal_to_snake(name: str) -> str: + """ + Convert PascalCase to snake_case. + + Args: + name (str): The name to be converted. + + Returns: + str: The converted name. + """ + s1 = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + s2 = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s1) + return s2.lower() diff --git a/bec_widgets/widgets/containers/dock/dock_area.py b/bec_widgets/widgets/containers/dock/dock_area.py index 800023bf..6856338d 100644 --- a/bec_widgets/widgets/containers/dock/dock_area.py +++ b/bec_widgets/widgets/containers/dock/dock_area.py @@ -14,6 +14,7 @@ from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.error_popups import SafeSlot +from bec_widgets.utils.name_utils import pascal_to_snake from bec_widgets.utils.toolbar import ( ExpandableMenuAction, MaterialIconAction, @@ -242,7 +243,8 @@ class BECDockArea(BECWidget, QWidget): def _create_widget_from_toolbar(self, widget_name: str) -> None: # Run with RPC broadcast to namespace of all widgets with RPCRegister.delayed_broadcast(): - dock_name = WidgetContainerUtils.generate_unique_name(widget_name, self.panels.keys()) + name = pascal_to_snake(widget_name) + dock_name = WidgetContainerUtils.generate_unique_name(name, self.panels.keys()) self.new(name=dock_name, widget=widget_name) def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions @@ -362,6 +364,11 @@ class BECDockArea(BECWidget, QWidget): f"Name {name} must be unique for docks, but already exists in DockArea " f"with name: {self.object_name} and id {self.gui_id}." ) + if not WidgetContainerUtils.has_name_valid_chars(name): + raise ValueError( + f"Name {name} contains invalid characters. " + f"Only alphanumeric characters and underscores are allowed." + ) else: # Name is not provided name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names) diff --git a/tests/unit_tests/test_bec_dock.py b/tests/unit_tests/test_bec_dock.py index 7922a86e..14c12d37 100644 --- a/tests/unit_tests/test_bec_dock.py +++ b/tests/unit_tests/test_bec_dock.py @@ -94,41 +94,45 @@ def test_undock_and_dock_docks(bec_dock_area, qtbot): ################################### def test_toolbar_add_plot_waveform(bec_dock_area): bec_dock_area.toolbar.widgets["menu_plots"].widgets["waveform"].trigger() - assert "Waveform_0" in bec_dock_area.panels - assert bec_dock_area.panels["Waveform_0"].widgets[0].config.widget_class == "Waveform" + assert "waveform_0" in bec_dock_area.panels + assert bec_dock_area.panels["waveform_0"].widgets[0].config.widget_class == "Waveform" def test_toolbar_add_plot_scatter_waveform(bec_dock_area): bec_dock_area.toolbar.widgets["menu_plots"].widgets["scatter_waveform"].trigger() - assert "ScatterWaveform_0" in bec_dock_area.panels + assert "scatter_waveform_0" in bec_dock_area.panels assert ( - bec_dock_area.panels["ScatterWaveform_0"].widgets[0].config.widget_class + bec_dock_area.panels["scatter_waveform_0"].widgets[0].config.widget_class == "ScatterWaveform" ) def test_toolbar_add_plot_image(bec_dock_area): bec_dock_area.toolbar.widgets["menu_plots"].widgets["image"].trigger() - assert "Image_0" in bec_dock_area.panels - assert bec_dock_area.panels["Image_0"].widgets[0].config.widget_class == "Image" + assert "image_0" in bec_dock_area.panels + assert bec_dock_area.panels["image_0"].widgets[0].config.widget_class == "Image" def test_toolbar_add_plot_motor_map(bec_dock_area): bec_dock_area.toolbar.widgets["menu_plots"].widgets["motor_map"].trigger() - assert "MotorMap_0" in bec_dock_area.panels - assert bec_dock_area.panels["MotorMap_0"].widgets[0].config.widget_class == "MotorMap" + assert "motor_map_0" in bec_dock_area.panels + assert bec_dock_area.panels["motor_map_0"].widgets[0].config.widget_class == "MotorMap" def test_toolbar_add_multi_waveform(bec_dock_area): bec_dock_area.toolbar.widgets["menu_plots"].widgets["multi_waveform"].trigger() - assert "MultiWaveform_0" in bec_dock_area.panels - assert bec_dock_area.panels["MultiWaveform_0"].widgets[0].config.widget_class == "MultiWaveform" + assert "multi_waveform_0" in bec_dock_area.panels + assert ( + bec_dock_area.panels["multi_waveform_0"].widgets[0].config.widget_class == "MultiWaveform" + ) def test_toolbar_add_device_positioner_box(bec_dock_area): bec_dock_area.toolbar.widgets["menu_devices"].widgets["positioner_box"].trigger() - assert "PositionerBox_0" in bec_dock_area.panels - assert bec_dock_area.panels["PositionerBox_0"].widgets[0].config.widget_class == "PositionerBox" + assert "positioner_box_0" in bec_dock_area.panels + assert ( + bec_dock_area.panels["positioner_box_0"].widgets[0].config.widget_class == "PositionerBox" + ) def test_toolbar_add_utils_queue(bec_dock_area, bec_queue_msg_full): @@ -136,20 +140,20 @@ def test_toolbar_add_utils_queue(bec_dock_area, bec_queue_msg_full): MessageEndpoints.scan_queue_status(), bec_queue_msg_full ) bec_dock_area.toolbar.widgets["menu_utils"].widgets["queue"].trigger() - assert "BECQueue_0" in bec_dock_area.panels - assert bec_dock_area.panels["BECQueue_0"].widgets[0].config.widget_class == "BECQueue" + assert "bec_queue_0" in bec_dock_area.panels + assert bec_dock_area.panels["bec_queue_0"].widgets[0].config.widget_class == "BECQueue" def test_toolbar_add_utils_status(bec_dock_area): bec_dock_area.toolbar.widgets["menu_utils"].widgets["status"].trigger() - assert "BECStatusBox_0" in bec_dock_area.panels - assert bec_dock_area.panels["BECStatusBox_0"].widgets[0].config.widget_class == "BECStatusBox" + assert "bec_status_box_0" in bec_dock_area.panels + assert bec_dock_area.panels["bec_status_box_0"].widgets[0].config.widget_class == "BECStatusBox" def test_toolbar_add_utils_progress_bar(bec_dock_area): bec_dock_area.toolbar.widgets["menu_utils"].widgets["progress_bar"].trigger() - assert "RingProgressBar_0" in bec_dock_area.panels + assert "ring_progress_bar_0" in bec_dock_area.panels assert ( - bec_dock_area.panels["RingProgressBar_0"].widgets[0].config.widget_class + bec_dock_area.panels["ring_progress_bar_0"].widgets[0].config.widget_class == "RingProgressBar" )