{title_html}
{content_html}
diff --git a/python/src/numerous/widgets/base/container.py b/python/src/numerous/widgets/base/container.py
index 59beba7..dbada57 100644
--- a/python/src/numerous/widgets/base/container.py
+++ b/python/src/numerous/widgets/base/container.py
@@ -41,7 +41,7 @@ def container(
style_str = "; ".join(style_parts)
return f"""
-
+
{content_html}
"""
diff --git a/python/src/numerous/widgets/base/url_params.py b/python/src/numerous/widgets/base/url_params.py
index c09c23d..a6054ec 100644
--- a/python/src/numerous/widgets/base/url_params.py
+++ b/python/src/numerous/widgets/base/url_params.py
@@ -87,7 +87,7 @@ def __init__(
def _handle_browser_updates(self, change: traitlets.Bunch) -> None:
"""Handle updates coming from the browser."""
with self._update_lock:
- trait_name = cast(str, change.name)
+ trait_name = cast("str", change.name)
new_value = change.new
if (
@@ -95,16 +95,16 @@ def _handle_browser_updates(self, change: traitlets.Bunch) -> None:
and self._on_params_change is not None
):
with suppress(TypeError, ValueError):
- self._on_params_change(cast(dict[str, str], new_value))
+ self._on_params_change(cast("dict[str, str]", new_value))
elif (
trait_name == "browser_path_segments"
and self._on_path_change is not None
):
- self._on_path_change(cast(list[str], new_value))
+ self._on_path_change(cast("list[str]", new_value))
elif (
trait_name == "browser_current_url" and self._on_url_change is not None
):
- self._on_url_change(cast(str, new_value))
+ self._on_url_change(cast("str", new_value))
def get_query_param(self, key: str, default: str = "") -> str:
"""
@@ -118,7 +118,7 @@ def get_query_param(self, key: str, default: str = "") -> str:
The parameter value or the default
"""
- params = cast(dict[str, str], self.browser_query_params)
+ params = cast("dict[str, str]", self.browser_query_params)
return params.get(key, default)
def get_query_params(self) -> dict[str, str]:
@@ -129,7 +129,7 @@ def get_query_params(self) -> dict[str, str]:
Dictionary of all query parameters
"""
- return cast(dict[str, str], dict(self.browser_query_params))
+ return cast("dict[str, str]", dict(self.browser_query_params))
def get_path_segments(self) -> list[str]:
"""
@@ -139,7 +139,7 @@ def get_path_segments(self) -> list[str]:
List of URL path segments
"""
- return cast(list[str], list(self.browser_path_segments))
+ return cast("list[str]", list(self.browser_path_segments))
def get_path_segment(self, index: int, default: str = "") -> str:
"""
@@ -153,7 +153,7 @@ def get_path_segment(self, index: int, default: str = "") -> str:
The segment value or the default
"""
- segments = cast(list[str], self.browser_path_segments)
+ segments = cast("list[str]", self.browser_path_segments)
if 0 <= index < len(segments):
return segments[index]
return default
@@ -167,7 +167,7 @@ def get_current_url(self) -> str:
parameters
"""
- return cast(str, self.browser_current_url)
+ return cast("str", self.browser_current_url)
def get_base_url(self) -> str:
"""
@@ -177,4 +177,4 @@ def get_base_url(self) -> str:
The base URL (e.g., 'https://example.com')
"""
- return cast(str, self.browser_base_url)
+ return cast("str", self.browser_base_url)
diff --git a/python/src/numerous/widgets/loadsave.py b/python/src/numerous/widgets/loadsave.py
index 192a662..482bc57 100644
--- a/python/src/numerous/widgets/loadsave.py
+++ b/python/src/numerous/widgets/loadsave.py
@@ -1,5 +1,6 @@
"""Module providing a save & load widget for generic items."""
+import inspect
from collections.abc import Callable
from typing import Any, Protocol, runtime_checkable
@@ -46,13 +47,17 @@ def load_item(self, item_id: str) -> tuple[bool, str | None]:
"""
...
- def save_item(self, force: bool = False) -> tuple[bool, str | None]:
+ def save_item(
+ self, force: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str | None]:
"""
Save the current item.
Args:
force: Whether to force a save even if the item doesn't appear modified.
This is used for Save As operations.
+ target_name: The name of the target item to save to. This is used for
+ Save As operations to existing items.
Returns:
A tuple of (success, message). If success is True, the item was saved
@@ -163,12 +168,20 @@ def load_item(self, item_id):
return True, f"Loaded {self.current_config['label']}"
return False, "Configuration not found"
- def save_item(self, force=False):
+ def save_item(self, force=False, target_name=None):
if self.current_config and self.current_id:
if self.modified or force:
- self.configs[self.current_id] = self.current_config.copy()
+ target_id = self.current_id
+ if target_name:
+ # Find the target by name
+ for config_id, config in self.configs.items():
+ if config["label"] == target_name:
+ target_id = config_id
+ break
+
+ self.configs[target_id] = self.current_config.copy()
self.modified = False
- return True, "Saved successfully"
+ return True, f"Saved to {target_name or 'current item'}"
return True, None
return False, "No configuration to save"
@@ -198,13 +211,16 @@ def save_item(self, force=False):
disable_load = traitlets.Bool(default_value=False).tag(sync=True)
disable_save = traitlets.Bool(default_value=False).tag(sync=True)
disable_save_as = traitlets.Bool(default_value=False).tag(sync=True)
+ disable_rename = traitlets.Bool(default_value=False).tag(sync=True)
disable_save_reason = traitlets.Unicode(allow_none=True).tag(sync=True)
+ disable_rename_reason = traitlets.Unicode(allow_none=True).tag(sync=True)
default_new_item_name = traitlets.Unicode(default_value="New Item").tag(sync=True)
# Action triggers (from Widget to Python)
do_save = traitlets.Bool(default_value=False).tag(sync=True)
do_reset = traitlets.Bool(default_value=False).tag(sync=True)
do_load = traitlets.Bool(default_value=False).tag(sync=True)
+ do_rename = traitlets.Bool(default_value=False).tag(sync=True)
# Response traits (from Python to Widget)
action_note = traitlets.Unicode(allow_none=True).tag(sync=True)
@@ -216,14 +232,22 @@ def save_item(self, force=False):
create_new_item = traitlets.Bool(default_value=False).tag(sync=True)
is_save_as = traitlets.Bool(default_value=False).tag(sync=True)
+ # For tracking save-as target
+ save_as_target_name = traitlets.Unicode(allow_none=True).tag(sync=True)
+
+ # For rename operations
+ rename_item_id = traitlets.Unicode(allow_none=True).tag(sync=True)
+ rename_new_name = traitlets.Unicode(allow_none=True).tag(sync=True)
+
_esm = ESM
_css = CSS
# Type aliases for readability
LoadCallback = Callable[[str], tuple[bool, str | None]]
- SaveCallback = Callable[[bool], tuple[bool, str | None]]
+ SaveCallback = Callable[..., tuple[bool, str | None]]
ResetCallback = Callable[[], tuple[bool, str | None]]
NewItemCallback = Callable[[str, bool], tuple[dict[str, Any], bool, str | None]]
+ RenameCallback = Callable[[str, str], tuple[bool, str | None]]
def __init__(
self,
@@ -232,11 +256,14 @@ def __init__(
on_save: SaveCallback | None = None,
on_reset: ResetCallback | None = None,
on_new: NewItemCallback | None = None,
+ on_rename: RenameCallback | None = None,
selected_item_id: str | None = None,
disable_load: bool = False,
disable_save: bool = False,
disable_save_as: bool = False,
+ disable_rename: bool = False,
disable_save_reason: str | None = None,
+ disable_rename_reason: str | None = None,
default_new_item_name: str = "New Item",
modified: bool = False,
modification_note: str | None = None,
@@ -250,17 +277,25 @@ def __init__(
on_load: Callback when an item is selected to load.
Should return (success, note).
on_save: Callback when save is requested.
- Should return (success, note).
+ Should return (success, note). Can optionally accept a target_name
+ parameter for Save As operations.
on_reset: Callback when reset is requested.
Should return (success, note).
- on_new: Callback when new item creation is requested.
+ on_new: Callback when new item creation is requested. This is called for
+ both "New Item" creation and "Save As" operations. The is_save_as
+ parameter distinguishes between the two cases.
Should return (item, success, note).
+ on_rename: Callback when item rename is requested.
+ Should return (success, note).
selected_item_id: ID of the item to select initially.
disable_load: Whether to disable the load button.
disable_save: Whether to disable the save button.
disable_save_as: Whether to disable the "Save As" button.
+ disable_rename: Whether to disable the rename functionality.
disable_save_reason: Optional reason why saving is disabled
(shown as tooltip).
+ disable_rename_reason: Optional reason why renaming is disabled
+ (shown as tooltip).
default_new_item_name: Default name for new items.
modified: Whether the current item is modified.
modification_note: Optional note to display about the modification.
@@ -274,6 +309,12 @@ def __init__(
self._on_save_callback = on_save
self._on_reset_callback = on_reset
self._on_new_callback = on_new
+ self._on_rename_callback = on_rename
+
+ # Check if save callback supports target_name parameter
+ self._save_callback_supports_target = self._check_save_callback_signature(
+ on_save
+ )
# Initialize the widget
super().__init__()
@@ -287,12 +328,37 @@ def __init__(
self.disable_load = disable_load
self.disable_save = disable_save
self.disable_save_as = disable_save_as
+ self.disable_rename = disable_rename
self.disable_save_reason = disable_save_reason
+ self.disable_rename_reason = disable_rename_reason
self.default_new_item_name = default_new_item_name
self.is_modified = modified
self.modification_note = modification_note
+ def _check_save_callback_signature(self, callback: SaveCallback | None) -> bool:
+ """
+ Check if the save callback supports the target_name parameter.
+
+ Args:
+ callback: The save callback to check.
+
+ Returns:
+ True if the callback supports target_name parameter, False otherwise.
+
+ """
+ if callback is None:
+ return False
+
+ try:
+ sig = inspect.signature(callback)
+ params = list(sig.parameters.keys())
+ except (ValueError, TypeError):
+ return False
+ else:
+ # Check if callback has target_name parameter
+ return "target_name" in params
+
def set_items(self, items: list[dict[str, Any]]) -> None:
"""
Update the list of items displayed in the widget.
@@ -340,6 +406,18 @@ def set_disable_save_as(self, disable: bool, reason: str | None = None) -> None:
self.disable_save_as = disable
self.disable_save_reason = reason
+ def set_disable_rename(self, disable: bool, reason: str | None = None) -> None:
+ """
+ Set whether rename is disabled and optionally provide a reason.
+
+ Args:
+ disable: Whether to disable the rename functionality.
+ reason: Optional reason why renaming is disabled (shown as tooltip).
+
+ """
+ self.disable_rename = disable
+ self.disable_rename_reason = reason
+
def set_selected_item(self, item_id: str | None) -> None:
"""
Set the selected item by ID.
@@ -350,6 +428,16 @@ def set_selected_item(self, item_id: str | None) -> None:
"""
self.selected_item_id = item_id
+ def set_save_as_target(self, target_name: str | None) -> None:
+ """
+ Set the target name for Save As operations.
+
+ Args:
+ target_name: The name of the target item to save to.
+
+ """
+ self.save_as_target_name = target_name
+
@traitlets.observe("do_save") # type: ignore[misc]
def _do_save_changed(self, change: traitlets.Bunch) -> None:
"""Handle save requests from the widget."""
@@ -357,7 +445,25 @@ def _do_save_changed(self, change: traitlets.Bunch) -> None:
return
if self._on_save_callback is not None:
- success, note = self._handle_save(save_forced=False)
+ # Get the target name if this is a Save As operation
+ target_name = self.save_as_target_name
+
+ # Find the target name by looking up the selected item
+ if (target_name is None or target_name == "") and self.selected_item_id:
+ target_item = next(
+ (
+ item
+ for item in self.items
+ if item["id"] == self.selected_item_id
+ ),
+ None,
+ )
+ if target_item:
+ target_name = target_item["label"]
+
+ success, note = self._handle_save(
+ save_forced=False, target_name=target_name
+ )
else:
success, note = True, None
@@ -368,8 +474,9 @@ def _do_save_changed(self, change: traitlets.Bunch) -> None:
if success:
self.set_modified(False)
- # Reset the flag
+ # Reset the flags
self.do_save = False
+ self.save_as_target_name = None
@traitlets.observe("do_reset") # type: ignore[misc]
def _do_reset_changed(self, change: traitlets.Bunch) -> None:
@@ -414,6 +521,44 @@ def _do_load_changed(self, change: traitlets.Bunch) -> None:
# Reset the flag
self.do_load = False
+ @traitlets.observe("do_rename") # type: ignore[misc]
+ def _do_rename_changed(self, change: traitlets.Bunch) -> None:
+ """Handle rename requests from the widget."""
+ if not change.new:
+ return
+
+ if (
+ self._on_rename_callback is not None
+ and self.rename_item_id
+ and self.rename_new_name
+ ):
+ success, note = self._on_rename_callback(
+ self.rename_item_id, self.rename_new_name
+ )
+ self.success_status = success
+ self.action_note = note
+
+ # Update the item in the list if rename was successful
+ if success:
+ updated_items = []
+ for item in self.items:
+ if item["id"] == self.rename_item_id:
+ updated_item = item.copy()
+ updated_item["label"] = self.rename_new_name
+ updated_items.append(updated_item)
+ else:
+ updated_items.append(item)
+ self.items = updated_items
+ self.search_results = updated_items.copy()
+ else:
+ self.success_status = False
+ self.action_note = "Rename failed: missing callback or parameters"
+
+ # Reset the flags
+ self.do_rename = False
+ self.rename_item_id = None
+ self.rename_new_name = None
+
@traitlets.observe("create_new_item") # type: ignore[misc]
def _create_new_item_changed(self, change: traitlets.Bunch) -> None:
"""Handle new item creation requests from the widget."""
@@ -465,15 +610,11 @@ def _create_item(
try:
return self._on_new_callback(name, is_save_as)
except TypeError:
- # If two parameters fail, try with just one parameter
- try:
- return self._on_new_callback(name) # type: ignore[call-arg]
- except (TypeError, ValueError, AttributeError, KeyError) as e:
- # Handle specific exceptions that might occur during item creation
- import uuid
+ # Callback might not support the second parameter, create default item
+ import uuid
- new_item = {"id": str(uuid.uuid4()), "label": name}
- return new_item, False, f"Failed to create new item: {e!s}"
+ new_item = {"id": str(uuid.uuid4()), "label": name}
+ return new_item, False, "Callback doesn't support is_save_as parameter"
def _handle_successful_item_creation(
self, new_item: dict[str, Any], is_save_as: bool
@@ -543,13 +684,16 @@ def _handle_modified_item_save(self, new_item: dict[str, Any]) -> None:
if not save_note:
self.action_note = f"Created '{new_item['label']}'"
- def _handle_save(self, save_forced: bool = False) -> tuple[bool, str | None]:
+ def _handle_save(
+ self, save_forced: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str | None]:
"""
Call the save callback.
Args:
save_forced: Whether to force a save even if the item doesn't
appear modified. This is used for Save As operations.
+ target_name: The name of the target item to save to, for Save As operations.
Returns:
A tuple of (success, note).
@@ -558,5 +702,9 @@ def _handle_save(self, save_forced: bool = False) -> tuple[bool, str | None]:
if self._on_save_callback is None:
return True, None
- # Always pass the force parameter - assume the callback supports it
+ # Call the callback with appropriate parameters
+ if self._save_callback_supports_target:
+ # New-style callback that supports target_name
+ return self._on_save_callback(save_forced, target_name)
+ # Legacy callback that only supports force parameter
return self._on_save_callback(save_forced)
diff --git a/python/src/numerous/widgets/numerous/stress_test_projects.py b/python/src/numerous/widgets/numerous/stress_test_projects.py
index 2f4c53c..c952a40 100644
--- a/python/src/numerous/widgets/numerous/stress_test_projects.py
+++ b/python/src/numerous/widgets/numerous/stress_test_projects.py
@@ -79,7 +79,7 @@ def main() -> None:
scenarios={},
)
- logging.info(f"\nCreating project {p+1}/{NUM_PROJECTS}")
+ logging.info(f"\nCreating project {p + 1}/{NUM_PROJECTS}")
save_project(project)
for s in range(SCENARIOS_PER_PROJECT):
@@ -91,7 +91,7 @@ def main() -> None:
files=None,
)
- logging.info(f" Creating scenario {s+1}/{SCENARIOS_PER_PROJECT}")
+ logging.info(f" Creating scenario {s + 1}/{SCENARIOS_PER_PROJECT}")
save_scenario(project, scenario)
for d in range(DOCUMENTS_PER_SCENARIO):
@@ -110,7 +110,9 @@ def main() -> None:
f"Created {NUM_PROJECTS} projects with {total_documents} total documents"
)
logging.info(f"Total time: {duration:.2f} seconds")
- logging.info(f"Average time per document: {(duration/total_documents):.3f} seconds")
+ logging.info(
+ f"Average time per document: {(duration / total_documents):.3f} seconds"
+ )
if __name__ == "__main__":
diff --git a/python/tests/test_core_logic.py b/python/tests/test_core_logic.py
new file mode 100644
index 0000000..cabd5bc
--- /dev/null
+++ b/python/tests/test_core_logic.py
@@ -0,0 +1,87 @@
+"""Tests for core logic functionality without LoadSaveWidget dependencies."""
+
+import inspect
+from typing import Any
+
+
+def test_callback_signature_detection() -> None:
+ """Test that we can detect callback signatures correctly."""
+
+ def legacy_callback(force: bool = False) -> tuple[bool, str]:
+ return True, f"Saved with force={force}"
+
+ def new_callback(
+ force: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str]:
+ return True, f"Saved with force={force}, target_name={target_name}"
+
+ def check_callback_signature(callback: Any) -> bool: # noqa: ANN401
+ """Check if the callback supports the target_name parameter."""
+ if callback is None:
+ return False
+
+ try:
+ sig = inspect.signature(callback)
+ params = list(sig.parameters.keys())
+ except (ValueError, TypeError):
+ return False
+ else:
+ return "target_name" in params
+
+ # Test signature detection
+ assert check_callback_signature(legacy_callback) is False
+ assert check_callback_signature(new_callback) is True
+
+ # Test with None
+ assert check_callback_signature(None) is False
+
+ # Test with non-callable
+ assert check_callback_signature("not a function") is False
+
+
+def test_save_as_scenario() -> None:
+ """Test that save-as scenarios work correctly."""
+ saved_calls: list[dict[str, Any]] = []
+
+ def save_callback(
+ force: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str]:
+ saved_calls.append({"force": force, "target_name": target_name})
+ return True, f"Saved to {target_name}"
+
+ # Simulate save-as operation
+ result = save_callback(force=False, target_name="New Configuration")
+
+ assert result[0] is True
+ assert "New Configuration" in result[1]
+ assert len(saved_calls) == 1
+ assert saved_calls[0]["target_name"] == "New Configuration"
+ assert saved_calls[0]["force"] is False
+
+
+def test_backwards_compatibility() -> None:
+ """Test that legacy callbacks still work correctly."""
+ saved_calls: list[dict[str, bool]] = []
+
+ def legacy_callback(force: bool = False) -> tuple[bool, str]:
+ saved_calls.append({"force": force})
+ return True, f"Saved with force={force}"
+
+ # Simulate legacy callback usage
+ result = legacy_callback(force=False)
+
+ assert result[0] is True
+ assert "force=False" in result[1]
+ assert len(saved_calls) == 1
+ assert saved_calls[0]["force"] is False
+
+
+if __name__ == "__main__":
+ try:
+ test_callback_signature_detection()
+ test_save_as_scenario()
+ test_backwards_compatibility()
+ except Exception: # noqa: BLE001
+ import traceback
+
+ traceback.print_exc()
diff --git a/python/tests/test_loadsave.py b/python/tests/test_loadsave.py
new file mode 100644
index 0000000..de1e449
--- /dev/null
+++ b/python/tests/test_loadsave.py
@@ -0,0 +1,304 @@
+"""Tests for LoadSaveWidget functionality."""
+
+import sys
+from pathlib import Path
+from unittest.mock import Mock, call
+
+
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+
+from numerous.widgets.loadsave import LoadSaveWidget
+
+
+class TestLoadSaveWidget:
+ """Test cases for LoadSaveWidget."""
+
+ def setup_method(self) -> None:
+ """Set up test fixtures."""
+ self.mock_on_load = Mock()
+ self.mock_on_save = Mock()
+ self.mock_on_reset = Mock()
+ self.mock_on_new = Mock()
+ self.mock_on_rename = Mock()
+
+ # Sample test data
+ self.test_items = [
+ {"id": "config_a", "label": "Configuration A"},
+ {"id": "config_b", "label": "Configuration B"},
+ {"id": "config_c", "label": "Configuration C"},
+ ]
+
+ # Reset mock return values
+ self.mock_on_load.return_value = (True, "Loaded successfully")
+ self.mock_on_save.return_value = (True, "Saved successfully")
+ self.mock_on_reset.return_value = (True, "Reset successfully")
+ self.mock_on_new.return_value = (
+ {"id": "new_config", "label": "New Config"},
+ True,
+ "Created successfully",
+ )
+ self.mock_on_rename.return_value = (True, "Renamed successfully")
+
+ def test_basic_initialization(self) -> None:
+ """Test basic widget initialization."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ assert widget.items == self.test_items
+ assert widget.selected_item_id is None
+ assert widget.is_modified is False
+
+ def test_load_operation(self) -> None:
+ """Test loading an item."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Simulate loading an item
+ widget.selected_item_id = "config_a"
+ widget.do_load = True
+
+ # Check that callback was called with correct parameters
+ self.mock_on_load.assert_called_once_with("config_a")
+
+ def test_save_operation(self) -> None:
+ """Test saving an item."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Simulate saving
+ widget.do_save = True
+
+ # Check that callback was called with force=False
+ self.mock_on_save.assert_called_once_with(False) # noqa: FBT003
+
+ def test_save_as_new_item(self) -> None:
+ """Test 'Save As' with a new item name."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Simulate Save As with new item
+ widget.new_item_name = "New Configuration"
+ widget.is_save_as = True
+ widget.create_new_item = True
+
+ # Check that callback was called with correct parameters
+ self.mock_on_new.assert_called_once_with("New Configuration", True) # noqa: FBT003
+
+ def test_save_as_existing_item_issue(self) -> None:
+ """Test the issue where 'Save As' to existing item doesn't pass the name."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Set up initial state: load configuration A
+ widget.selected_item_id = "config_a"
+ widget.do_load = True
+ widget.is_modified = True
+
+ # Now simulate "Save As" to existing configuration C
+ # This should pass the target config name to the save callback
+ widget.selected_item_id = "config_c"
+ widget.do_save = True
+
+ # The issue: save callback should receive information about target config
+ # Currently it only receives force=False, but should also receive target name
+ calls = self.mock_on_save.call_args_list
+ assert len(calls) == 1
+
+ # This is the current behavior (the bug) - save callback doesn't know target
+ call_args = calls[0]
+ assert call_args == call(False) # Only force parameter # noqa: FBT003
+
+ def test_reset_operation(self) -> None:
+ """Test resetting an item."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Simulate reset
+ widget.do_reset = True
+
+ # Check that callback was called
+ self.mock_on_reset.assert_called_once_with()
+
+ def test_new_item_creation(self) -> None:
+ """Test creating a new item."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Simulate new item creation
+ widget.new_item_name = "New Config"
+ widget.is_save_as = False
+ widget.create_new_item = True
+
+ # Check that callback was called with correct parameters
+ self.mock_on_new.assert_called_once_with("New Config", False) # noqa: FBT003
+
+ def test_save_as_workflow(self) -> None:
+ """Test the complete Save As workflow that demonstrates the issue."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Step 1: Load configuration A
+ widget.selected_item_id = "config_a"
+ widget.do_load = True
+ self.mock_on_load.assert_called_once_with("config_a")
+
+ # Step 2: Make some modifications
+ widget.is_modified = True
+
+ # Step 3: User selects "Save As" and chooses existing configuration C
+ # This should ideally pass "config_c" name to the save callback
+ widget.selected_item_id = "config_c"
+ widget.do_save = True
+
+ # The issue: save callback should know we're saving to "config_c"
+ # but it doesn't receive this information
+ save_calls = self.mock_on_save.call_args_list
+ assert len(save_calls) == 1
+
+ # Current behavior (the bug): only force parameter is passed
+ assert save_calls[0] == call(False) # noqa: FBT003
+
+ # Expected behavior (after fix): should pass target config info
+ # This test documents the current broken behavior
+
+ def test_save_callback_signature_compatibility(self) -> None:
+ """Test that save callback works with different signatures."""
+
+ # Test callback that only accepts force parameter (current)
+ def save_callback_old(force: bool) -> tuple[bool, str | None]:
+ return True, f"Saved with force={force}"
+
+ # Test callback that accepts force and target_name (proposed fix)
+ def save_callback_new(
+ force: bool, target_name: str | None = None
+ ) -> tuple[bool, str | None]:
+ return True, f"Saved with force={force}, target={target_name}"
+
+ # Widget should work with both callback signatures
+ widget1 = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=save_callback_old,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ widget2 = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=save_callback_new,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Both should work
+ assert widget1._on_save_callback == save_callback_old # noqa: SLF001
+ assert widget2._on_save_callback == save_callback_new # noqa: SLF001
+
+ def test_modified_state_management(self) -> None:
+ """Test modified state management."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Initially not modified
+ assert widget.is_modified is False
+
+ # Set modified
+ widget.set_modified(is_modified=True, note="Test modification")
+ assert widget.is_modified is True
+ assert widget.modification_note == "Test modification"
+
+ # After successful save, should be unmodified
+ widget.do_save = True
+ assert widget.is_modified is False
+
+ def test_disable_save_functionality(self) -> None:
+ """Test disabling save functionality."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ disable_save=True,
+ disable_save_reason="Testing disabled save",
+ )
+
+ assert widget.disable_save is True
+ assert widget.disable_save_reason == "Testing disabled save"
+
+ # Test dynamic disable
+ widget.set_disable_save(False)
+ assert widget.disable_save is False
+
+ def test_item_selection(self) -> None:
+ """Test item selection functionality."""
+ widget = LoadSaveWidget(
+ items=self.test_items,
+ on_load=self.mock_on_load,
+ on_save=self.mock_on_save,
+ on_reset=self.mock_on_reset,
+ on_new=self.mock_on_new,
+ )
+
+ # Set selected item
+ widget.set_selected_item("config_b")
+ assert widget.selected_item_id == "config_b"
+
+ # Set to None
+ widget.set_selected_item(None)
+ assert widget.selected_item_id is None
+
+ def test_disable_save_as_functionality(self) -> None:
+ """Test that disable save-as functionality works."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test"}],
+ disable_save_as=True,
+ )
+
+ assert widget.disable_save_as is True
diff --git a/python/tests/test_loadsave_disable_controls.py b/python/tests/test_loadsave_disable_controls.py
new file mode 100644
index 0000000..d6df228
--- /dev/null
+++ b/python/tests/test_loadsave_disable_controls.py
@@ -0,0 +1,156 @@
+"""Tests for LoadSaveWidget disable controls functionality."""
+
+import sys
+from pathlib import Path
+
+
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+
+from numerous.widgets.loadsave import LoadSaveWidget
+
+
+class TestLoadSaveWidgetDisableControls:
+ """Test cases for the disable controls functionality of LoadSaveWidget."""
+
+ def test_disable_rename_in_constructor(self) -> None:
+ """Test that rename functionality can be disabled in the constructor."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}],
+ disable_rename=True,
+ disable_rename_reason="User lacks permissions",
+ )
+ assert widget.disable_rename is True
+ assert widget.disable_rename_reason == "User lacks permissions"
+
+ def test_disable_rename_dynamically_via_method(self) -> None:
+ """Test that rename functionality can be disabled dynamically via methods."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}],
+ disable_rename=False,
+ )
+ assert widget.disable_rename is False
+ assert widget.disable_rename_reason is None
+
+ # Disable dynamically
+ widget.set_disable_rename(True, "Admin revoked permissions") # noqa: FBT003
+ assert widget.disable_rename is True
+ assert widget.disable_rename_reason == "Admin revoked permissions"
+
+ # Re-enable dynamically
+ widget.set_disable_rename(False, None) # noqa: FBT003
+ assert widget.disable_rename is False
+ assert widget.disable_rename_reason is None
+
+ def test_disable_all_ui_elements_simultaneously(self) -> None:
+ """Test that all UI elements can be disabled for restricted users."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}],
+ disable_save=True,
+ disable_save_reason="System maintenance",
+ disable_load=True,
+ disable_rename=True,
+ disable_rename_reason="Insufficient permissions",
+ )
+
+ # Check all are disabled
+ assert widget.disable_save is True
+ assert widget.disable_load is True
+ assert widget.disable_rename is True
+
+ # Check reasons are set (only save and rename have reason attributes)
+ assert widget.disable_save_reason == "System maintenance"
+ assert widget.disable_rename_reason == "Insufficient permissions"
+
+ def test_disable_controls_with_existing_functionality(self) -> None:
+ """Test that disable controls work with existing functionality."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}],
+ disable_save=True,
+ disable_save_reason="Testing save disable",
+ disable_load=True,
+ )
+
+ # Existing disable functionality should still work
+ assert widget.disable_save is True
+ assert widget.disable_load is True
+
+ def test_granular_control_methods(self) -> None:
+ """Test that granular control methods work for all UI elements."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}],
+ disable_save=False,
+ disable_rename=False,
+ )
+
+ # Test updating reasons
+ widget.set_disable_save(True, "Database backup in progress") # noqa: FBT003
+ widget.set_disable_rename(True, "Feature temporarily disabled") # noqa: FBT003
+
+ assert widget.disable_save_reason == "Database backup in progress"
+ assert widget.disable_rename_reason == "Feature temporarily disabled"
+
+ def test_dynamic_permission_updates_based_on_user_context(self) -> None:
+ """Test that permissions can be updated dynamically based on user context."""
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}],
+ disable_rename=True,
+ disable_rename_reason="Checking permissions...",
+ )
+
+ # Initially disabled
+ assert widget.disable_rename is True
+
+ # Simulate permission check completed - user has permissions
+ widget.set_disable_rename(False, None) # noqa: FBT003
+ assert widget.disable_rename is False
+ assert widget.disable_rename_reason is None
+
+ # Simulate user context change - permissions revoked
+ widget.set_disable_rename(True, "Session expired - please re-authenticate") # noqa: FBT003
+ assert widget.disable_rename is True
+ assert (
+ widget.disable_rename_reason == "Session expired - please re-authenticate"
+ )
+
+ def test_disable_rename_doesnt_affect_other_widget_state(self) -> None:
+ """Test that disabling rename doesn't affect other widget state."""
+ widget = LoadSaveWidget(
+ items=[
+ {"id": "item1", "label": "Item 1"},
+ {"id": "item2", "label": "Item 2"},
+ ],
+ selected_item_id="item1",
+ disable_rename=False,
+ )
+
+ # Widget should maintain its item list and selection
+ assert len(widget.items) == 2 # noqa: PLR2004
+ assert widget.selected_item_id == "item1"
+
+ # Disabling rename shouldn't affect other state
+ widget.set_disable_rename(True, "Testing state consistency") # noqa: FBT003
+ assert len(widget.items) == 2 # noqa: PLR2004
+ assert widget.selected_item_id == "item1"
+ assert widget.disable_rename is True
+
+
+if __name__ == "__main__":
+ test_class = TestLoadSaveWidgetDisableControls()
+
+ test_methods = [
+ test_class.test_disable_rename_in_constructor,
+ test_class.test_disable_rename_dynamically_via_method,
+ test_class.test_disable_all_ui_elements_simultaneously,
+ test_class.test_disable_controls_with_existing_functionality,
+ test_class.test_granular_control_methods,
+ test_class.test_dynamic_permission_updates_based_on_user_context,
+ test_class.test_disable_rename_doesnt_affect_other_widget_state,
+ ]
+
+ for test_method in test_methods:
+ try:
+ test_method()
+ except Exception: # noqa: BLE001
+ import traceback
+
+ traceback.print_exc()
diff --git a/python/tests/test_loadsave_rename_functionality.py b/python/tests/test_loadsave_rename_functionality.py
new file mode 100644
index 0000000..fa1a17d
--- /dev/null
+++ b/python/tests/test_loadsave_rename_functionality.py
@@ -0,0 +1,170 @@
+"""Tests for LoadSaveWidget rename functionality."""
+
+import sys
+from pathlib import Path
+
+
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+
+from numerous.widgets.loadsave import LoadSaveWidget
+
+
+class TestLoadSaveWidgetRenameFunctionality:
+ """Test cases for the rename functionality of LoadSaveWidget."""
+
+ def test_rename_callback_parameter_in_constructor(self) -> None:
+ """Test that on_rename callback can be passed to the widget constructor."""
+
+ def rename_callback(item_id: str, new_name: str) -> tuple[bool, str]:
+ return True, f"Renamed {item_id} to {new_name}"
+
+ widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test Item"}], on_rename=rename_callback
+ )
+
+ assert widget._on_rename_callback == rename_callback # noqa: SLF001
+
+ def test_rename_operation_with_callback(self) -> None:
+ """Test that rename operations call the callback with correct parameters."""
+ rename_calls: list[dict[str, str]] = []
+
+ def rename_callback(item_id: str, new_name: str) -> tuple[bool, str]:
+ rename_calls.append({"item_id": item_id, "new_name": new_name})
+ return True, f"Renamed {item_id} to {new_name}"
+
+ widget = LoadSaveWidget(
+ items=[{"id": "config1", "label": "Configuration 1"}],
+ on_rename=rename_callback,
+ )
+
+ # Simulate rename operation
+ widget.rename_item_id = "config1"
+ widget.rename_new_name = "Updated Configuration"
+ widget.do_rename = True
+
+ assert len(rename_calls) == 1
+ assert rename_calls[0]["item_id"] == "config1"
+ assert rename_calls[0]["new_name"] == "Updated Configuration"
+
+ def test_rename_updates_item_list_on_success(self) -> None:
+ """Test that successful rename operations update the widget's item list."""
+
+ def successful_rename_callback(
+ item_id: str, # noqa: ARG001
+ new_name: str,
+ ) -> tuple[bool, str]:
+ return True, f"Renamed to {new_name}"
+
+ widget = LoadSaveWidget(
+ items=[
+ {"id": "item1", "label": "Original Name"},
+ {"id": "item2", "label": "Other Item"},
+ ],
+ on_rename=successful_rename_callback,
+ )
+
+ # Rename item1
+ widget.rename_item_id = "item1"
+ widget.rename_new_name = "New Name"
+ widget.do_rename = True
+
+ # Check that the item list was updated
+ updated_item = next(
+ (item for item in widget.items if item["id"] == "item1"), None
+ )
+ assert updated_item is not None
+ assert updated_item["label"] == "New Name"
+
+ # Check that other items weren't affected
+ other_item = next(
+ (item for item in widget.items if item["id"] == "item2"), None
+ )
+ assert other_item is not None
+ assert other_item["label"] == "Other Item"
+
+ def test_rename_handles_callback_failure(self) -> None:
+ """Test that rename operations handle callback failures gracefully."""
+
+ def failing_rename_callback(
+ item_id: str, # noqa: ARG001
+ new_name: str, # noqa: ARG001
+ ) -> tuple[bool, str]:
+ return False, "Rename failed due to permissions"
+
+ widget = LoadSaveWidget(
+ items=[{"id": "item1", "label": "Original Name"}],
+ on_rename=failing_rename_callback,
+ )
+
+ # Attempt rename that will fail
+ widget.rename_item_id = "item1"
+ widget.rename_new_name = "New Name"
+ widget.do_rename = True
+
+ # Item list should not be updated on failure
+ item = next((item for item in widget.items if item["id"] == "item1"), None)
+ assert item is not None
+ assert item["label"] == "Original Name" # Should remain unchanged
+
+ assert widget.success_status is False
+ assert "permissions" in widget.action_note
+
+ def test_rename_without_callback_shows_error(self) -> None:
+ """Test that rename operations without callback show appropriate error."""
+ widget = LoadSaveWidget(
+ items=[{"id": "item1", "label": "Original Name"}],
+ # No on_rename callback provided
+ )
+
+ # Attempt rename without callback
+ widget.rename_item_id = "item1"
+ widget.rename_new_name = "New Name"
+ widget.do_rename = True
+
+ assert widget.success_status is False
+ assert "missing callback" in widget.action_note
+
+ def test_rename_clears_operation_state(self) -> None:
+ """Test that rename operations clear their state after completion."""
+
+ def rename_callback(
+ item_id: str, # noqa: ARG001
+ new_name: str,
+ ) -> tuple[bool, str]:
+ return True, f"Renamed to {new_name}"
+
+ widget = LoadSaveWidget(
+ items=[{"id": "item1", "label": "Original Name"}],
+ on_rename=rename_callback,
+ )
+
+ # Perform rename
+ widget.rename_item_id = "item1"
+ widget.rename_new_name = "New Name"
+ widget.do_rename = True
+
+ # Check that state is cleared
+ assert not widget.do_rename
+ assert widget.rename_item_id is None
+ assert widget.rename_new_name is None
+
+
+if __name__ == "__main__":
+ test_class = TestLoadSaveWidgetRenameFunctionality()
+
+ test_methods = [
+ test_class.test_rename_callback_parameter_in_constructor,
+ test_class.test_rename_operation_with_callback,
+ test_class.test_rename_updates_item_list_on_success,
+ test_class.test_rename_handles_callback_failure,
+ test_class.test_rename_without_callback_shows_error,
+ test_class.test_rename_clears_operation_state,
+ ]
+
+ for test_method in test_methods:
+ try:
+ test_method()
+ except Exception: # noqa: BLE001
+ import traceback
+
+ traceback.print_exc()
diff --git a/python/tests/test_loadsave_save_as_target_names.py b/python/tests/test_loadsave_save_as_target_names.py
new file mode 100644
index 0000000..c0b8fd5
--- /dev/null
+++ b/python/tests/test_loadsave_save_as_target_names.py
@@ -0,0 +1,247 @@
+"""Tests for LoadSaveWidget save-as target name functionality."""
+
+import sys
+from pathlib import Path
+
+
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+
+from numerous.widgets.loadsave import LoadSaveWidget
+
+
+class TestLoadSaveWidgetSaveAsTargetNames:
+ """Test cases for the save-as target name functionality of LoadSaveWidget."""
+
+ def test_save_callback_receives_target_name_parameter(self) -> None:
+ """Test that save callbacks can receive target name for save-as operations."""
+ save_calls: list[dict[str, str | bool | None]] = []
+
+ def enhanced_save_callback(
+ force: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str]:
+ save_calls.append({"force": force, "target_name": target_name})
+ return True, f"Saved to {target_name or 'current item'}"
+
+ widget = LoadSaveWidget(
+ items=[
+ {"id": "config_a", "label": "Configuration A"},
+ {"id": "config_b", "label": "Configuration B"},
+ ],
+ on_save=enhanced_save_callback,
+ )
+
+ # Test save-as with target name
+ widget.save_as_target_name = "Configuration B"
+ widget.do_save = True
+
+ assert len(save_calls) == 1
+ assert save_calls[0]["target_name"] == "Configuration B"
+ assert not save_calls[0]["force"]
+
+ def test_save_as_target_name_lookup_from_selected_item(self) -> None:
+ """Test that target names are properly looked up from selected item IDs."""
+ save_calls: list[dict[str, str | bool | None]] = []
+
+ def save_callback(
+ force: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str]:
+ save_calls.append({"force": force, "target_name": target_name})
+ return True, f"Saved to {target_name}"
+
+ widget = LoadSaveWidget(
+ items=[
+ {"id": "project_alpha", "label": "Project Alpha"},
+ {"id": "project_beta", "label": "Project Beta"},
+ {"id": "project_gamma", "label": "Project Gamma"},
+ ],
+ on_save=save_callback,
+ )
+
+ # Select an item and trigger save (simulating save-as)
+ widget.selected_item_id = "project_beta"
+ widget.do_save = True
+
+ assert len(save_calls) == 1
+ assert save_calls[0]["target_name"] == "Project Beta"
+
+ def test_backwards_compatibility_with_legacy_save_callbacks(self) -> None:
+ """Test that legacy save callbacks without target_name parameter still work."""
+ save_calls: list[dict[str, bool]] = []
+
+ def legacy_save_callback(force: bool = False) -> tuple[bool, str]:
+ save_calls.append({"force": force})
+ return True, "Saved with legacy callback"
+
+ widget = LoadSaveWidget(
+ items=[{"id": "item1", "label": "Item 1"}], on_save=legacy_save_callback
+ )
+
+ # Trigger save operation
+ widget.do_save = True
+
+ assert len(save_calls) == 1
+ assert not save_calls[0]["force"]
+
+ def test_save_as_workflow_with_configuration_management(self) -> None:
+ """Test complete save-as workflow with configuration management scenario."""
+ # Simulate a configuration management system
+ configurations = {
+ "dev_config": {
+ "id": "dev_config",
+ "label": "Development Config",
+ "data": {"env": "dev"},
+ },
+ "test_config": {
+ "id": "test_config",
+ "label": "Test Config",
+ "data": {"env": "test"},
+ },
+ "prod_config": {
+ "id": "prod_config",
+ "label": "Production Config",
+ "data": {"env": "prod"},
+ },
+ }
+
+ current_config = None
+ save_operations: list[str] = []
+
+ def configuration_save_callback(
+ force: bool = False, # noqa: ARG001
+ target_name: str | None = None,
+ ) -> tuple[bool, str]:
+ nonlocal current_config
+
+ if target_name:
+ # Find target configuration by name
+ target_config = None
+ for config in configurations.values():
+ if config["label"] == target_name:
+ target_config = config
+ break
+
+ if target_config:
+ target_config["data"] = current_config # type: ignore[assignment]
+ save_operations.append(f"Saved to {target_name}")
+ return True, f"Configuration saved to {target_name}"
+ return False, f"Target configuration '{target_name}' not found"
+ save_operations.append("Saved to current")
+ return True, "Configuration saved to current"
+
+ widget = LoadSaveWidget(
+ items=[
+ {"id": "dev_config", "label": "Development Config"},
+ {"id": "test_config", "label": "Test Config"},
+ {"id": "prod_config", "label": "Production Config"},
+ ],
+ on_save=configuration_save_callback,
+ )
+
+ # Simulate workflow: modify dev config and save as test config
+ current_config = {"env": "modified", "new_feature": True}
+ widget.save_as_target_name = "Test Config"
+ widget.do_save = True
+
+ assert len(save_operations) == 1
+ assert save_operations[0] == "Saved to Test Config"
+ assert configurations["test_config"]["data"] == {
+ "env": "modified",
+ "new_feature": True,
+ }
+
+ def test_explicit_target_name_takes_precedence_over_selected_item(self) -> None:
+ """Test that explicit target names take precedence over selected item lookup."""
+ save_calls: list[dict[str, str | bool | None]] = []
+
+ def save_callback(
+ force: bool = False, target_name: str | None = None
+ ) -> tuple[bool, str]:
+ save_calls.append({"force": force, "target_name": target_name})
+ return True, f"Saved to {target_name}"
+
+ widget = LoadSaveWidget(
+ items=[
+ {"id": "option_a", "label": "Option A"},
+ {"id": "option_b", "label": "Option B"},
+ ],
+ on_save=save_callback,
+ )
+
+ # Set both explicit target name and selected item
+ widget.save_as_target_name = "Explicit Target" # This should take precedence
+ widget.selected_item_id = "option_a" # This should be ignored
+ widget.do_save = True
+
+ assert len(save_calls) == 1
+ assert save_calls[0]["target_name"] == "Explicit Target" # Not "Option A"
+
+ def test_save_operation_state_cleanup_after_completion(self) -> None:
+ """Test that save operation state is properly cleaned up after completion."""
+
+ def save_callback(
+ force: bool = False, # noqa: ARG001
+ target_name: str | None = None,
+ ) -> tuple[bool, str]:
+ return True, f"Saved to {target_name}"
+
+ widget = LoadSaveWidget(
+ items=[{"id": "item1", "label": "Item 1"}], on_save=save_callback
+ )
+
+ # Set save-as target and trigger operation
+ widget.save_as_target_name = "Target Item"
+ widget.do_save = True
+
+ # State should be cleaned up after operation
+ assert not widget.do_save
+ assert widget.save_as_target_name is None
+
+ def test_save_callback_signature_detection_mechanism(self) -> None:
+ """Test the automatic detection of save callback signatures."""
+
+ def legacy_callback(force: bool) -> tuple[bool, str]: # noqa: ARG001
+ return True, "Legacy save"
+
+ def enhanced_callback(
+ force: bool, # noqa: ARG001
+ target_name: str | None = None, # noqa: ARG001
+ ) -> tuple[bool, str]:
+ return True, "Enhanced save"
+
+ # Test legacy callback detection
+ legacy_widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test"}], on_save=legacy_callback
+ )
+
+ # Should detect that legacy callback doesn't support target_name
+ assert not legacy_widget._save_callback_supports_target # noqa: SLF001
+
+ # Test enhanced callback detection
+ enhanced_widget = LoadSaveWidget(
+ items=[{"id": "test", "label": "Test"}], on_save=enhanced_callback
+ )
+
+ # Should detect that enhanced callback supports target_name
+ assert enhanced_widget._save_callback_supports_target # noqa: SLF001
+
+
+if __name__ == "__main__":
+ test_class = TestLoadSaveWidgetSaveAsTargetNames()
+
+ test_methods = [
+ test_class.test_save_callback_receives_target_name_parameter,
+ test_class.test_save_as_target_name_lookup_from_selected_item,
+ test_class.test_backwards_compatibility_with_legacy_save_callbacks,
+ test_class.test_save_as_workflow_with_configuration_management,
+ test_class.test_explicit_target_name_takes_precedence_over_selected_item,
+ test_class.test_save_operation_state_cleanup_after_completion,
+ test_class.test_save_callback_signature_detection_mechanism,
+ ]
+
+ for test_method in test_methods:
+ try:
+ test_method()
+ except Exception: # noqa: BLE001
+ import traceback
+
+ traceback.print_exc()