Skip to content

Commit 881acc0

Browse files
authored
refactor(plugins): convert Plugin from Protocol to ABC (#1741)
1 parent 30e3020 commit 881acc0

File tree

4 files changed

+50
-45
lines changed

4 files changed

+50
-45
lines changed

src/strands/plugins/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
```python
88
from strands.plugins import Plugin
99
10-
class LoggingPlugin:
10+
class LoggingPlugin(Plugin):
1111
name = "logging"
1212
1313
def init_plugin(self, agent: Agent) -> None:

src/strands/plugins/plugin.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
"""Plugin protocol for extending agent functionality.
1+
"""Plugin base class for extending agent functionality.
22
3-
This module defines the Plugin Protocol, which provides a composable way to
3+
This module defines the Plugin base class, which provides a composable way to
44
add behavior changes to agents through a standardized initialization pattern.
55
"""
66

7+
from abc import ABC, abstractmethod
78
from collections.abc import Awaitable
8-
from typing import TYPE_CHECKING, Protocol, runtime_checkable
9+
from typing import TYPE_CHECKING
910

1011
if TYPE_CHECKING:
1112
from ..agent import Agent
1213

1314

14-
@runtime_checkable
15-
class Plugin(Protocol):
16-
"""Protocol for objects that extend agent functionality.
15+
class Plugin(ABC):
16+
"""Base class for objects that extend agent functionality.
1717
1818
Plugins provide a composable way to add behavior changes to agents.
1919
They are initialized with an agent instance and can register hooks,
@@ -24,16 +24,21 @@ class Plugin(Protocol):
2424
2525
Example:
2626
```python
27-
class MyPlugin:
27+
class MyPlugin(Plugin):
2828
name = "my-plugin"
2929
3030
def init_plugin(self, agent: Agent) -> None:
3131
agent.add_hook(self.on_model_call, BeforeModelCallEvent)
3232
```
3333
"""
3434

35-
name: str
35+
@property
36+
@abstractmethod
37+
def name(self) -> str:
38+
"""A stable string identifier for the plugin."""
39+
...
3640

41+
@abstractmethod
3742
def init_plugin(self, agent: "Agent") -> None | Awaitable[None]:
3843
"""Initialize the plugin with an agent instance.
3944

src/strands/plugins/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class _PluginRegistry:
2828
```python
2929
registry = _PluginRegistry(agent)
3030
31-
class MyPlugin:
31+
class MyPlugin(Plugin):
3232
name = "my-plugin"
3333
3434
def init_plugin(self, agent: Agent) -> None:

tests/strands/plugins/test_plugins.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
# Plugin Protocol Tests
1111

1212

13-
def test_plugin_protocol_is_runtime_checkable():
14-
"""Test that Plugin Protocol is runtime checkable with isinstance."""
13+
def test_plugin_class_requires_inheritance():
14+
"""Test that Plugin class requires inheritance."""
1515

16-
class MyPlugin:
16+
class MyPlugin(Plugin):
1717
name = "my-plugin"
1818

1919
def init_plugin(self, agent):
@@ -23,10 +23,10 @@ def init_plugin(self, agent):
2323
assert isinstance(plugin, Plugin)
2424

2525

26-
def test_plugin_protocol_sync_implementation():
27-
"""Test Plugin Protocol works with synchronous init_plugin."""
26+
def test_plugin_class_sync_implementation():
27+
"""Test Plugin class works with synchronous init_plugin."""
2828

29-
class SyncPlugin:
29+
class SyncPlugin(Plugin):
3030
name = "sync-plugin"
3131

3232
def init_plugin(self, agent):
@@ -35,7 +35,7 @@ def init_plugin(self, agent):
3535
plugin = SyncPlugin()
3636
mock_agent = unittest.mock.Mock()
3737

38-
# Verify the plugin matches the protocol
38+
# Verify the plugin is an instance of Plugin
3939
assert isinstance(plugin, Plugin)
4040
assert plugin.name == "sync-plugin"
4141

@@ -45,10 +45,10 @@ def init_plugin(self, agent):
4545

4646

4747
@pytest.mark.asyncio
48-
async def test_plugin_protocol_async_implementation():
49-
"""Test Plugin Protocol works with asynchronous init_plugin."""
48+
async def test_plugin_class_async_implementation():
49+
"""Test Plugin class works with asynchronous init_plugin."""
5050

51-
class AsyncPlugin:
51+
class AsyncPlugin(Plugin):
5252
name = "async-plugin"
5353

5454
async def init_plugin(self, agent):
@@ -57,7 +57,7 @@ async def init_plugin(self, agent):
5757
plugin = AsyncPlugin()
5858
mock_agent = unittest.mock.Mock()
5959

60-
# Verify the plugin matches the protocol
60+
# Verify the plugin is an instance of Plugin
6161
assert isinstance(plugin, Plugin)
6262
assert plugin.name == "async-plugin"
6363

@@ -66,33 +66,33 @@ async def init_plugin(self, agent):
6666
assert mock_agent.custom_attribute == "initialized by async plugin"
6767

6868

69-
def test_plugin_protocol_requires_name():
70-
"""Test that Plugin Protocol requires a name property."""
69+
def test_plugin_class_requires_name():
70+
"""Test that Plugin class requires a name property."""
7171

72-
class PluginWithoutName:
73-
def init_plugin(self, agent):
74-
pass
72+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
73+
74+
class PluginWithoutName(Plugin):
75+
def init_plugin(self, agent):
76+
pass
77+
78+
PluginWithoutName()
7579

76-
plugin = PluginWithoutName()
77-
# A class without 'name' should not pass isinstance check
78-
assert not isinstance(plugin, Plugin)
7980

81+
def test_plugin_class_requires_init_plugin_method():
82+
"""Test that Plugin class requires an init_plugin method."""
8083

81-
def test_plugin_protocol_requires_init_plugin_method():
82-
"""Test that Plugin Protocol requires an init_plugin method."""
84+
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
8385

84-
class PluginWithoutInitPlugin:
85-
name = "incomplete-plugin"
86+
class PluginWithoutInitPlugin(Plugin):
87+
name = "incomplete-plugin"
8688

87-
plugin = PluginWithoutInitPlugin()
88-
# A class without 'init_plugin' should not pass isinstance check
89-
assert not isinstance(plugin, Plugin)
89+
PluginWithoutInitPlugin()
9090

9191

92-
def test_plugin_protocol_with_class_attribute_name():
93-
"""Test Plugin Protocol works when name is a class attribute."""
92+
def test_plugin_class_with_class_attribute_name():
93+
"""Test Plugin class works when name is a class attribute."""
9494

95-
class PluginWithClassAttribute:
95+
class PluginWithClassAttribute(Plugin):
9696
name: str = "class-attr-plugin"
9797

9898
def init_plugin(self, agent):
@@ -103,10 +103,10 @@ def init_plugin(self, agent):
103103
assert plugin.name == "class-attr-plugin"
104104

105105

106-
def test_plugin_protocol_with_property_name():
107-
"""Test Plugin Protocol works when name is a property."""
106+
def test_plugin_class_with_property_name():
107+
"""Test Plugin class works when name is a property."""
108108

109-
class PluginWithProperty:
109+
class PluginWithProperty(Plugin):
110110
@property
111111
def name(self):
112112
return "property-plugin"
@@ -137,7 +137,7 @@ def registry(mock_agent):
137137
def test_plugin_registry_add_and_init_calls_init_plugin(registry, mock_agent):
138138
"""Test adding a plugin calls its init_plugin method."""
139139

140-
class TestPlugin:
140+
class TestPlugin(Plugin):
141141
name = "test-plugin"
142142

143143
def __init__(self):
@@ -157,7 +157,7 @@ def init_plugin(self, agent):
157157
def test_plugin_registry_add_duplicate_raises_error(registry, mock_agent):
158158
"""Test that adding a duplicate plugin raises an error."""
159159

160-
class TestPlugin:
160+
class TestPlugin(Plugin):
161161
name = "test-plugin"
162162

163163
def init_plugin(self, agent):
@@ -175,7 +175,7 @@ def init_plugin(self, agent):
175175
def test_plugin_registry_add_and_init_with_async_plugin(registry, mock_agent):
176176
"""Test that add_and_init handles async plugins using run_async."""
177177

178-
class AsyncPlugin:
178+
class AsyncPlugin(Plugin):
179179
name = "async-plugin"
180180

181181
def __init__(self):

0 commit comments

Comments
 (0)