Compare commits

...

6 Commits

Author SHA1 Message Date
wyzula_j c0600a0cba feat: BL plugin menu in BECDockArea 2026-05-12 20:44:52 +02:00
semantic-release 1057db9d76 3.9.1
Automatically generated by python-semantic-release
2026-05-12 17:46:15 +00:00
wyzula_j be35e249f9 wip further opt 2026-05-12 19:45:22 +02:00
wyzula_j cdd833dfc2 tests(scan_control): tests extended and optimized 2026-05-12 19:45:22 +02:00
wyzula_j 3c7834b492 fix: logpanel fixture overwriting xread 2026-05-12 19:45:22 +02:00
wyzula_j acd35a2786 fix(scan_control): restore scan parameters from history are fetched on demand with button 2026-05-12 19:45:22 +02:00
9 changed files with 209 additions and 44 deletions
+11
View File
@@ -1,6 +1,17 @@
# CHANGELOG # CHANGELOG
## v3.9.1 (2026-05-12)
### Bug Fixes
- Logpanel fixture overwriting xread
([`3c7834b`](https://github.com/bec-project/bec_widgets/commit/3c7834b492a5d2da13689f58b20caf38dda9ac1d))
- **scan_control**: Restore scan parameters from history are fetched on demand with button
([`acd35a2`](https://github.com/bec-project/bec_widgets/commit/acd35a278660ce4962167af6237b5d12007f0774))
## v3.9.0 (2026-05-12) ## v3.9.0 (2026-05-12)
### Bug Fixes ### Bug Fixes
+9
View File
@@ -143,6 +143,15 @@ def get_plugin_designer_registry() -> dict[str, tuple[str, str]]:
return {} return {}
@lru_cache
def get_plugin_widget_icons() -> dict[str, str]:
"""If there is a plugin repository installed, return the designer widget icon registry."""
designer_module = get_plugin_designer_module()
if designer_module and hasattr(designer_module, "widget_icons"):
return designer_module.widget_icons
return {}
def get_all_plugin_widgets() -> BECClassContainer: def get_all_plugin_widgets() -> BECClassContainer:
"""If there is a plugin repository installed, load all widgets from it.""" """If there is a plugin repository installed, load all widgets from it."""
if plugin := user_widget_plugin(): if plugin := user_widget_plugin():
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from functools import lru_cache
from typing import Literal, Mapping, Sequence from typing import Literal, Mapping, Sequence
import slugify import slugify
@@ -20,7 +21,12 @@ from qtpy.QtWidgets import (
import bec_widgets.widgets.containers.qt_ads as QtAds import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets import BECWidget, SafeProperty, SafeSlot from bec_widgets import BECWidget, SafeProperty, SafeSlot
from bec_widgets.cli.designer_plugins import widget_icons from bec_widgets.cli.designer_plugins import widget_icons
from bec_widgets.utils.bec_plugin_helper import (
get_plugin_rpc_widget_registry,
get_plugin_widget_icons,
)
from bec_widgets.utils.colors import apply_theme from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.plugin_utils import get_rpc_widget_registry
from bec_widgets.utils.rpc_decorator import rpc_timeout from bec_widgets.utils.rpc_decorator import rpc_timeout
from bec_widgets.utils.rpc_widget_handler import widget_handler from bec_widgets.utils.rpc_widget_handler import widget_handler
from bec_widgets.utils.toolbars.actions import ( from bec_widgets.utils.toolbars.actions import (
@@ -74,6 +80,19 @@ PROFILE_STATE_KEYS = {key: SETTINGS_KEYS[key] for key in ("geom", "state", "ads_
StartupProfile = Literal["restore", "skip"] | str | None StartupProfile = Literal["restore", "skip"] | str | None
@lru_cache
def _plugin_toolbar_actions() -> dict[str, tuple[str, str, str]]:
plugin_registry = get_plugin_rpc_widget_registry()
internal_registry = get_rpc_widget_registry()
plugin_icons = get_plugin_widget_icons()
return {
widget_name: (plugin_icons.get(widget_name, "widgets"), f"Add {widget_name}", widget_name)
for widget_name in sorted(plugin_registry)
if widget_name not in internal_registry
}
class BECDockArea(DockAreaWidget): class BECDockArea(DockAreaWidget):
RPC = True RPC = True
PLUGIN = False PLUGIN = False
@@ -390,6 +409,10 @@ class BECDockArea(DockAreaWidget):
_build_menu("menu_devices", "Add Device Control ", device_actions) _build_menu("menu_devices", "Add Device Control ", device_actions)
_build_menu("menu_utils", "Add Utils ", util_actions) _build_menu("menu_utils", "Add Utils ", util_actions)
plugin_actions = _plugin_toolbar_actions()
if plugin_actions:
_build_menu("menu_plugins", "Add Plugins ", plugin_actions)
# Create flat toolbar bundles for each widget type # Create flat toolbar bundles for each widget type
def _build_flat_bundles(category: str, mapping: dict[str, tuple[str, str, str]]): def _build_flat_bundles(category: str, mapping: dict[str, tuple[str, str, str]]):
bundle = ToolbarBundle(f"flat_{category}", self.toolbar.components) bundle = ToolbarBundle(f"flat_{category}", self.toolbar.components)
@@ -460,14 +483,16 @@ class BECDockArea(DockAreaWidget):
bda.add_action("dark_mode") bda.add_action("dark_mode")
self.toolbar.add_bundle(bda) self.toolbar.add_bundle(bda)
self._apply_toolbar_layout() # Store mappings on self for use in _hook_toolbar and _apply_toolbar_layout
# Store mappings on self for use in _hook_toolbar
self._ACTION_MAPPINGS = { self._ACTION_MAPPINGS = {
"menu_plots": plot_actions, "menu_plots": plot_actions,
"menu_devices": device_actions, "menu_devices": device_actions,
"menu_utils": util_actions, "menu_utils": util_actions,
} }
if plugin_actions:
self._ACTION_MAPPINGS["menu_plugins"] = plugin_actions
self._apply_toolbar_layout()
def _hook_toolbar(self): def _hook_toolbar(self):
def _connect_menu(menu_key: str): def _connect_menu(menu_key: str):
@@ -476,7 +501,8 @@ class BECDockArea(DockAreaWidget):
# first two items not needed for this part # first two items not needed for this part
for key, (_, _, widget_type) in mapping.items(): for key, (_, _, widget_type) in mapping.items():
act = menu.actions[key].action toolbar_action = menu.actions[key]
act = toolbar_action.action
if key == "terminal": if key == "terminal":
act.triggered.connect( act.triggered.connect(
lambda _, t=widget_type: self.new(widget=t, closable=True, startup_cmd=None) lambda _, t=widget_type: self.new(widget=t, closable=True, startup_cmd=None)
@@ -487,12 +513,18 @@ class BECDockArea(DockAreaWidget):
widget=t, closable=True, show_settings_action=False widget=t, closable=True, show_settings_action=False
) )
) )
elif menu_key == "menu_plugins":
act.triggered.connect(
lambda _, t=widget_type, a=toolbar_action: self._new_plugin_widget(t, a)
)
else: else:
act.triggered.connect(lambda _, t=widget_type: self.new(widget=t)) act.triggered.connect(lambda _, t=widget_type: self.new(widget=t))
_connect_menu("menu_plots") _connect_menu("menu_plots")
_connect_menu("menu_devices") _connect_menu("menu_devices")
_connect_menu("menu_utils") _connect_menu("menu_utils")
if "menu_plugins" in self._ACTION_MAPPINGS:
_connect_menu("menu_plugins")
def _connect_flat_actions(mapping: dict[str, tuple[str, str, str]]): def _connect_flat_actions(mapping: dict[str, tuple[str, str, str]]):
for action_id, (_, _, widget_type) in mapping.items(): for action_id, (_, _, widget_type) in mapping.items():
@@ -507,6 +539,10 @@ class BECDockArea(DockAreaWidget):
self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all) self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all)
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot) self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
def _new_plugin_widget(self, widget_type: str, toolbar_action: MaterialIconAction) -> None:
# Created as helper method for simple tests
self.new(widget=widget_type, dock_icon=toolbar_action.get_icon())
def _set_editable(self, editable: bool) -> None: def _set_editable(self, editable: bool) -> None:
self.workspace_is_locked = not editable self.workspace_is_locked = not editable
self._editable = editable self._editable = editable
@@ -1108,14 +1144,10 @@ class BECDockArea(DockAreaWidget):
if mode_key == "user": if mode_key == "user":
bundles = ["spacer_bundle", "workspace", "dock_actions"] bundles = ["spacer_bundle", "workspace", "dock_actions"]
elif mode_key == "creator": elif mode_key == "creator":
bundles = [ bundles = ["menu_plots", "menu_devices", "menu_utils"]
"menu_plots", if "menu_plugins" in getattr(self, "_ACTION_MAPPINGS", {}):
"menu_devices", bundles.append("menu_plugins")
"menu_utils", bundles += ["spacer_bundle", "workspace", "dock_actions"]
"spacer_bundle",
"workspace",
"dock_actions",
]
elif mode_key == "plot": elif mode_key == "plot":
bundles = ["flat_plots", "spacer_bundle", "workspace", "dock_actions"] bundles = ["flat_plots", "spacer_bundle", "workspace", "dock_actions"]
elif mode_key == "device": elif mode_key == "device":
@@ -26,7 +26,6 @@ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
from bec_widgets.widgets.control.scan_control.scan_group_box import ScanGroupBox from bec_widgets.widgets.control.scan_control.scan_group_box import ScanGroupBox
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
class ScanParameterConfig(BaseModel): class ScanParameterConfig(BaseModel):
@@ -84,7 +83,6 @@ class ScanControl(BECWidget, QWidget):
self.kwarg_boxes = [] self.kwarg_boxes = []
self.expert_mode = False # TODO implement in the future versions self.expert_mode = False # TODO implement in the future versions
self.previous_scan = None self.previous_scan = None
self.last_scan_found = None
# Widget Default Parameters # Widget Default Parameters
self.config.default_scan = default_scan self.config.default_scan = default_scan
@@ -123,17 +121,12 @@ class ScanControl(BECWidget, QWidget):
scan_selection_layout.addWidget(self.comboBox_scan_selection, 1) scan_selection_layout.addWidget(self.comboBox_scan_selection, 1)
self.scan_selection_group.layout().addLayout(scan_selection_layout) self.scan_selection_group.layout().addLayout(scan_selection_layout)
# Label to reload the last scan parameters within scan selection group box # Button to reload the last scan parameters on demand.
self.toggle_layout = QHBoxLayout() self.last_scan_button = QPushButton(
self.toggle_layout.addSpacerItem( "Restore last scan parameters", self.scan_selection_group
QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
) )
self.last_scan_label = QLabel("Restore last scan parameters", self.scan_selection_group) self.last_scan_button.clicked.connect(self.request_last_executed_scan_parameters)
self.toggle = ToggleSwitch(parent=self.scan_selection_group, checked=False) self.scan_selection_group.layout().addWidget(self.last_scan_button)
self.toggle.enabled.connect(self.request_last_executed_scan_parameters)
self.toggle_layout.addWidget(self.last_scan_label)
self.toggle_layout.addWidget(self.toggle)
self.scan_selection_group.layout().addLayout(self.toggle_layout)
self.scan_selection_group.setSizePolicy( self.scan_selection_group.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed
) )
@@ -206,7 +199,6 @@ class ScanControl(BECWidget, QWidget):
"""Callback for scan selection combo box""" """Callback for scan selection combo box"""
selected_scan_name = self.comboBox_scan_selection.currentText() selected_scan_name = self.comboBox_scan_selection.currentText()
self.scan_selected.emit(selected_scan_name) self.scan_selected.emit(selected_scan_name)
self.request_last_executed_scan_parameters()
self.restore_scan_parameters(selected_scan_name) self.restore_scan_parameters(selected_scan_name)
@SafeSlot() @SafeSlot()
@@ -215,10 +207,6 @@ class ScanControl(BECWidget, QWidget):
""" """
Requests the last executed scan parameters from BEC and restores them to the scan control widget. Requests the last executed scan parameters from BEC and restores them to the scan control widget.
""" """
self.last_scan_found = False
if not self.toggle.checked:
return
current_scan = self.comboBox_scan_selection.currentText() current_scan = self.comboBox_scan_selection.currentText()
history = ( history = (
self.client.connector.xread( self.client.connector.xread(
@@ -246,8 +234,6 @@ class ScanControl(BECWidget, QWidget):
if merged and self.kwarg_boxes: if merged and self.kwarg_boxes:
for box in self.kwarg_boxes: for box in self.kwarg_boxes:
box.set_parameters(merged) box.set_parameters(merged)
self.last_scan_found = True
break break
@SafeProperty(str) @SafeProperty(str)
@@ -496,8 +482,6 @@ class ScanControl(BECWidget, QWidget):
Args: Args:
scan_name(str): Name of the scan to restore the parameters for. scan_name(str): Name of the scan to restore the parameters for.
""" """
if self.last_scan_found is True:
return
scan_params = self.config.scans.get(scan_name, None) scan_params = self.config.scans.get(scan_name, None)
if scan_params is None and self.previous_scan is None: if scan_params is None and self.previous_scan is None:
return return
+2 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "bec_widgets" name = "bec_widgets"
version = "3.9.0" version = "3.9.1"
description = "BEC Widgets" description = "BEC Widgets"
requires-python = ">=3.11" requires-python = ">=3.11"
classifiers = [ classifiers = [
@@ -60,6 +60,7 @@ qtermwidget = ["pyside6_qtermwidget"]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
+30 -1
View File
@@ -2,7 +2,10 @@ from importlib.machinery import FileFinder, SourceFileLoader
from types import ModuleType from types import ModuleType
from unittest import mock from unittest import mock
from bec_widgets.utils.bec_plugin_helper import _all_widgets_from_all_submods from bec_widgets.utils.bec_plugin_helper import (
_all_widgets_from_all_submods,
get_plugin_widget_icons,
)
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
@@ -69,3 +72,29 @@ def test_all_widgets_from_module_no_widgets():
widgets = _all_widgets_from_all_submods(module).as_dict() widgets = _all_widgets_from_all_submods(module).as_dict()
assert widgets == {} assert widgets == {}
def test_get_plugin_widget_icons_from_designer_module():
designer_module = mock.MagicMock(spec=ModuleType)
designer_module.widget_icons = {"PluginWidget": "star"}
get_plugin_widget_icons.cache_clear()
try:
with mock.patch(
"bec_widgets.utils.bec_plugin_helper.get_plugin_designer_module",
return_value=designer_module,
):
assert get_plugin_widget_icons() == {"PluginWidget": "star"}
finally:
get_plugin_widget_icons.cache_clear()
def test_get_plugin_widget_icons_without_designer_module():
get_plugin_widget_icons.cache_clear()
try:
with mock.patch(
"bec_widgets.utils.bec_plugin_helper.get_plugin_designer_module", return_value=None
):
assert get_plugin_widget_icons() == {}
finally:
get_plugin_widget_icons.cache_clear()
+66
View File
@@ -11,6 +11,7 @@ from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import QDialog, QMessageBox, QWidget from qtpy.QtWidgets import QDialog, QMessageBox, QWidget
import bec_widgets.widgets.containers.dock_area.basic_dock_area as basic_dock_module import bec_widgets.widgets.containers.dock_area.basic_dock_area as basic_dock_module
import bec_widgets.widgets.containers.dock_area.dock_area as dock_area_module
import bec_widgets.widgets.containers.dock_area.profile_utils as profile_utils import bec_widgets.widgets.containers.dock_area.profile_utils as profile_utils
from bec_widgets.widgets.containers.dock_area.basic_dock_area import ( from bec_widgets.widgets.containers.dock_area.basic_dock_area import (
DockAreaWidget, DockAreaWidget,
@@ -68,6 +69,13 @@ def temp_profile_dir():
return os.environ["BECWIDGETS_PROFILE_DIR"] return os.environ["BECWIDGETS_PROFILE_DIR"]
@pytest.fixture
def clear_plugin_toolbar_actions_cache():
dock_area_module._plugin_toolbar_actions.cache_clear()
yield
dock_area_module._plugin_toolbar_actions.cache_clear()
@pytest.fixture @pytest.fixture
def module_profile_factory(monkeypatch, tmp_path): def module_profile_factory(monkeypatch, tmp_path):
"""Provide a helper to create synthetic module-level (read-only) profiles.""" """Provide a helper to create synthetic module-level (read-only) profiles."""
@@ -985,6 +993,64 @@ class TestToolbarFunctionality:
# Verify save was called with the filename # Verify save was called with the filename
mock_screenshot.save.assert_called_once_with(str(screenshot_path)) mock_screenshot.save.assert_called_once_with(str(screenshot_path))
def test_plugin_toolbar_actions_empty_when_no_plugins(self, clear_plugin_toolbar_actions_cache):
"""Test that no plugin toolbar actions are produced when no plugin widgets exist."""
with patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_plugin_rpc_widget_registry",
return_value={},
):
plugin_actions = dock_area_module._plugin_toolbar_actions()
assert plugin_actions == {}
def test_plugin_toolbar_actions_include_available_plugins(
self, clear_plugin_toolbar_actions_cache
):
"""Test that plugin toolbar actions are built from RPC widgets and generated icons."""
plugin_registry = {
"FakePluginWidget": ("fake_plugin.widgets.fake_plugin_widget", "FakePluginWidget")
}
with (
patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_plugin_rpc_widget_registry",
return_value=plugin_registry,
),
patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_plugin_widget_icons",
return_value={"FakePluginWidget": "star"},
),
):
plugin_actions = dock_area_module._plugin_toolbar_actions()
assert plugin_actions == {
"FakePluginWidget": ("star", "Add FakePluginWidget", "FakePluginWidget")
}
def test_plugin_toolbar_actions_ignore_builtin_name_collisions(
self, clear_plugin_toolbar_actions_cache
):
"""Test that plugin widgets shadowed by built-ins are not added to the plugin menu."""
plugin_registry = {"Waveform": ("fake_plugin.widgets.waveform", "Waveform")}
with patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_plugin_rpc_widget_registry",
return_value=plugin_registry,
):
plugin_actions = dock_area_module._plugin_toolbar_actions()
assert plugin_actions == {}
def test_new_plugin_widget_passes_toolbar_icon_to_new(self):
"""Test that plugin widget creation passes the toolbar icon to dock creation."""
dock_area = MagicMock()
toolbar_action = MagicMock()
dock_icon = object()
toolbar_action.get_icon.return_value = dock_icon
BECDockArea._new_plugin_widget(dock_area, "FakePluginWidget", toolbar_action)
toolbar_action.get_icon.assert_called_once_with()
dock_area.new.assert_called_once_with(widget="FakePluginWidget", dock_icon=dock_icon)
class TestDockSettingsDialog: class TestDockSettingsDialog:
"""Test dock settings dialog functionality.""" """Test dock settings dialog functionality."""
+2 -2
View File
@@ -62,8 +62,8 @@ TEST_LOG_MESSAGES = [
@pytest.fixture @pytest.fixture
def log_panel(qtbot, mocked_client): def log_panel(qtbot, mocked_client, monkeypatch):
mocked_client.connector.xread = lambda *_, **__: TEST_LOG_MESSAGES monkeypatch.setattr(mocked_client.connector, "xread", lambda *_, **__: TEST_LOG_MESSAGES)
widget = LogPanel() widget = LogPanel()
qtbot.addWidget(widget) qtbot.addWidget(widget)
qtbot.waitExposed(widget) qtbot.waitExposed(widget)
+40 -7
View File
@@ -503,12 +503,47 @@ def test_changing_scans_remember_parameters(scan_control, mocked_client):
assert grid_kwargs["burst_at_each_point"] == kwargs["burst_at_each_point"] assert grid_kwargs["burst_at_each_point"] == kwargs["burst_at_each_point"]
@pytest.mark.skip(reason="Unreliable - GH issue #1134") def test_scan_selection_does_not_fetch_last_scan_parameters(
def test_get_scan_parameters_from_redis(scan_control, mocked_client): scan_control, mocked_client, monkeypatch
):
xread = MagicMock(wraps=mocked_client.connector.xread)
monkeypatch.setattr(mocked_client.connector, "xread", xread)
scan_control.comboBox_scan_selection.setCurrentText("line_scan")
assert scan_control.comboBox_scan_selection.currentText() == "line_scan"
scan_control.comboBox_scan_selection.setCurrentText("grid_scan")
xread.assert_not_called()
def test_restore_last_scan_parameters_button_fetches_on_demand(
scan_control, mocked_client, monkeypatch
):
xread = MagicMock(wraps=mocked_client.connector.xread)
monkeypatch.setattr(mocked_client.connector, "xread", xread)
scan_control.comboBox_scan_selection.setCurrentText("grid_scan")
scan_control.comboBox_scan_selection.setCurrentText("line_scan")
xread.assert_not_called()
scan_control.last_scan_button.click()
xread.assert_called_once_with(
MessageEndpoints.scan_history(), from_start=True, user_id=scan_control.object_name
)
args, kwargs = scan_control.get_scan_parameters(bec_object=False)
assert args == ["samx", 0.0, 2.0]
assert kwargs["steps"] == 10
assert kwargs["relative"] is False
assert kwargs["exp_time"] == 2
def test_get_scan_parameters_from_redis(scan_control):
scan_name = "line_scan" scan_name = "line_scan"
scan_control.comboBox_scan_selection.setCurrentText(scan_name) scan_control.comboBox_scan_selection.setCurrentText(scan_name)
scan_control.toggle.checked = True scan_control.last_scan_button.click()
args, kwargs = scan_control.get_scan_parameters(bec_object=False) args, kwargs = scan_control.get_scan_parameters(bec_object=False)
@@ -588,8 +623,7 @@ def test_scan_metadata_is_passed_to_scan_function(scan_control: ScanControl):
scans.grid_scan.assert_called_once_with(metadata=TEST_MD) scans.grid_scan.assert_called_once_with(metadata=TEST_MD)
@pytest.mark.skip(reason="Unreliable - GH issue #1134") def test_restore_parameters_with_fewer_arg_bundles(scan_control):
def test_restore_parameters_with_fewer_arg_bundles(scan_control, qtbot):
""" """
Ensure that when more argument bundles are present than exist in the Ensure that when more argument bundles are present than exist in the
stored history, restoring parameters regenerates the arg box to the stored history, restoring parameters regenerates the arg box to the
@@ -605,8 +639,7 @@ def test_restore_parameters_with_fewer_arg_bundles(scan_control, qtbot):
assert scan_control.arg_box.count_arg_rows() == 3 assert scan_control.arg_box.count_arg_rows() == 3
# Trigger restore of parameters from history # Trigger restore of parameters from history
scan_control.toggle.checked = True scan_control.last_scan_button.click()
qtbot.wait(200)
# After restore, arg_box should have only one bundle (the history size) # After restore, arg_box should have only one bundle (the history size)
assert scan_control.arg_box.count_arg_rows() == 1 assert scan_control.arg_box.count_arg_rows() == 1