Remove arcade_ prefix requirement and add entry point toolkit discovery (#485)
## Summary This PR removes the requirement that all toolkits must have the arcade_ prefix and introduces a more flexible toolkit discovery system using Python entry points. ### 🏷️ Flexible Toolkit Naming * Community toolkits: Only add arcade_ prefix when the user is in arcade-ai/toolkits/ directory and explicitly chooses to create a community contribution. * External toolkits: No prefix requirement - developers can name their toolkits however they want * Toolkit names are now determined by user choice rather than enforced automatically ### 🔍 Entry Point Discovery * Added find_arcade_toolkits_from_entrypoints() method to discover toolkits via entry points * Entry point group: arcade_toolkits with name: toolkit_name * Updated pyproject.toml template to include entry point configuration * Entry point discovery takes precedence over prefix-based discovery for deduplication ### 📦 Backward Compatibility * Existing arcade_* prefixed toolkits continue to work via find_arcade_toolkits_from_prefix() find_all_arcade_toolkits() now combines both discovery methods * Deduplication logic prefers entry point toolkits over prefix-based ones when package names match ### 🛠️ `arcade new` Template Updates * pyproject.toml template for `arcade new` now includes entry point configuration: [project.entry-points.arcade_toolkits] ### 🔧 Minor Improvements * Refactored _strip_arcade_prefix() into a separate method for reusability * Updated variable naming for clarity (community_toolkit → is_community_toolkit) ### Benefits * Developer Freedom: Toolkit developers are no longer forced to use the arcade_ prefix. They are also no longer forced to use the package name as the toolkit name. * Cleaner Naming: External toolkits can use more natural names (e.g., my_company_toolkit instead of arcade_my_company_toolkit) * Better Discovery: Entry points provide a more standard Python mechanism for plugin discovery * Flexible Distribution: Toolkits can be distributed with any package name while still being discoverable ### Testing * Added comprehensive tests for the new entry point functionality * Tests cover edge cases like deduplication, error handling, and backward compatibility ### Version Bumps arcade-core: 2.0.0 → 2.1.0 arcade-ai: 2.0.5 → 2.1.0 This change makes the Arcade toolkit ecosystem more flexible and developer-friendly while maintaining full backward compatibility with existing toolkits. --------- Co-authored-by: Mateo Torres <mateo@arcade.dev>
This commit is contained in:
parent
32292d4b39
commit
856606f38c
6 changed files with 465 additions and 26 deletions
|
|
@ -147,12 +147,8 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
"""Create a new toolkit from a template with user input."""
|
||||
toolkit_directory = Path(output_directory)
|
||||
|
||||
package_name = toolkit_name if toolkit_name.startswith("arcade_") else f"arcade_{toolkit_name}"
|
||||
|
||||
# Check for illegal characters in the toolkit name
|
||||
if re.match(r"^[a-z0-9_]+$", package_name):
|
||||
toolkit_name = package_name.replace("arcade_", "", 1)
|
||||
|
||||
if re.match(r"^[a-z0-9_]+$", toolkit_name):
|
||||
if (toolkit_directory / toolkit_name).exists():
|
||||
console.print(f"[red]Toolkit '{toolkit_name}' already exists.[/red]")
|
||||
exit(1)
|
||||
|
|
@ -180,15 +176,16 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
cwd = Path.cwd()
|
||||
# TODO: this detection mechanism works only for people that didn't change the
|
||||
# name of the repo, a better detection method is required here
|
||||
community_toolkit = False
|
||||
is_community_toolkit = False
|
||||
if cwd.name == "toolkits" and cwd.parent.name == "arcade-ai":
|
||||
community_toolkit = ask_yes_no_question(
|
||||
"Is your toolkit a community contribution (to be merged into Arcade's `arcade-ai` repo)?",
|
||||
default=False, # False for now to keep the default behavior
|
||||
prompt = (
|
||||
"Is your toolkit a community contribution (to be merged into "
|
||||
"\x1b]8;;https://github.com/ArcadeAI/arcade-ai\x1b\\ArcadeAI/arcade-ai\x1b]8;;\x1b\\ repo)?"
|
||||
)
|
||||
is_community_toolkit = ask_yes_no_question(prompt, default=False)
|
||||
|
||||
context = {
|
||||
"package_name": package_name,
|
||||
"package_name": "arcade_" + toolkit_name if is_community_toolkit else toolkit_name,
|
||||
"toolkit_name": toolkit_name,
|
||||
"toolkit_description": toolkit_description,
|
||||
"toolkit_author_name": toolkit_author_name,
|
||||
|
|
@ -200,7 +197,7 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
"arcade_ai_min_version": ARCADE_AI_MIN_VERSION,
|
||||
"arcade_ai_max_version": ARCADE_AI_MAX_VERSION,
|
||||
"creation_year": datetime.now().year,
|
||||
"community_toolkit": community_toolkit,
|
||||
"is_community_toolkit": is_community_toolkit,
|
||||
}
|
||||
template_directory = Path(__file__).parent / "templates" / "{{ toolkit_name }}"
|
||||
|
||||
|
|
@ -210,7 +207,7 @@ def create_new_toolkit(output_directory: str, toolkit_name: str) -> None:
|
|||
)
|
||||
|
||||
# Create dynamic ignore pattern based on user preferences
|
||||
ignore_pattern = create_ignore_pattern(include_evals, community_toolkit)
|
||||
ignore_pattern = create_ignore_pattern(include_evals, is_community_toolkit)
|
||||
|
||||
try:
|
||||
create_package(env, template_directory, toolkit_directory, context, ignore_pattern)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ dev = [
|
|||
"ruff>=0.7.4,<0.8.0",
|
||||
]
|
||||
|
||||
# Tell Arcade.dev that this package is a toolkit
|
||||
[project.entry-points.arcade_toolkits]
|
||||
toolkit_name = "{{ package_name }}"
|
||||
|
||||
# Use local path sources for arcade libs when working locally
|
||||
[tool.uv.sources]
|
||||
arcade-ai = { path = "../../", editable = true }
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ class Toolkit(BaseModel):
|
|||
"""
|
||||
Validator to strip the 'arcade_' prefix from the name if it exists.
|
||||
"""
|
||||
return cls._strip_arcade_prefix(value)
|
||||
|
||||
@classmethod
|
||||
def _strip_arcade_prefix(cls, value: str) -> str:
|
||||
"""
|
||||
Strip the 'arcade_' prefix from the name if it exists.
|
||||
"""
|
||||
if value.startswith("arcade_"):
|
||||
return value[len("arcade_") :]
|
||||
return value
|
||||
|
|
@ -68,24 +75,24 @@ class Toolkit(BaseModel):
|
|||
repo = metadata.get("Repository", None) # type: ignore[attr-defined]
|
||||
|
||||
except importlib.metadata.PackageNotFoundError as e:
|
||||
raise ToolkitLoadError(f"Package {package} not found.") from e
|
||||
raise ToolkitLoadError(f"Package '{package}' not found.") from e
|
||||
except KeyError as e:
|
||||
raise ToolkitLoadError(f"Metadata key error for package {package}.") from e
|
||||
raise ToolkitLoadError(f"Metadata key error for package '{package}'.") from e
|
||||
except Exception as e:
|
||||
raise ToolkitLoadError(f"Failed to load metadata for package {package}.") from e
|
||||
raise ToolkitLoadError(f"Failed to load metadata for package '{package}'.") from e
|
||||
|
||||
# Get the package directory
|
||||
try:
|
||||
package_dir = Path(get_package_directory(package))
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise ToolkitLoadError(f"Failed to locate package directory for {package}.") from e
|
||||
raise ToolkitLoadError(f"Failed to locate package directory for '{package}'.") from e
|
||||
|
||||
# Get all python files in the package directory
|
||||
try:
|
||||
modules = [f for f in package_dir.glob("**/*.py") if f.is_file()]
|
||||
except OSError as e:
|
||||
raise ToolkitLoadError(
|
||||
f"Failed to locate Python files in package directory for {package}."
|
||||
f"Failed to locate Python files in package directory for '{package}'."
|
||||
) from e
|
||||
|
||||
toolkit = cls(
|
||||
|
|
@ -110,31 +117,118 @@ class Toolkit(BaseModel):
|
|||
return toolkit
|
||||
|
||||
@classmethod
|
||||
def find_all_arcade_toolkits(cls) -> list["Toolkit"]:
|
||||
def from_entrypoint(cls, entry: importlib.metadata.EntryPoint) -> "Toolkit":
|
||||
"""
|
||||
Find all installed packages prefixed with 'arcade_' in the current
|
||||
Python interpreter's environment and load them as Toolkits.
|
||||
Load a Toolkit from an entrypoint.
|
||||
|
||||
The entrypoint value is used as the toolkit name, while the package name
|
||||
is extracted from the distribution that owns the entrypoint.
|
||||
|
||||
Args:
|
||||
entry: The EntryPoint object from importlib.metadata
|
||||
|
||||
Returns:
|
||||
List[Toolkit]: A list of Toolkit instances.
|
||||
A Toolkit instance
|
||||
|
||||
Raises:
|
||||
ToolkitLoadError: If the toolkit cannot be loaded
|
||||
"""
|
||||
# Get the package name from the distribution that owns this entrypoint
|
||||
if not hasattr(entry, "dist") or entry.dist is None:
|
||||
raise ToolkitLoadError(
|
||||
f"Entry point '{entry.name}' does not have distribution metadata. "
|
||||
f"This may indicate an incomplete package installation."
|
||||
)
|
||||
|
||||
package_name = entry.dist.name
|
||||
|
||||
toolkit = cls.from_package(package_name)
|
||||
toolkit.name = cls._strip_arcade_prefix(entry.value)
|
||||
|
||||
return toolkit
|
||||
|
||||
@classmethod
|
||||
def find_arcade_toolkits_from_entrypoints(cls) -> list["Toolkit"]:
|
||||
"""
|
||||
Find and load as Toolkits all installed packages in the
|
||||
current Python interpreter's environment that have a
|
||||
registered entrypoint under the 'arcade.toolkits' group.
|
||||
"""
|
||||
toolkits = []
|
||||
toolkit_entries: list[importlib.metadata.EntryPoint] = []
|
||||
|
||||
try:
|
||||
toolkit_entries = importlib.metadata.entry_points(
|
||||
group="arcade_toolkits", name="toolkit_name"
|
||||
)
|
||||
for entry in toolkit_entries:
|
||||
try:
|
||||
toolkit = cls.from_entrypoint(entry)
|
||||
toolkits.append(toolkit)
|
||||
logger.debug(
|
||||
f"Loaded toolkit from entry point: {entry.name} = '{toolkit.name}'"
|
||||
)
|
||||
except ToolkitLoadError as e:
|
||||
logger.warning(
|
||||
f"Warning: {e} Skipping toolkit from entry point '{entry.value}'"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Entry point discovery failed or not available: {e}")
|
||||
|
||||
return toolkits
|
||||
|
||||
@classmethod
|
||||
def find_arcade_toolkits_from_prefix(cls) -> list["Toolkit"]:
|
||||
"""
|
||||
Find and load as Toolkits all installed packages in the
|
||||
current Python interpreter's environment that are prefixed with 'arcade_'.
|
||||
"""
|
||||
import sysconfig
|
||||
|
||||
# Get the site-packages directory of the current interpreter
|
||||
toolkits = []
|
||||
site_packages_dir = sysconfig.get_paths()["purelib"]
|
||||
|
||||
arcade_packages = [
|
||||
dist.metadata["Name"]
|
||||
for dist in importlib.metadata.distributions(path=[site_packages_dir])
|
||||
if dist.metadata["Name"].startswith("arcade_")
|
||||
]
|
||||
toolkits = []
|
||||
|
||||
for package in arcade_packages:
|
||||
try:
|
||||
toolkits.append(cls.from_package(package))
|
||||
toolkit = cls.from_package(package)
|
||||
toolkits.append(toolkit)
|
||||
logger.debug(f"Loaded toolkit from prefix discovery: {package}")
|
||||
except ToolkitLoadError as e:
|
||||
logger.warning(f"Warning: {e} Skipping toolkit {package}")
|
||||
|
||||
return toolkits
|
||||
|
||||
@classmethod
|
||||
def find_all_arcade_toolkits(cls) -> list["Toolkit"]:
|
||||
"""
|
||||
Find and load as Toolkits all installed packages in the
|
||||
current Python interpreter's environment that either
|
||||
1. Have a registered entrypoint under the 'arcade.toolkits' group, or
|
||||
2. Are prefixed with 'arcade_'
|
||||
|
||||
Returns:
|
||||
List[Toolkit]: A list of Toolkit instances.
|
||||
"""
|
||||
# Find toolkits
|
||||
entrypoint_toolkits = cls.find_arcade_toolkits_from_entrypoints()
|
||||
prefix_toolkits = cls.find_arcade_toolkits_from_prefix()
|
||||
|
||||
# Deduplicate. Entrypoints are preferred over prefix-based toolkits.
|
||||
seen_package_names = set()
|
||||
all_toolkits = []
|
||||
for toolkit in entrypoint_toolkits + prefix_toolkits:
|
||||
if toolkit.package_name not in seen_package_names:
|
||||
all_toolkits.append(toolkit)
|
||||
seen_package_names.add(toolkit.package_name)
|
||||
|
||||
return all_toolkits
|
||||
|
||||
|
||||
def get_package_directory(package_name: str) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "arcade-core"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
description = "Arcade Core - Core library for Arcade platform"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
|
|
|||
344
libs/tests/core/test_toolkit.py
Normal file
344
libs/tests/core/test_toolkit.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from arcade_core.errors import ToolkitLoadError
|
||||
from arcade_core.toolkit import Toolkit
|
||||
|
||||
|
||||
class TestToolkit:
|
||||
"""Test the Toolkit class functionality."""
|
||||
|
||||
def test_strip_arcade_prefix_validator(self):
|
||||
"""Test that the name validator strips the arcade_ prefix."""
|
||||
toolkit = Toolkit(
|
||||
name="arcade_test",
|
||||
package_name="arcade_test",
|
||||
description="Test toolkit",
|
||||
version="1.0.0",
|
||||
)
|
||||
assert toolkit.name == "test"
|
||||
|
||||
def test_no_arcade_prefix_unchanged(self):
|
||||
"""Test that names without arcade_ prefix remain unchanged."""
|
||||
toolkit = Toolkit(
|
||||
name="mytest",
|
||||
package_name="mytest",
|
||||
description="Test toolkit",
|
||||
version="1.0.0",
|
||||
)
|
||||
assert toolkit.name == "mytest"
|
||||
|
||||
def test_strip_arcade_prefix_method(self):
|
||||
"""Test the _strip_arcade_prefix static method."""
|
||||
assert Toolkit._strip_arcade_prefix("arcade_test") == "test"
|
||||
assert Toolkit._strip_arcade_prefix("test") == "test"
|
||||
assert Toolkit._strip_arcade_prefix("arcade_my_toolkit") == "my_toolkit"
|
||||
assert Toolkit._strip_arcade_prefix("myarcade_toolkit") == "myarcade_toolkit"
|
||||
assert Toolkit._strip_arcade_prefix("") == ""
|
||||
assert Toolkit._strip_arcade_prefix("arcade_") == ""
|
||||
|
||||
|
||||
class TestFromEntrypoint:
|
||||
"""Test the from_entrypoint class method."""
|
||||
|
||||
@patch("arcade_core.toolkit.Toolkit.from_package")
|
||||
def test_from_entrypoint_success(self, mock_from_package):
|
||||
"""Test successful creation of toolkit from entry point."""
|
||||
# Create mock entry point with dist
|
||||
mock_entry = MagicMock()
|
||||
mock_entry.value = "my_toolkit"
|
||||
mock_entry.name = "toolkit_name"
|
||||
mock_entry.dist = MagicMock()
|
||||
mock_entry.dist.name = "my-toolkit"
|
||||
|
||||
# Mock the from_package return
|
||||
mock_toolkit = Toolkit(
|
||||
name="my_toolkit",
|
||||
package_name="my-toolkit",
|
||||
version="1.2.3",
|
||||
description="My test toolkit",
|
||||
author=["Test Author"],
|
||||
homepage="https://github.com/test/toolkit",
|
||||
)
|
||||
mock_from_package.return_value = mock_toolkit
|
||||
|
||||
toolkit = Toolkit.from_entrypoint(mock_entry)
|
||||
|
||||
mock_from_package.assert_called_once_with("my-toolkit")
|
||||
assert toolkit.name == "my_toolkit"
|
||||
assert toolkit.package_name == "my-toolkit"
|
||||
|
||||
def test_from_entrypoint_no_dist(self):
|
||||
"""Test handling when entry point has no dist attribute."""
|
||||
# Create mock entry point without dist
|
||||
mock_entry = MagicMock()
|
||||
mock_entry.value = "my_toolkit"
|
||||
mock_entry.name = "toolkit_name"
|
||||
mock_entry.dist = None
|
||||
|
||||
with pytest.raises(ToolkitLoadError, match="does not have distribution metadata"):
|
||||
Toolkit.from_entrypoint(mock_entry)
|
||||
|
||||
|
||||
class TestFindArcadeToolkitsFromEntrypoints:
|
||||
"""Test the find_arcade_toolkits_from_entrypoints method."""
|
||||
|
||||
@patch("arcade_core.toolkit.importlib.metadata.entry_points")
|
||||
@patch("arcade_core.toolkit.Toolkit.from_entrypoint")
|
||||
def test_find_from_entrypoints_success(self, mock_from_ep, mock_entry_points):
|
||||
"""Test successful discovery of toolkits from entry points."""
|
||||
# Create mock entry points
|
||||
mock_ep1 = MagicMock()
|
||||
mock_ep1.name = "toolkit_name"
|
||||
mock_ep1.value = "toolkit1"
|
||||
mock_ep2 = MagicMock()
|
||||
mock_ep2.name = "toolkit_name"
|
||||
mock_ep2.value = "toolkit2"
|
||||
|
||||
mock_entry_points.return_value = [mock_ep1, mock_ep2]
|
||||
|
||||
# Create mock toolkits
|
||||
toolkit1 = Toolkit(
|
||||
name="toolkit1",
|
||||
package_name="toolkit1",
|
||||
version="1.0.0",
|
||||
description="Toolkit 1",
|
||||
)
|
||||
toolkit2 = Toolkit(
|
||||
name="toolkit2",
|
||||
package_name="toolkit2",
|
||||
version="1.0.0",
|
||||
description="Toolkit 2",
|
||||
)
|
||||
mock_from_ep.side_effect = [toolkit1, toolkit2]
|
||||
|
||||
toolkits = Toolkit.find_arcade_toolkits_from_entrypoints()
|
||||
|
||||
assert len(toolkits) == 2
|
||||
assert toolkits[0].name == "toolkit1"
|
||||
assert toolkits[1].name == "toolkit2"
|
||||
|
||||
@patch("arcade_core.toolkit.importlib.metadata.entry_points")
|
||||
@patch("arcade_core.toolkit.Toolkit.from_entrypoint")
|
||||
def test_find_from_entrypoints_with_errors(self, mock_from_ep, mock_entry_points):
|
||||
"""Test that errors in loading individual toolkits are handled gracefully."""
|
||||
# Create mock entry points
|
||||
mock_ep1 = MagicMock()
|
||||
mock_ep1.name = "toolkit_name"
|
||||
mock_ep1.value = "toolkit1"
|
||||
mock_ep2 = MagicMock()
|
||||
mock_ep2.name = "toolkit_name"
|
||||
mock_ep2.value = "toolkit2"
|
||||
mock_ep3 = MagicMock()
|
||||
mock_ep3.name = "toolkit_name"
|
||||
mock_ep3.value = "toolkit3"
|
||||
mock_entry_points.return_value = [mock_ep1, mock_ep2, mock_ep3]
|
||||
|
||||
# Create mock toolkits
|
||||
toolkit1 = Toolkit(
|
||||
name="toolkit1",
|
||||
package_name="toolkit1",
|
||||
version="1.0.0",
|
||||
description="Toolkit 1",
|
||||
)
|
||||
toolkit3 = Toolkit(
|
||||
name="toolkit3",
|
||||
package_name="toolkit3",
|
||||
version="1.0.0",
|
||||
description="Toolkit 3",
|
||||
)
|
||||
mock_from_ep.side_effect = [toolkit1, ToolkitLoadError("Failed to load toolkit2"), toolkit3]
|
||||
|
||||
toolkits = Toolkit.find_arcade_toolkits_from_entrypoints()
|
||||
|
||||
assert len(toolkits) == 2
|
||||
assert toolkits[0].name == "toolkit1"
|
||||
assert toolkits[1].name == "toolkit3"
|
||||
|
||||
@patch("arcade_core.toolkit.importlib.metadata.entry_points")
|
||||
def test_find_from_entrypoints_no_group(self, mock_entry_points):
|
||||
"""Test when arcade_toolkits entry point group doesn't exist."""
|
||||
# Mock entry_points to return empty list
|
||||
mock_entry_points.return_value = []
|
||||
|
||||
# Test
|
||||
toolkits = Toolkit.find_arcade_toolkits_from_entrypoints()
|
||||
|
||||
assert toolkits == []
|
||||
|
||||
|
||||
class TestFindAllArcadeToolkits:
|
||||
"""Test the combined toolkit discovery method."""
|
||||
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_entrypoints")
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_prefix")
|
||||
def test_find_all_no_duplicates(self, mock_find_prefix, mock_find_ep):
|
||||
"""Test that find_all returns combined results without duplicates."""
|
||||
# Create mock toolkits
|
||||
toolkit1 = Toolkit(
|
||||
name="toolkit1",
|
||||
package_name="toolkit1",
|
||||
version="1.0.0",
|
||||
description="Toolkit 1",
|
||||
)
|
||||
toolkit2 = Toolkit(
|
||||
name="toolkit2",
|
||||
package_name="toolkit2",
|
||||
version="1.0.0",
|
||||
description="Toolkit 2",
|
||||
)
|
||||
# Mock the discovery methods
|
||||
mock_find_ep.return_value = [toolkit1]
|
||||
mock_find_prefix.return_value = [toolkit2]
|
||||
|
||||
toolkits = Toolkit.find_all_arcade_toolkits()
|
||||
|
||||
assert len(toolkits) == 2
|
||||
assert any(t.name == "toolkit1" for t in toolkits)
|
||||
assert any(t.name == "toolkit2" for t in toolkits)
|
||||
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_entrypoints")
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_prefix")
|
||||
def test_find_all_with_duplicates_prefers_entrypoint(self, mock_find_prefix, mock_find_ep):
|
||||
"""Test that entry point toolkits are preferred over prefix-based ones."""
|
||||
toolkit_ep = Toolkit(
|
||||
name="test",
|
||||
package_name="arcade_test", # Same package name
|
||||
version="2.0.0",
|
||||
description="Entry point version",
|
||||
)
|
||||
|
||||
toolkit_prefix = Toolkit(
|
||||
name="test",
|
||||
package_name="arcade_test", # Same package name
|
||||
version="1.0.0",
|
||||
description="Prefix version",
|
||||
)
|
||||
# Mock the discovery methods
|
||||
mock_find_ep.return_value = [toolkit_ep]
|
||||
mock_find_prefix.return_value = [toolkit_prefix]
|
||||
|
||||
toolkits = Toolkit.find_all_arcade_toolkits()
|
||||
|
||||
assert len(toolkits) == 1
|
||||
assert toolkits[0].version == "2.0.0"
|
||||
assert toolkits[0].description == "Entry point version"
|
||||
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_entrypoints")
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_prefix")
|
||||
def test_find_all_empty(self, mock_find_prefix, mock_find_ep):
|
||||
"""Test when no toolkits are found."""
|
||||
mock_find_ep.return_value = []
|
||||
mock_find_prefix.return_value = []
|
||||
|
||||
toolkits = Toolkit.find_all_arcade_toolkits()
|
||||
assert toolkits == []
|
||||
|
||||
|
||||
class TestEntryPointCompatibility:
|
||||
"""Test compatibility scenarios for entry point discovery."""
|
||||
|
||||
@patch("arcade_core.toolkit.importlib.metadata.entry_points")
|
||||
@patch("arcade_core.toolkit.Toolkit.from_entrypoint")
|
||||
def test_duplicate_toolkit_names_in_entrypoints(self, mock_from_ep, mock_entry_points):
|
||||
"""Test handling of duplicate toolkit names in entry points."""
|
||||
# Create mock entry points with same name
|
||||
mock_ep1 = MagicMock()
|
||||
mock_ep1.name = "toolkit_name"
|
||||
mock_ep1.value = "test_v1"
|
||||
|
||||
mock_ep2 = MagicMock()
|
||||
mock_ep2.name = "toolkit_name"
|
||||
mock_ep2.value = "test_v2"
|
||||
|
||||
mock_entry_points.return_value = [mock_ep1, mock_ep2]
|
||||
|
||||
# Mock toolkit creation - both with same toolkit name
|
||||
toolkit1 = Toolkit(
|
||||
name="test",
|
||||
package_name="test_v1",
|
||||
version="1.0.0",
|
||||
description="Test v1",
|
||||
)
|
||||
|
||||
toolkit2 = Toolkit(
|
||||
name="test",
|
||||
package_name="test_v2",
|
||||
version="2.0.0",
|
||||
description="Test v2",
|
||||
)
|
||||
|
||||
mock_from_ep.side_effect = [toolkit1, toolkit2]
|
||||
|
||||
# Should return both even with same display name
|
||||
toolkits = Toolkit.find_arcade_toolkits_from_entrypoints()
|
||||
assert len(toolkits) == 2
|
||||
|
||||
@patch("arcade_core.toolkit.Toolkit.from_package")
|
||||
def test_from_entrypoint_with_arcade_prefix(self, mock_from_package):
|
||||
"""Test that arcade_ prefix is stripped from entry point toolkits."""
|
||||
# Create mock entry point
|
||||
mock_entry = MagicMock()
|
||||
mock_entry.value = "arcade_example"
|
||||
mock_entry.name = "toolkit_name"
|
||||
mock_entry.dist = MagicMock()
|
||||
mock_entry.dist.name = "arcade-example"
|
||||
|
||||
# Mock the from_package return
|
||||
mock_toolkit = Toolkit(
|
||||
name="arcade_example",
|
||||
package_name="arcade-example",
|
||||
version="1.0.0",
|
||||
description="Example toolkit",
|
||||
)
|
||||
mock_from_package.return_value = mock_toolkit
|
||||
|
||||
toolkit = Toolkit.from_entrypoint(mock_entry)
|
||||
|
||||
assert toolkit.name == "example"
|
||||
assert toolkit.package_name == "arcade-example"
|
||||
|
||||
|
||||
class TestToolkitIntegration:
|
||||
"""Integration tests for toolkit discovery and loading."""
|
||||
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_entrypoints")
|
||||
@patch("arcade_core.toolkit.Toolkit.find_arcade_toolkits_from_prefix")
|
||||
def test_mixed_toolkit_sources(self, mock_find_prefix, mock_find_ep):
|
||||
"""Test discovering toolkits from both sources with various naming patterns."""
|
||||
# Create toolkits with different naming patterns
|
||||
ep_toolkit1 = Toolkit(
|
||||
name="custom",
|
||||
package_name="my_custom_toolkit",
|
||||
version="1.0.0",
|
||||
description="Custom toolkit",
|
||||
)
|
||||
ep_toolkit2 = Toolkit(
|
||||
name="utils",
|
||||
package_name="arcade_utils",
|
||||
version="2.0.0",
|
||||
description="Utils toolkit",
|
||||
)
|
||||
prefix_toolkit1 = Toolkit(
|
||||
name="legacy",
|
||||
package_name="arcade_legacy",
|
||||
version="1.0.0",
|
||||
description="Legacy toolkit",
|
||||
)
|
||||
prefix_toolkit2 = Toolkit(
|
||||
name="utils",
|
||||
package_name="arcade_utils", # Same package name as ep_toolkit2
|
||||
version="0.9.0",
|
||||
description="Old utils toolkit",
|
||||
)
|
||||
mock_find_ep.return_value = [ep_toolkit1, ep_toolkit2]
|
||||
mock_find_prefix.return_value = [prefix_toolkit1, prefix_toolkit2]
|
||||
|
||||
toolkits = Toolkit.find_all_arcade_toolkits()
|
||||
|
||||
# Should have 3 toolkits (ep_toolkit2 supersedes prefix_toolkit2 due to same package_name)
|
||||
assert len(toolkits) == 3
|
||||
names = {t.name for t in toolkits}
|
||||
assert names == {"custom", "utils", "legacy"}
|
||||
utils_toolkit = next(t for t in toolkits if t.name == "utils")
|
||||
assert utils_toolkit.version == "2.0.0"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "arcade-ai"
|
||||
version = "2.0.6"
|
||||
version = "2.1.0"
|
||||
description = "Arcade.dev - Tool Calling platform for Agents"
|
||||
readme = "README.md"
|
||||
license = {file = "LICENSE"}
|
||||
|
|
|
|||
Loading…
Reference in a new issue