From 2ff07d11da2bba90d4e686daaf49ee6dc91b6549 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 2 May 2025 22:08:16 +0530 Subject: [PATCH 1/4] feat: Add bind_param (singular) to align with other packages --- .../src/toolbox_core/sync_tool.py | 28 +++++++++++++++---- .../toolbox-core/src/toolbox_core/tool.py | 28 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 33177b11..dc8d3218 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -159,13 +159,31 @@ def bind_params( """ Binds parameters to values or callables that produce values. - Args: - bound_params: A mapping of parameter names to values or callables that - produce values. + Args: + bound_params: A mapping of parameter names to values or callables that + produce values. - Returns: - A new ToolboxSyncTool instance with the specified parameters bound. + Returns: + A new ToolboxSyncTool instance with the specified parameters bound. """ new_async_tool = self.__async_tool.bind_params(bound_params) return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) + + def bind_param( + self, + param_name: str, + param_value: Union[Callable[[], Any], Any], + ) -> "ToolboxTool": + """ + Binds a parameter to the value or callables that produce it. + + Args: + param_name: The name of the bound parameter. + param_value: The value of the bound parameter, or a callable that + returns the value. + + Returns: + A new ToolboxSyncTool instance with the specified parameter bound. + """ + return self.bind_params({param_name: param_value}) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index def47c2d..19b6d710 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -307,12 +307,12 @@ def bind_params( """ Binds parameters to values or callables that produce values. - Args: - bound_params: A mapping of parameter names to values or callables that - produce values. + Args: + bound_params: A mapping of parameter names to values or callables that + produce values. - Returns: - A new ToolboxTool instance with the specified parameters bound. + Returns: + A new ToolboxTool instance with the specified parameters bound. """ param_names = set(p.name for p in self.__params) for name in bound_params.keys(): @@ -335,3 +335,21 @@ def bind_params( params=new_params, bound_params=MappingProxyType(all_bound_params), ) + + def bind_param( + self, + param_name: str, + param_value: Union[Callable[[], Any], Any], + ) -> "ToolboxTool": + """ + Binds a parameter to the value or callables that produce it. + + Args: + param_name: The name of the bound parameter. + param_value: The value of the bound parameter, or a callable that + returns the value. + + Returns: + A new ToolboxTool instance with the specified parameters bound. + """ + return self.bind_params({param_name: param_value}) From 045503d1f2c41f4051cfbf2bc62d64ef6d3ce270 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 2 May 2025 22:08:27 +0530 Subject: [PATCH 2/4] chore: Add unit test coverage. --- packages/toolbox-core/tests/test_client.py | 150 +++++++++++++++++++-- 1 file changed, 141 insertions(+), 9 deletions(-) diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index 7274cc0d..6bc9b739 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -435,8 +435,8 @@ async def test_load_toolset_success(self, tool_name, client): assert "argB" in res @pytest.mark.asyncio - async def test_bind_param_success(self, tool_name, client): - """Tests 'bind_param' with a bound parameter specified.""" + async def test_bind_params_success(self, tool_name, client): + """Tests 'bind_params' with a bound parameter specified.""" tool = await client.load_tool(tool_name) assert len(tool.__signature__.parameters) == 2 @@ -451,8 +451,8 @@ async def test_bind_param_success(self, tool_name, client): assert "argA" in res @pytest.mark.asyncio - async def test_bind_callable_param_success(self, tool_name, client): - """Tests 'bind_param' with a bound parameter specified.""" + async def test_bind_callable_params_success(self, tool_name, client): + """Tests 'bind_params' with a bound parameter specified.""" tool = await client.load_tool(tool_name) assert len(tool.__signature__.parameters) == 2 @@ -467,7 +467,7 @@ async def test_bind_callable_param_success(self, tool_name, client): assert "argA" in res @pytest.mark.asyncio - async def test_bind_param_fail(self, tool_name, client): + async def test_bind_params_fail(self, tool_name, client): """Tests 'bind_params' with a bound parameter that doesn't exist.""" tool = await client.load_tool(tool_name) @@ -479,7 +479,7 @@ async def test_bind_param_fail(self, tool_name, client): assert "unable to bind parameters: no parameter named argC" in str(e.value) @pytest.mark.asyncio - async def test_rebind_param_fail(self, tool_name, client): + async def test_rebind_params_fail(self, tool_name, client): """ Tests that 'bind_params' fails when attempting to re-bind a parameter that has already been bound. @@ -502,7 +502,7 @@ async def test_rebind_param_fail(self, tool_name, client): ) @pytest.mark.asyncio - async def test_bind_param_static_value_success(self, tool_name, client): + async def test_bind_params_static_value_success(self, tool_name, client): """ Tests bind_params method with a static value. """ @@ -522,7 +522,7 @@ async def test_bind_param_static_value_success(self, tool_name, client): assert res_payload == {"argA": passed_value_a, "argB": bound_value} @pytest.mark.asyncio - async def test_bind_param_sync_callable_value_success(self, tool_name, client): + async def test_bind_params_sync_callable_value_success(self, tool_name, client): """ Tests bind_params method with a sync callable value. """ @@ -544,7 +544,7 @@ async def test_bind_param_sync_callable_value_success(self, tool_name, client): bound_sync_callable.assert_called_once() @pytest.mark.asyncio - async def test_bind_param_async_callable_value_success(self, tool_name, client): + async def test_bind_params_async_callable_value_success(self, tool_name, client): """ Tests bind_params method with an async callable value. """ @@ -566,6 +566,138 @@ async def test_bind_param_async_callable_value_success(self, tool_name, client): bound_async_callable.assert_awaited_once() + @pytest.mark.asyncio + async def test_bind_param_success(self, tool_name, client): + """Tests 'bind_param' with a bound parameter specified.""" + tool = await client.load_tool(tool_name) + + assert len(tool.__signature__.parameters) == 2 + assert "argA" in tool.__signature__.parameters + + tool = tool.bind_param("argA", 5) + + assert len(tool.__signature__.parameters) == 1 + assert "argA" not in tool.__signature__.parameters + + res = await tool(True) + assert "argA" in res + + @pytest.mark.asyncio + async def test_bind_callable_param_success(self, tool_name, client): + """Tests 'bind_param' with a bound parameter specified.""" + tool = await client.load_tool(tool_name) + + assert len(tool.__signature__.parameters) == 2 + assert "argA" in tool.__signature__.parameters + + tool = tool.bind_param("argA", lambda: 5) + + assert len(tool.__signature__.parameters) == 1 + assert "argA" not in tool.__signature__.parameters + + res = await tool(True) + assert "argA" in res + + @pytest.mark.asyncio + async def test_bind_param_fail(self, tool_name, client): + """Tests 'bind_param' with a bound parameter that doesn't exist.""" + tool = await client.load_tool(tool_name) + + assert len(tool.__signature__.parameters) == 2 + assert "argA" in tool.__signature__.parameters + + with pytest.raises(Exception) as e: + tool.bind_param("argC", lambda: 5) + assert "unable to bind parameters: no parameter named argC" in str(e.value) + + @pytest.mark.asyncio + async def test_rebind_param_fail(self, tool_name, client): + """ + Tests that 'bind_param' fails when attempting to re-bind a + parameter that has already been bound. + """ + tool = await client.load_tool(tool_name) + + assert len(tool.__signature__.parameters) == 2 + assert "argA" in tool.__signature__.parameters + + tool_with_bound_param = tool.bind_param("argA", lambda: 10) + + assert len(tool_with_bound_param.__signature__.parameters) == 1 + assert "argA" not in tool_with_bound_param.__signature__.parameters + + with pytest.raises(ValueError) as e: + tool_with_bound_param.bind_param("argA", lambda: 20) + + assert "cannot re-bind parameter: parameter 'argA' is already bound" in str( + e.value + ) + + @pytest.mark.asyncio + async def test_bind_param_static_value_success(self, tool_name, client): + """ + Tests bind_param method with a static value. + """ + + bound_value = "Test value" + + tool = await client.load_tool(tool_name) + bound_tool = tool.bind_param("argB", bound_value) + + assert bound_tool is not tool + assert "argB" not in bound_tool.__signature__.parameters + assert "argA" in bound_tool.__signature__.parameters + + passed_value_a = 42 + res_payload = await bound_tool(argA=passed_value_a) + + assert res_payload == {"argA": passed_value_a, "argB": bound_value} + + @pytest.mark.asyncio + async def test_bind_param_sync_callable_value_success(self, tool_name, client): + """ + Tests bind_param method with a sync callable value. + """ + + bound_value_result = True + bound_sync_callable = Mock(return_value=bound_value_result) + + tool = await client.load_tool(tool_name) + bound_tool = tool.bind_param("argB", bound_sync_callable) + + assert bound_tool is not tool + assert "argB" not in bound_tool.__signature__.parameters + assert "argA" in bound_tool.__signature__.parameters + + passed_value_a = 42 + res_payload = await bound_tool(argA=passed_value_a) + + assert res_payload == {"argA": passed_value_a, "argB": bound_value_result} + bound_sync_callable.assert_called_once() + + @pytest.mark.asyncio + async def test_bind_param_async_callable_value_success(self, tool_name, client): + """ + Tests bind_param method with an async callable value. + """ + + bound_value_result = True + bound_async_callable = AsyncMock(return_value=bound_value_result) + + tool = await client.load_tool(tool_name) + bound_tool = tool.bind_param("argB", bound_async_callable) + + assert bound_tool is not tool + assert "argB" not in bound_tool.__signature__.parameters + assert "argA" in bound_tool.__signature__.parameters + + passed_value_a = 42 + res_payload = await bound_tool(argA=passed_value_a) + + assert res_payload == {"argA": passed_value_a, "argB": bound_value_result} + bound_async_callable.assert_awaited_once() + + class TestUnusedParameterValidation: """ Tests for validation errors related to unused auth tokens or bound From 6ffe15a5df48b13af5a35237937616f333baeec9 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 2 May 2025 22:11:37 +0530 Subject: [PATCH 3/4] chore: Delint --- packages/toolbox-core/tests/test_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index 6bc9b739..d510d73f 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -565,7 +565,6 @@ async def test_bind_params_async_callable_value_success(self, tool_name, client) assert res_payload == {"argA": passed_value_a, "argB": bound_value_result} bound_async_callable.assert_awaited_once() - @pytest.mark.asyncio async def test_bind_param_success(self, tool_name, client): """Tests 'bind_param' with a bound parameter specified.""" From abd8e37ede14900e53dc862473b235c0f91c075d Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Mon, 5 May 2025 11:35:37 +0530 Subject: [PATCH 4/4] chore: Delint --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index dc8d3218..27302251 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -174,7 +174,7 @@ def bind_param( self, param_name: str, param_value: Union[Callable[[], Any], Any], - ) -> "ToolboxTool": + ) -> "ToolboxSyncTool": """ Binds a parameter to the value or callables that produce it.