Skip to content

Commit 01a0c69

Browse files
committed
Implement a basic plugin system
1 parent 5f140af commit 01a0c69

File tree

4 files changed

+247
-0
lines changed

4 files changed

+247
-0
lines changed

trinity/extensibility/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from trinity.extensibility.events import ( # noqa: F401
2+
BaseEvent
3+
)
4+
from trinity.extensibility.plugin import ( # noqa: F401
5+
BasePlugin,
6+
DebugPlugin,
7+
PluginContext,
8+
)
9+
from trinity.extensibility.plugin_manager import ( # noqa: F401
10+
PluginManager,
11+
)

trinity/extensibility/events.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import (
2+
Any,
3+
Type,
4+
TYPE_CHECKING,
5+
)
6+
from argparse import (
7+
Namespace,
8+
)
9+
10+
from trinity.config import (
11+
ChainConfig,
12+
)
13+
14+
15+
if TYPE_CHECKING:
16+
from trinity.extensibility import ( # noqa: F401
17+
BasePlugin,
18+
)
19+
20+
21+
class BaseEvent:
22+
"""
23+
The base class for all plugin events. Plugin events can be broadcasted for all different
24+
kind of reasons. Plugins can act based on these events and consume the events even before
25+
the plugin is started, giving plugins the chance to start based on an event or a series of
26+
events. The startup of Trinity itself can be an event as well as the start of a plugin itself
27+
which, for instance, gives other plugins the chance to start based on these previous events.
28+
"""
29+
pass
30+
31+
32+
class TrinityStartupEvent(BaseEvent):
33+
"""
34+
Broadcasted when Trinity is starting.
35+
"""
36+
def __init__(self, args: Namespace, chain_config: ChainConfig) -> None:
37+
self.args = args
38+
self.chain_config = chain_config
39+
40+
41+
class PluginStartedEvent(BaseEvent):
42+
"""
43+
Broadcasted when a plugin was started
44+
"""
45+
def __init__(self, plugin: 'BasePlugin') -> None:
46+
self.plugin = plugin
47+
48+
49+
class ResourceAvailableEvent(BaseEvent):
50+
"""
51+
Broadcasted when a resource becomes available
52+
"""
53+
def __init__(self, resource: Any, resource_type: Type[Any]) -> None:
54+
self.resource = resource
55+
self.resource_type = resource_type

trinity/extensibility/plugin.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from abc import (
2+
ABC,
3+
abstractmethod
4+
)
5+
from argparse import (
6+
ArgumentParser
7+
)
8+
import logging
9+
from typing import (
10+
Any
11+
)
12+
13+
from trinity.extensibility.events import (
14+
BaseEvent
15+
)
16+
17+
# TODO: we spec this out later
18+
PluginContext = Any
19+
20+
21+
class BasePlugin(ABC):
22+
23+
@property
24+
@abstractmethod
25+
def name(self) -> str:
26+
"""
27+
Describe the name of the plugin
28+
"""
29+
raise NotImplementedError(
30+
"Must be implemented by subclasses"
31+
)
32+
33+
@property
34+
def logger(self) -> logging.Logger:
35+
return logging.getLogger('trinity.extensibility.plugin.BasePlugin#{0}'.format(self.name))
36+
37+
@abstractmethod
38+
def configure_parser(self, arg_parser: ArgumentParser) -> None:
39+
"""
40+
Called at startup, giving the plugin a chance to amend the Trinity CLI argument parser
41+
"""
42+
raise NotImplementedError(
43+
"Must be implemented by subclasses"
44+
)
45+
46+
@abstractmethod
47+
def handle_event(self, activation_event: BaseEvent) -> None:
48+
"""
49+
Notify the plugin about an event, giving it the chance to do internal accounting right
50+
before :meth:`~trinity.extensibility.plugin.BasePlugin.should_start` is called
51+
"""
52+
53+
raise NotImplementedError(
54+
"Must be implemented by subclasses"
55+
)
56+
57+
@abstractmethod
58+
def should_start(self) -> bool:
59+
"""
60+
Return ``True`` if the plugin should start, otherwise return ``False``
61+
"""
62+
63+
raise NotImplementedError(
64+
"Must be implemented by subclasses"
65+
)
66+
67+
@abstractmethod
68+
def start(self, context: PluginContext) -> None:
69+
"""
70+
The ``start`` method is called only once when the plugin is started
71+
"""
72+
raise NotImplementedError(
73+
"Must be implemented by subclasses"
74+
)
75+
76+
def stop(self) -> None:
77+
"""
78+
Called when the plugin gets stopped. Should be overwritten to perform cleanup
79+
work in case the plugin set up external resources.
80+
"""
81+
pass
82+
83+
84+
class DebugPlugin(BasePlugin):
85+
"""
86+
This is a dummy plugin useful for demonstration and debugging purposes
87+
"""
88+
89+
@property
90+
def name(self) -> str:
91+
return "Debug Plugin"
92+
93+
def configure_parser(self, arg_parser: ArgumentParser) -> None:
94+
arg_parser.add_argument("--debug-plugin", type=bool, required=False)
95+
96+
def handle_event(self, activation_event: BaseEvent) -> None:
97+
self.logger.info("Debug plugin: handle_event called: ", activation_event)
98+
99+
def should_start(self) -> bool:
100+
self.logger.info("Debug plugin: should_start called")
101+
return True
102+
103+
def start(self, context: PluginContext) -> None:
104+
self.logger.info("Debug plugin: start called")
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from argparse import (
2+
ArgumentParser
3+
)
4+
import logging
5+
from typing import (
6+
Iterable,
7+
List,
8+
Union,
9+
)
10+
11+
from trinity.extensibility.events import (
12+
BaseEvent,
13+
PluginStartedEvent,
14+
)
15+
from trinity.extensibility.plugin import (
16+
BasePlugin,
17+
)
18+
19+
20+
class PluginManager:
21+
"""
22+
The plugin manager is responsible to register, keep and manage the life cycle of any available
23+
plugins.
24+
25+
.. note::
26+
27+
This API is very much in flux and is expected to change heavily.
28+
"""
29+
30+
def __init__(self) -> None:
31+
self._plugin_store: List[BasePlugin] = []
32+
self._started_plugins: List[BasePlugin] = []
33+
self._logger = logging.getLogger("trinity.extensibility.plugin_manager.PluginManager")
34+
35+
def register(self, plugins: Union[BasePlugin, Iterable[BasePlugin]]) -> None:
36+
"""
37+
Register one or multiple instances of :class:`~trinity.extensibility.plugin.BasePlugin`
38+
with the plugin manager.
39+
"""
40+
41+
new_plugins = [plugins] if isinstance(plugins, BasePlugin) else plugins
42+
self._plugin_store.extend(new_plugins)
43+
44+
def amend_argparser_config(self, arg_parser: ArgumentParser) -> None:
45+
"""
46+
Call :meth:`~trinity.extensibility.plugin.BasePlugin.configure_parser` for every registered
47+
plugin, giving them the option to amend the global parser setup.
48+
"""
49+
for plugin in self._plugin_store:
50+
plugin.configure_parser(arg_parser)
51+
52+
def broadcast(self, event: BaseEvent, exclude: BasePlugin = None) -> None:
53+
"""
54+
Notify every registered :class:`~trinity.extensibility.plugin.BasePlugin` about an
55+
event and check whether the plugin wants to start based on that event.
56+
57+
If a plugin gets started it will cause a
58+
:class:`~trinity.extensibility.events.PluginStartedEvent` to get
59+
broadcasted to all other plugins, giving them the chance to start based on that.
60+
"""
61+
for plugin in self._plugin_store:
62+
63+
if plugin is exclude:
64+
continue
65+
66+
plugin.handle_event(event)
67+
68+
if plugin in self._started_plugins:
69+
continue
70+
71+
if not plugin.should_start():
72+
continue
73+
74+
plugin.start(None)
75+
self._started_plugins.append(plugin)
76+
self._logger.info("Plugin started: {}".format(plugin.name))
77+
self.broadcast(PluginStartedEvent(plugin), plugin)

0 commit comments

Comments
 (0)