diff --git a/src/strands/tools/loader.py b/src/strands/tools/loader.py index 5935077db..2f601866d 100644 --- a/src/strands/tools/loader.py +++ b/src/strands/tools/loader.py @@ -4,7 +4,6 @@ import logging import os import sys -import warnings from pathlib import Path from typing import List, cast @@ -99,45 +98,6 @@ def load_python_tools(tool_path: str, tool_name: str) -> List[AgentTool]: logger.exception("tool_name=<%s>, sys_path=<%s> | failed to load python tool(s)", tool_name, sys.path) raise - @staticmethod - def load_python_tool(tool_path: str, tool_name: str) -> AgentTool: - """DEPRECATED: Load a Python tool module and return a single AgentTool for backwards compatibility. - - Use `load_python_tools` to retrieve all tools defined in a .py file (returns a list). - This function will emit a `DeprecationWarning` and return the first discovered tool. - """ - warnings.warn( - "ToolLoader.load_python_tool is deprecated and will be removed in Strands SDK 2.0. " - "Use ToolLoader.load_python_tools(...) which always returns a list of AgentTool.", - DeprecationWarning, - stacklevel=2, - ) - - tools = ToolLoader.load_python_tools(tool_path, tool_name) - if not tools: - raise RuntimeError(f"No tools found in {tool_path} for {tool_name}") - return tools[0] - - @classmethod - def load_tool(cls, tool_path: str, tool_name: str) -> AgentTool: - """DEPRECATED: Load a single tool based on its file extension for backwards compatibility. - - Use `load_tools` to retrieve all tools defined in a file (returns a list). - This function will emit a `DeprecationWarning` and return the first discovered tool. - """ - warnings.warn( - "ToolLoader.load_tool is deprecated and will be removed in Strands SDK 2.0. " - "Use ToolLoader.load_tools(...) which always returns a list of AgentTool.", - DeprecationWarning, - stacklevel=2, - ) - - tools = ToolLoader.load_tools(tool_path, tool_name) - if not tools: - raise RuntimeError(f"No tools found in {tool_path} for {tool_name}") - - return tools[0] - @classmethod def load_tools(cls, tool_path: str, tool_name: str) -> list[AgentTool]: """Load tools from a file based on its file extension. diff --git a/tests/strands/tools/test_loader.py b/tests/strands/tools/test_loader.py index 6b86d00ee..56914979d 100644 --- a/tests/strands/tools/test_loader.py +++ b/tests/strands/tools/test_loader.py @@ -32,220 +32,6 @@ def tool_module(tool_path): return ".".join(os.path.splitext(tool_path)[0].split(os.sep)[-2:]) -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - import strands - - @strands.tools.tool - def identity(a: int): - return a - """) - ], - indirect=True, -) -def test_load_python_tool_path_function_based(tool_path): - tool = ToolLoader.load_python_tool(tool_path, "identity") - - assert isinstance(tool, DecoratedFunctionTool) - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - TOOL_SPEC = { - "name": "identity", - "description": "identity tool", - "inputSchema": { - "type": "object", - "properties": { - "a": { - "type": "integer", - }, - }, - }, - } - - def identity(a: int): - return a - """) - ], - indirect=True, -) -def test_load_python_tool_path_module_based(tool_path): - tool = ToolLoader.load_python_tool(tool_path, "identity") - - assert isinstance(tool, PythonAgentTool) - - -def test_load_python_tool_path_invalid(): - with pytest.raises(ImportError, match="Could not create spec for identity"): - ToolLoader.load_python_tool("invalid", "identity") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - def no_spec(): - return - """) - ], - indirect=True, -) -def test_load_python_tool_path_no_spec(tool_path): - with pytest.raises(AttributeError, match="Tool no_spec missing TOOL_SPEC"): - ToolLoader.load_python_tool(tool_path, "no_spec") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - TOOL_SPEC = {"name": "no_function"} - """) - ], - indirect=True, -) -def test_load_python_tool_path_no_function(tool_path): - with pytest.raises(AttributeError, match="Tool no_function missing function"): - ToolLoader.load_python_tool(tool_path, "no_function") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - TOOL_SPEC = {"name": "no_callable"} - - no_callable = "not callable" - """) - ], - indirect=True, -) -def test_load_python_tool_path_no_callable(tool_path): - with pytest.raises(TypeError, match="Tool no_callable function is not callable"): - ToolLoader.load_python_tool(tool_path, "no_callable") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - import strands - - @strands.tools.tool - def identity(a: int): - return a - """) - ], - indirect=True, -) -def test_load_python_tool_dot_function_based(tool_path, tool_module): - _ = tool_path - tool_module = f"{tool_module}:identity" - - tool = ToolLoader.load_python_tool(tool_module, "identity") - - assert isinstance(tool, DecoratedFunctionTool) - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - TOOL_SPEC = {"name": "no_function"} - """) - ], - indirect=True, -) -def test_load_python_tool_dot_no_function(tool_path, tool_module): - _ = tool_path - - with pytest.raises(AttributeError, match=re.escape(f"Module {tool_module} has no function named no_function")): - ToolLoader.load_python_tool(f"{tool_module}:no_function", "no_function") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - def no_decorator(): - return - """) - ], - indirect=True, -) -def test_load_python_tool_dot_no_decorator(tool_path, tool_module): - _ = tool_path - - with pytest.raises(ValueError, match=re.escape(f"Function no_decorator in {tool_module} is not a valid tool")): - ToolLoader.load_python_tool(f"{tool_module}:no_decorator", "no_decorator") - - -def test_load_python_tool_dot_missing(): - with pytest.raises(ImportError, match="Failed to import module missing"): - ToolLoader.load_python_tool("missing:function", "function") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - import strands - - @strands.tools.tool - def identity(a: int): - return a - """) - ], - indirect=True, -) -def test_load_tool(tool_path): - tool = ToolLoader.load_tool(tool_path, "identity") - - assert isinstance(tool, DecoratedFunctionTool) - - -def test_load_tool_missing(): - with pytest.raises(FileNotFoundError, match="Tool file not found"): - ToolLoader.load_tool("missing", "function") - - -def test_load_tool_invalid_ext(tmp_path): - tool_path = tmp_path / "tool.txt" - tool_path.touch() - - with pytest.raises(ValueError, match="Unsupported tool file type: .txt"): - ToolLoader.load_tool(str(tool_path), "function") - - -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent(""" - def no_spec(): - return - """) - ], - indirect=True, -) -def test_load_tool_no_spec(tool_path): - with pytest.raises(AttributeError, match="Tool no_spec missing TOOL_SPEC"): - ToolLoader.load_tool(tool_path, "no_spec") - - with pytest.raises(AttributeError, match="Tool no_spec missing TOOL_SPEC"): - ToolLoader.load_tools(tool_path, "no_spec") - - with pytest.raises(AttributeError, match="Tool no_spec missing TOOL_SPEC"): - ToolLoader.load_python_tool(tool_path, "no_spec") - - with pytest.raises(AttributeError, match="Tool no_spec missing TOOL_SPEC"): - ToolLoader.load_python_tools(tool_path, "no_spec") - - @pytest.mark.parametrize( "tool_path", [ @@ -284,29 +70,3 @@ def test_load_python_tool_path_multiple_function_based(tool_path): assert names == {"alpha", "bravo"} -@pytest.mark.parametrize( - "tool_path", - [ - textwrap.dedent( - """ - import strands - - @strands.tools.tool - def alpha(): - return "alpha" - - @strands.tools.tool - def bravo(): - return "bravo" - """ - ) - ], - indirect=True, -) -def test_load_tool_path_returns_single_tool(tool_path): - # loaded_python_tool and loaded_tool returns single item - loaded_python_tool = ToolLoader.load_python_tool(tool_path, "alpha") - loaded_tool = ToolLoader.load_tool(tool_path, "alpha") - - assert loaded_python_tool.tool_name == "alpha" - assert loaded_tool.tool_name == "alpha"