Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions src/strands/tools/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import logging
import os
import sys
import warnings
from pathlib import Path
from typing import List, cast

Expand Down Expand Up @@ -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.
Expand Down
240 changes: 0 additions & 240 deletions tests/strands/tools/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down Expand Up @@ -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"