From c1dd33fda899a08880f53ef5f36ec0b177efc12a Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 25 Mar 2026 16:34:01 -0400 Subject: [PATCH 1/2] fix: stabilize search/list material selection --- src/pieces/core/list_command.py | 30 +++++++++++++----------------- tests/assets/list_assets_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/pieces/core/list_command.py b/src/pieces/core/list_command.py index da72c2f8..bada63e3 100644 --- a/src/pieces/core/list_command.py +++ b/src/pieces/core/list_command.py @@ -1,5 +1,4 @@ from collections.abc import Iterable -import threading from pieces.settings import Settings from pieces._vendor.pieces_os_client.wrapper.basic_identifier.asset import BasicAsset @@ -24,28 +23,25 @@ def list_command(cls, **kwargs): def list_assets(cls, **kwargs): from pieces.utils import PiecesSelectMenu - assets = kwargs.get( - "assets", - [BasicAsset(item.id) for item in BasicAsset.get_identifiers()], # type: ignore[assignment] - ) + assets = kwargs.get("assets") + if assets is None: + assets = [BasicAsset(item.id) for item in BasicAsset.get_identifiers()] + + menu_entries = [] + for index, asset in enumerate(assets, start=1): + try: + menu_entries.append( + (f"{index}: {asset.name}", {"asset_id": asset.id, **kwargs}) + ) + except AttributeError: + continue select_menu = PiecesSelectMenu( - [], + menu_entries, AssetsCommands.open_asset, kwargs.get("footer"), title="Select a material", ) - - def update_assets(): - for i, asset in enumerate(assets, start=1): - try: - select_menu.add_entry( - (f"{i}: {asset.name}", {"asset_id": asset.id, **kwargs}) - ) - except AttributeError: - pass - - threading.Thread(target=update_assets).start() select_menu.run() @classmethod diff --git a/tests/assets/list_assets_test.py b/tests/assets/list_assets_test.py index 90637cc7..6d005f62 100644 --- a/tests/assets/list_assets_test.py +++ b/tests/assets/list_assets_test.py @@ -5,6 +5,7 @@ from pieces.settings import Settings import sys from pieces.core.assets_command import AssetsCommands +from pieces.core.list_command import ListCommand MODULE_NAME = "pieces.core.list_command" OPEN_MODULE_NAME = "pieces.core.assets_command" @@ -66,3 +67,28 @@ def test_open_asset_success(mock_run, mock_assets, mock_basic_asset, mock_sh, tm assert result is None mock_run.assert_called_once() + + +@patch("pieces.utils.PiecesSelectMenu") +@patch(f"{OPEN_MODULE_NAME}.Settings.pieces_client.assets") +def test_list_assets_builds_menu_entries_with_asset_ids( + mock_assets_api, mock_menu_cls, mock_assets +): + mock_assets_api.return_value = mock_assets + + ListCommand.list_assets(assets=mock_assets, footer="Search results") + + mock_menu_cls.assert_called_once() + menu_entries, on_enter, footer = mock_menu_cls.call_args.args + + assert footer == "Search results" + assert on_enter == AssetsCommands.open_asset + assert len(menu_entries) == len(mock_assets) + assert [label for label, _ in menu_entries] == [ + f"{index}: {asset.name}" for index, asset in enumerate(mock_assets, start=1) + ] + assert [payload["asset_id"] for _, payload in menu_entries] == [ + asset.id for asset in mock_assets + ] + mock_menu_cls.return_value.run.assert_called_once() + mock_menu_cls.return_value.add_entry.assert_not_called() From 5fe30462fed9518ff6b96b4a0bacd56eb4b8d8a6 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 25 Mar 2026 17:50:59 -0400 Subject: [PATCH 2/2] fix: handle empty material selectors --- src/pieces/core/list_command.py | 4 +++ tests/assets/list_assets_test.py | 62 +++++++++++++++++++------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/pieces/core/list_command.py b/src/pieces/core/list_command.py index bada63e3..5dc508be 100644 --- a/src/pieces/core/list_command.py +++ b/src/pieces/core/list_command.py @@ -36,6 +36,10 @@ def list_assets(cls, **kwargs): except AttributeError: continue + if not menu_entries: + Settings.logger.print("No materials available to select.") + return + select_menu = PiecesSelectMenu( menu_entries, AssetsCommands.open_asset, diff --git a/tests/assets/list_assets_test.py b/tests/assets/list_assets_test.py index 6d005f62..79c17f84 100644 --- a/tests/assets/list_assets_test.py +++ b/tests/assets/list_assets_test.py @@ -1,9 +1,6 @@ import pytest -from pieces.app import main -from tests.utils import capture_stderr, restore_stderr, mock_select_menus, SCRIPT_NAME from unittest.mock import patch, Mock from pieces.settings import Settings -import sys from pieces.core.assets_command import AssetsCommands from pieces.core.list_command import ListCommand @@ -11,31 +8,34 @@ OPEN_MODULE_NAME = "pieces.core.assets_command" -@pytest.fixture -def mock_sys_argv_drive(): - with patch("sys.argv", [SCRIPT_NAME, "drive"]): - yield +@patch("pieces.utils.PiecesSelectMenu") +@patch(f"{MODULE_NAME}.BasicAsset") +@patch(f"{OPEN_MODULE_NAME}.Settings.pieces_client.assets") +def test_list_assets_builds_menu_entries_from_default_asset_lookup( + mock_assets_api, mock_basic_asset, mock_menu_cls, mock_assets +): + mock_assets_api.return_value = mock_assets + mock_basic_asset.get_identifiers.return_value = [ + Mock(id=asset.id) for asset in mock_assets + ] + mock_basic_asset.side_effect = lambda asset_id: next( + asset for asset in mock_assets if asset.id == asset_id + ) + ListCommand.list_assets() -def test_list_assets(mock_api_client, mock_assets, mock_settings, mock_sys_argv_drive): - def main_func(): - with ( - patch(f"{MODULE_NAME}.Settings.pieces_client", mock_api_client), - patch(f"{MODULE_NAME}.BasicAsset") as mock_basic_asset, - ): - mock_basic_asset.get_identifiers.return_value = mock_assets - main() - Settings.logger.debug(mock_api_client) + mock_menu_cls.assert_called_once() + menu_entries, on_enter, footer = mock_menu_cls.call_args.args - expected_assets = [ - (f"{i}: {asset.name}", {"ITEM_INDEX": i, "show_warning": False}) - for i, asset in enumerate(mock_assets, start=1) + assert footer is None + assert on_enter == AssetsCommands.open_asset + assert [label for label, _ in menu_entries] == [ + f"{index}: {asset.name}" for index, asset in enumerate(mock_assets, start=1) ] - - # Call the mock select menus - mock_select_menus( - main_func, "pieces.utils", [], None, AssetsCommands.open_asset, expected_assets - ) + assert [payload["asset_id"] for _, payload in menu_entries] == [ + asset.id for asset in mock_assets + ] + mock_menu_cls.return_value.run.assert_called_once() @patch("shutil.which") @@ -92,3 +92,17 @@ def test_list_assets_builds_menu_entries_with_asset_ids( ] mock_menu_cls.return_value.run.assert_called_once() mock_menu_cls.return_value.add_entry.assert_not_called() + + +@patch.object(Settings, "logger") +@patch("pieces.utils.PiecesSelectMenu") +@patch(f"{OPEN_MODULE_NAME}.Settings.pieces_client.assets") +def test_list_assets_skips_menu_when_no_entries_can_be_built( + mock_assets_api, mock_menu_cls, mock_logger +): + mock_assets_api.return_value = [object()] + + ListCommand.list_assets(assets=[object()]) + + mock_menu_cls.assert_not_called() + mock_logger.print.assert_called_once_with("No materials available to select.")