1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-05 00:12:49 +01:00

test(device-manager): use mocked client for tests

This commit is contained in:
2026-01-08 11:32:02 +01:00
committed by Jan Wyzula
parent 04a30ea04c
commit f71c8c882f
8 changed files with 218 additions and 119 deletions

View File

@@ -344,6 +344,7 @@ class DeviceFormDialog(QtWidgets.QDialog):
# Config unchanged, we can reuse previous connection status. Only do this if the new
# connection status is UNKNOWN as the current validation should not test the connection.
connection_status = self._validation_result[2]
validation_msg = self._validation_result[3]
except Exception:
logger.debug(
f"Device config validation changed for config: {device_config} compared to previous validation. Using status from recent validation."

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from enum import IntEnum
from functools import partial
from typing import TYPE_CHECKING, Dict, List, Tuple
from typing import TYPE_CHECKING, List, Tuple
from bec_lib.logger import bec_logger
from bec_qthemes import apply_theme, material_icon
@@ -12,16 +12,17 @@ from qtpy import QtCore, QtGui, QtWidgets
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.device_manager.components import OphydValidation
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
get_validation_icons,
)
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import BECProgressBar
if TYPE_CHECKING:
from bec_widgets.utils.colors import AccentColor
from bec_widgets.widgets.control.device_manager.components.device_table.device_table import (
_ValidationResultIter,
)
logger = bec_logger.logger
@@ -234,22 +235,18 @@ class UploadRedisDialog(QtWidgets.QDialog):
class UploadAction(IntEnum):
"""Enum for upload actions."""
CANCEL = QtWidgets.QDialog.Rejected
OK = QtWidgets.QDialog.Accepted
CANCEL = QtWidgets.QDialog.DialogCode.Rejected
OK = QtWidgets.QDialog.DialogCode.Accepted
CONNECTION_TEST_REQUESTED = 999
# Signal to trigger upload after confirmation
upload_confirmed = QtCore.Signal(int)
# Request ophyd validation for all untested device connections
# list of device configs, added: bool, connect: bool
request_ophyd_validation = QtCore.Signal(list, bool, bool)
def __init__(
self,
parent,
ophyd_test_widget: OphydValidation,
device_configs: dict[str, Tuple[dict, int, int]] | None = None,
):
def __init__(self, parent, device_configs: dict[str, Tuple[dict, int, int]] | None = None):
super().__init__(parent=parent)
self.device_configs: dict[str, Tuple[dict, int, int]] = device_configs or {}
self.ophyd_test_widget = ophyd_test_widget
self._transparent_button_style = "background-color: transparent; border: none;"
self.colors = get_accent_colors()
@@ -267,14 +264,9 @@ class UploadRedisDialog(QtWidgets.QDialog):
self.has_invalid_configs: int = 0
self.has_untested_connections: int = 0
self.has_cannot_connect: int = 0
self._current_progress: int | None = None
self._setup_ui()
self._update_ui()
# Disable validation features if no ophyd test widget provided, else connect validation
self._validation_connection = self.ophyd_test_widget.validation_completed.connect(
self._update_from_ophyd_device_tests
)
def set_device_config(self, device_configs: dict[str, Tuple[dict, int, int]]):
"""
@@ -288,18 +280,6 @@ class UploadRedisDialog(QtWidgets.QDialog):
self.device_configs = device_configs
self._update_ui()
def accept(self):
self.cleanup()
return super().accept()
def reject(self):
self.cleanup()
return super().reject()
def cleanup(self):
"""Cleanup on dialog finish."""
self.ophyd_test_widget.validation_completed.disconnect(self._validation_connection)
def _setup_ui(self):
"""Setup the main UI for the dialog."""
self.setWindowTitle("Upload Configuration to BEC Server")
@@ -347,11 +327,6 @@ class UploadRedisDialog(QtWidgets.QDialog):
button_layout.addWidget(self.validate_connections_btn)
button_layout.addStretch()
button_layout.addSpacing(16)
# Progress bar
self._progress_bar = BECProgressBar(self)
self._progress_bar.setVisible(False)
button_layout.addWidget(self._progress_bar)
action_layout.addLayout(button_layout)
# Status indicator
@@ -498,7 +473,7 @@ class UploadRedisDialog(QtWidgets.QDialog):
@SafeSlot()
def _validate_connections(self):
"""Request validation of all untested connections."""
"""Request validation of all untested connections. This will close the dialog."""
testable_devices: List[dict] = []
for _, (config, _, connection_status) in self.device_configs.items():
if connection_status == ConnectionStatus.UNKNOWN.value:
@@ -507,13 +482,8 @@ class UploadRedisDialog(QtWidgets.QDialog):
testable_devices.append(config)
if len(testable_devices) > 0:
self.validate_connections_btn.setEnabled(False)
self._progress_bar.setVisible(True)
self._progress_bar.maximum = len(testable_devices)
self._progress_bar.minimum = 0
self._progress_bar.set_value(0)
self._current_progress = 0
self.ophyd_test_widget.change_device_configs(testable_devices, added=True, connect=True)
self.request_ophyd_validation.emit(testable_devices, True, True)
self.done(self.UploadAction.CONNECTION_TEST_REQUESTED)
@SafeSlot()
def _handle_upload(self):
@@ -611,35 +581,40 @@ class UploadRedisDialog(QtWidgets.QDialog):
return
self.update_device_status(device_config, config_status, connection_status)
@SafeSlot(list)
def _multiple_updates_from_ophyd_device_tests(self, validation_results: _ValidationResultIter):
"""
Callback slot for receiving multiple validation result updates from the ophyd test widget.
Args:
validation_results (list): List of tuples containing (device_config, config_status, connection_status, validation_msg).
"""
for cfg, cfg_status, conn_status, val_msg in validation_results:
self.update_device_status(cfg, cfg_status, conn_status)
self._update_ui()
@SafeSlot(dict, int, int)
def update_device_status(self, device_config: dict, config_status: int, connection_status: int):
"""Update the status of a specific device."""
# Update device config status
self._update_device_configs(device_config, config_status, connection_status, "")
# Recalculate summaries and UI state
self._update_ui()
def _update_device_configs(
self,
device_config: dict[str, Any],
config_status: int,
connection_status: int,
validation_msg: str,
):
device_name = device_config.get("name", "")
old_config, _, _ = self.device_configs.get(device_name, (None, None, None))
if old_config is not None:
self.device_configs[device_name] = (device_config, config_status, connection_status)
if self._current_progress is not None:
self._current_progress += 1
self._progress_bar.set_value(self._current_progress)
if self._current_progress >= self._progress_bar.maximum:
self._progress_bar.setVisible(False)
self._progress_bar.set_value(0)
self._current_progress = None
self.validation_completed()
self._update_ui()
return
# Update UI sections
self.config_section.add_device(device_config, config_status, connection_status)
# Recalculate summaries and UI state
self._update_ui()
def validation_completed(self):
"""Called when connection validation is completed."""
self.validate_connections_btn.setEnabled(True)
self._update_ui()
else:
# If device not found, add it
self.config_section.add_device(device_config, config_status, connection_status)
def main(): # pragma: no cover
@@ -705,12 +680,7 @@ def main(): # pragma: no cover
]
configs = {cfg[0]["name"]: cfg for cfg in sample_configs}
apply_theme("dark")
from unittest import mock
ophyd_test_widget = mock.MagicMock(spec=OphydValidation)
dialog = UploadRedisDialog(
parent=None, device_configs=configs, ophyd_test_widget=ophyd_test_widget
)
dialog = UploadRedisDialog(parent=None, device_configs=configs)
dialog.show()
sys.exit(app.exec_())

View File

@@ -35,6 +35,7 @@ from bec_widgets.widgets.control.device_manager.components import (
)
from bec_widgets.widgets.control.device_manager.components._util import SharedSelectionSignal
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
)
from bec_widgets.widgets.services.device_browser.device_item.config_communicator import (
@@ -57,7 +58,7 @@ class DeviceManagerDisplayWidget(DockAreaWidget):
request_ophyd_validation = Signal(list, bool, bool)
def __init__(self, parent=None, client=None, *args, **kwargs):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, variant="compact", *args, **kwargs)
# Push to Redis dialog
@@ -312,6 +313,13 @@ class DeviceManagerDisplayWidget(DockAreaWidget):
configs = list(self.device_table_view.get_selected_device_configs())
if not configs:
configs = self.device_table_view.get_device_config()
# Adjust the state of the icons in the device table view
self.device_table_view.update_multiple_device_validations(
[
(cfg, ConfigStatus.UNKNOWN.value, ConnectionStatus.UNKNOWN.value, "")
for cfg in configs
]
)
self.request_ophyd_validation.emit(configs, True, connect)
def _update_config_enabled_button(self, enabled: bool):
@@ -474,7 +482,10 @@ class DeviceManagerDisplayWidget(DockAreaWidget):
validation_results = self.device_table_view.get_validation_results()
# Create and show upload dialog
self._upload_redis_dialog = UploadRedisDialog(
parent=self, device_configs=validation_results, ophyd_test_widget=self.ophyd_test_view
parent=self, device_configs=validation_results
)
self._upload_redis_dialog.request_ophyd_validation.connect(
self.request_ophyd_validation.emit
)
# Show dialog
@@ -484,6 +495,10 @@ class DeviceManagerDisplayWidget(DockAreaWidget):
self._push_composition_to_redis(action="set")
elif reply == UploadRedisDialog.UploadAction.CANCEL:
self.ophyd_test_view.cancel_all_validations()
elif reply == UploadRedisDialog.UploadAction.CONNECTION_TEST_REQUESTED:
return QMessageBox.information(
self, "Connection Test Requested", "Running connection test on untested devices."
)
def _push_composition_to_redis(self, action: ConfigAction):
"""Push the current device composition to Redis."""

View File

@@ -1,3 +1,4 @@
# pylint: skip-file
from unittest.mock import MagicMock
from bec_lib.device import Device as BECDevice
@@ -255,6 +256,13 @@ class DMMock:
signals.append((device_name, signal_name, signal_info))
return signals
def _get_redis_device_config(self) -> list[dict]:
"""Mock method to emulate DeviceManager._get_redis_device_config."""
configs = []
for device in self.devices.values():
configs.append(device._config)
return configs
DEVICES = [
FakePositioner("samx", limits=[-10, 10], read_value=2.0),

View File

@@ -210,8 +210,8 @@ class DeviceTable(BECWidget, QtWidgets.QWidget):
_auto_size_request = QtCore.Signal()
def __init__(self, parent: QtWidgets.QWidget | None = None):
super().__init__(parent=parent)
def __init__(self, parent: QtWidgets.QWidget | None = None, client=None):
super().__init__(parent=parent, client=client)
self.headers_key_map: dict[str, str] = {
"Valid": "valid",
"Connect": "connect",
@@ -823,13 +823,14 @@ class DeviceTable(BECWidget, QtWidgets.QWidget):
# Public API to be called via signals/slots
# -------------------------------------------------------------------------
@SafeSlot(list)
@SafeSlot(list, bool)
def set_device_config(self, device_configs: _DeviceCfgIter, skip_validation: bool = False):
"""
Set the device config. This will clear any existing configs.
Args:
device_configs (Iterable[dict[str, Any]]): The device configs to set.
skip_validation (bool): Whether to skip validation for the set devices.
"""
self.set_busy(True, text="Loading device configurations...")
with self.table_sort_on_hold:
@@ -857,7 +858,7 @@ class DeviceTable(BECWidget, QtWidgets.QWidget):
self.device_config_in_sync_with_redis.emit(in_sync_with_redis)
self.set_busy(False, text="")
@SafeSlot(list)
@SafeSlot(list, bool)
def add_device_configs(self, device_configs: _DeviceCfgIter, skip_validation: bool = False):
"""
Add devices to the config. If a device already exists, it will be replaced. If the validation is
@@ -866,6 +867,7 @@ class DeviceTable(BECWidget, QtWidgets.QWidget):
Args:
device_configs (Iterable[dict[str, Any]]): The device configs to add.
skip_validation (bool): Whether to skip validation for the added devices.
"""
self.set_busy(True, text="Adding device configurations...")
already_in_table = []
@@ -894,13 +896,14 @@ class DeviceTable(BECWidget, QtWidgets.QWidget):
self.device_config_in_sync_with_redis.emit(in_sync_with_redis)
self.set_busy(False, text="")
@SafeSlot(list)
@SafeSlot(list, bool)
def update_device_configs(self, device_configs: _DeviceCfgIter, skip_validation: bool = False):
"""
Update devices in the config. If a device does not exist, it will be added.
Args:
device_configs (Iterable[dict[str, Any]]): The device configs to update.
skip_validation (bool): Whether to skip validation for the updated devices.
"""
self.set_busy(True, text="Loading device configurations...")
cfgs_updated = []

View File

@@ -494,7 +494,14 @@ class OphydValidation(BECWidget, QtWidgets.QWidget):
def device_table_config_changed(
self, device_configs: list[dict[str, Any]], added: bool, skip_validation: bool
) -> None:
"""Slot to handle device config changes in the device table."""
"""
Slot to handle device config changes in the device table.
Args:
device_configs (list[dict[str, Any]]): List of device configurations.
added (bool): Whether the devices are added to the existing list.
skip_validation (bool): Whether to skip validation for the added devices.
"""
self.change_device_configs(
device_configs=device_configs, added=added, skip_validation=skip_validation
)
@@ -528,7 +535,6 @@ class OphydValidation(BECWidget, QtWidgets.QWidget):
force_connect (bool, optional): Whether to force connection during validation. Defaults to False.
timeout (float, optional): Timeout for connection attempt. Defaults to 5.0.
skip_validation (bool, optional): Whether to skip validation for the added devices. Defaults to False.
keep_device_item_in_list (bool, optional): Whether to keep the device item in the list after validation in success case.
"""
if not READY_TO_TEST:
logger.error("Cannot change device configs: dependencies not available.")
@@ -652,7 +658,7 @@ class OphydValidation(BECWidget, QtWidgets.QWidget):
)
widget.request_rerun_validation.connect(self._on_request_rerun_validation)
self.list_widget.add_widget_item(device_name, widget)
if skip_validation is False:
if not skip_validation:
self.__delayed_submit_test(widget, connect, force_connect, timeout)
def _remove_device(self, device_name: str) -> None:

View File

@@ -57,6 +57,8 @@ from bec_widgets.widgets.control.device_manager.components.ophyd_validation.vali
)
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
from .client_mocks import mocked_client
class TestConstants:
"""Test class for constants and configuration values."""
@@ -296,9 +298,9 @@ class TestDeviceTable:
"""Test class for DeviceTable component."""
@pytest.fixture
def device_table(self, qtbot) -> Generator[DeviceTable, None, None]:
def device_table(self, qtbot, mocked_client) -> Generator[DeviceTable, None, None]:
"""Fixture to create a DeviceTable instance."""
table = DeviceTable()
table = DeviceTable(client=mocked_client)
qtbot.addWidget(table)
qtbot.waitExposed(table)
yield table
@@ -997,7 +999,7 @@ class TestOphydValidation:
assert label.text() == "Connect Legend:"
@pytest.fixture
def ophyd_test(self, qtbot):
def ophyd_test(self, qtbot, mocked_client):
"""Fixture to create an OphydValidation instance. We patch the method that starts the polling loop to avoid side effects."""
with (
mock.patch(
@@ -1009,7 +1011,7 @@ class TestOphydValidation:
return_value=False,
),
):
widget = OphydValidation()
widget = OphydValidation(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@@ -1034,6 +1036,47 @@ class TestOphydValidation:
qtbot.mouseClick(ophyd_test._stop_validation_button, QtCore.Qt.LeftButton)
assert click_event.is_set()
def test_ophyd_test_keep_visible_after_validation(self, ophyd_test: OphydValidation, qtbot):
"""Test the keep visible after validation logic."""
# Initially false
assert len(ophyd_test._keep_visible_after_validation) == 0
# Add device to keep visible
ophyd_test.add_device_to_keep_visible_after_validation("device_1")
assert "device_1" in ophyd_test._keep_visible_after_validation
# Add second device
ophyd_test.add_device_to_keep_visible_after_validation("device_2")
assert "device_2" in ophyd_test._keep_visible_after_validation
assert len(ophyd_test._keep_visible_after_validation) == 2
# Remove device
ophyd_test.remove_device_to_keep_visible_after_validation("device_1")
assert "device_1" not in ophyd_test._keep_visible_after_validation
assert "device_2" in ophyd_test._keep_visible_after_validation
# Change config with skip validation and device in keep visible list
with (
mock.patch.object(
ophyd_test, "_is_device_in_redis_session", return_value=True
) as mock_is_device_in_redis_session,
mock.patch.object(ophyd_test, "_add_device_config") as mock_add_device_config,
mock.patch.object(
ophyd_test, "_on_device_test_completed"
) as mock_on_device_test_completed,
):
ophyd_test.change_device_configs(
[{"name": "device_2", "deviceClass": "TestClass"}],
added=True,
skip_validation=False,
)
mock_add_device_config.assert_called_once()
mock_on_device_test_completed.assert_called_once_with(
{"name": "device_2", "deviceClass": "TestClass"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
"Device already in session.",
)
def test_ophyd_test_adding_devices(self, ophyd_test: OphydValidation, qtbot):
"""Test adding devices to OphydValidation widget."""
sample_devices = [

View File

@@ -41,6 +41,8 @@ from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophy
OphydValidation,
)
from .client_mocks import mocked_client
@pytest.fixture
def device_config() -> dict:
@@ -164,6 +166,67 @@ class TestDeviceManagerViewDialogs:
connection_settings_layout.count() == fields_in_config * 2
) # Each field has a label and a widget
def test_device_form_dialog_help_methods(
self, device_form_dialog: DeviceFormDialog, device_config, qtbot
):
"""Test help methods in DeviceFormDialog."""
# Test handle devices already in session results
dialog = device_form_dialog
# Test _handle_devices_already_in_session_results
with mock.patch.object(dialog, "_handle_validation_result") as mock_handle_validation:
dialog._handle_devices_already_in_session_results([(device_config, 0, 0, "")])
mock_handle_validation.assert_called_once_with(device_config, 0, 0, "")
mock_handle_validation.reset_mock()
dialog._handle_devices_already_in_session_results([])
mock_handle_validation.assert_not_called()
mock_handle_validation.reset_mock()
dialog._handle_devices_already_in_session_results(
[(device_config, 1, 0, ""), (device_config, 0, 0, "")]
)
mock_handle_validation.assert_called_once_with(
device_config, 1, 0, ""
) # Should be called with first
# Test _handle_validation_result
# I. No wait dialog present
dialog._handle_validation_result(device_config, 1, 3, "All good")
assert dialog._validation_result == (device_config, 1, 3, "All good")
# II. No previous validation, but wait dialog present
with mock.patch.object(dialog, "_wait_dialog") as mock_wait_dialog:
dialog._handle_validation_result(device_config, 1, 3, "All good")
assert dialog.config_validation_result == (device_config, 1, 3, "All good")
mock_wait_dialog.accept.assert_called_once()
mock_wait_dialog.close.assert_called_once()
mock_wait_dialog.deleteLater.assert_called_once()
mock_wait_dialog.reset_mock()
assert dialog._wait_dialog is None
# III. Previous validation present and the same config, wait dialog present
with mock.patch.object(dialog, "_wait_dialog") as mock_wait_dialog:
dialog._validation_result = (device_config, 1, 1, "Previous bad")
dialog._handle_validation_result(device_config, 1, 3, "All good")
assert dialog.config_validation_result == (device_config, 1, 1, "Previous bad")
mock_wait_dialog.accept.assert_called_once()
mock_wait_dialog.close.assert_called_once()
mock_wait_dialog.deleteLater.assert_called_once()
mock_wait_dialog.reset_mock()
assert dialog._wait_dialog is None
# IV. Previous validation present but different config, wait dialog present
with mock.patch.object(dialog, "_wait_dialog") as mock_wait_dialog:
different_config = device_config.copy()
different_config["deviceClass"] = "DifferentClass"
dialog._validation_result = (different_config, 1, 1, "Previous bad")
dialog._handle_validation_result(device_config, 1, 3, "All good")
assert dialog.config_validation_result == (device_config, 1, 3, "All good")
mock_wait_dialog.accept.assert_called_once()
mock_wait_dialog.close.assert_called_once()
mock_wait_dialog.deleteLater.assert_called_once()
def test_set_device_config(self, device_form_dialog: DeviceFormDialog, qtbot):
"""Test setting device configuration in DeviceFormDialog."""
dialog = device_form_dialog
@@ -330,9 +393,7 @@ class TestDeviceManagerViewDialogs:
@pytest.fixture
def upload_redis_dialog(self, qtbot):
"""Fixture for UploadRedisDialog."""
dialog = UploadRedisDialog(
parent=None, ophyd_test_widget=mock.MagicMock(spec=OphydValidation), device_configs={}
)
dialog = UploadRedisDialog(parent=None, device_configs={})
try:
qtbot.addWidget(dialog)
qtbot.waitExposed(dialog)
@@ -460,37 +521,16 @@ class TestDeviceManagerViewDialogs:
assert dialog.config_section.summary_label.text() == expected_text
def test_upload_redis_validate_connections(self, device_configs_invalid, qtbot):
"""Test the validate connections method in UploadRedisDialog."""
configs = device_configs_invalid
ophyd_test_mock = mock.MagicMock(spec=OphydValidation)
try:
dialog = UploadRedisDialog(
parent=None, ophyd_test_widget=ophyd_test_mock, device_configs=configs
)
qtbot.addWidget(dialog)
qtbot.waitExposed(dialog)
with mock.patch.object(
dialog.ophyd_test_widget, "change_device_configs"
) as mock_change:
dialog._validate_connections()
mock_change.assert_called_once_with(
[cfg for k, (cfg, _, _) in configs.items() if k in ["Device_0", "Device_3"]],
added=True,
connect=True,
)
finally:
dialog.close()
class TestDeviceManagerView:
"""Test class for DeviceManagerView functionality."""
@pytest.fixture
def dm_view(self, qtbot):
def dm_view(self, qtbot, mocked_client):
"""Fixture for DeviceManagerView."""
widget = DeviceManagerView()
# Assign the mocked client
widget.device_manager_widget.client = mocked_client
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@@ -513,7 +553,6 @@ class TestDeviceManagerView:
# Reset for test loading current config
dm_widget._initialized = False
dm_widget.stacked_layout.setCurrentWidget(dm_widget._overlay_widget)
dm_widget.client.device_manager = mock.MagicMock()
with mock.patch.object(
dm_widget.client.device_manager, "_get_redis_device_config"
@@ -532,12 +571,26 @@ class TestDeviceManagerView:
mock_set.assert_called_once_with([])
@pytest.fixture
def device_manager_display_widget(self, qtbot):
"""Fixture for DeviceManagerDisplayWidget within DeviceManagerView."""
widget = DeviceManagerDisplayWidget()
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def device_manager_display_widget(self, qtbot, mocked_client):
"""Fixture for DeviceManagerDisplayWidget within DeviceManagerView.
We will patch the OphydValidation _thread_pool_poll_loop to avoid starting threads during tests,
and the _is_device_in_redis_session method to avoid Redis dependencies
"""
with (
mock.patch(
"bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation.OphydValidation._thread_pool_poll_loop",
return_value=None,
),
mock.patch(
"bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation.OphydValidation._is_device_in_redis_session",
return_value=False,
),
):
widget = DeviceManagerDisplayWidget(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@pytest.fixture
def device_configs(self, device_config: dict):