Skip to content

Commit 434f7ee

Browse files
unodeattzonko
andauthored
Implement PluginManager and HelpPlugin (#326)
* Implement PluginManager interface Co-authored-by: Alex Tzonkov <[email protected]>
1 parent b184c3e commit 434f7ee

17 files changed

+509
-320
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- name: Run Flake8
3636
run: flake8
3737
- name: Black code style
38-
run: black . --check --target-version py38 --exclude '\.mypy_cache/|\.venv/|env/|(.*/)*snapshots/|.pytype/'
38+
run: black . --check --target-version py310 --exclude '\.mypy_cache/|\.venv/|env/|(.*/)*snapshots/|.pytype/'
3939
- name: Docstring formatting
4040
run: docformatter -c -r . --wrap-summaries 88 --wrap-descriptions 88
4141
- name: Check import order with isort

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ repos:
2222
- "--filter-files"
2323
- repo: https://github.com/psf/black
2424
# Code style formatting
25-
rev: 21.5b1
25+
rev: 22.3.0
2626
hooks:
2727
- id: black
2828
exclude: (.*/)*snapshots/
@@ -31,6 +31,7 @@ repos:
3131
rev: 3.9.0
3232
hooks:
3333
- id: flake8
34+
exclude: (.*/)*snapshots/
3435
- repo: https://github.com/mattseymour/pre-commit-pytype
3536
rev: '2020.10.8'
3637
hooks:

mmpy_bot/bot.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import asyncio
22
import logging
33
import sys
4-
from typing import Optional, Sequence
4+
from typing import List, Optional, Union
55

66
from mmpy_bot.driver import Driver
77
from mmpy_bot.event_handler import EventHandler
8-
from mmpy_bot.plugins import ExamplePlugin, Plugin, WebHookExample
8+
from mmpy_bot.plugins import (
9+
ExamplePlugin,
10+
HelpPlugin,
11+
Plugin,
12+
PluginManager,
13+
WebHookExample,
14+
)
915
from mmpy_bot.settings import Settings
1016
from mmpy_bot.webhook_server import WebHookServer
1117

@@ -22,18 +28,18 @@ class Bot:
2228
def __init__(
2329
self,
2430
settings: Optional[Settings] = None,
25-
plugins: Optional[Sequence[Plugin]] = None,
31+
plugins: Optional[Union[List[Plugin], PluginManager]] = None,
2632
enable_logging: bool = True,
2733
):
28-
if plugins is None:
29-
plugins = [ExamplePlugin(), WebHookExample()]
34+
self._setup_plugin_manager(plugins)
35+
3036
# Use default settings if none were specified.
3137
self.settings = settings or Settings()
3238

39+
self.console = None
40+
3341
if enable_logging:
3442
self._register_logger()
35-
else:
36-
self.console = None
3743

3844
self.driver = Driver(
3945
{
@@ -48,9 +54,9 @@ def __init__(
4854
}
4955
)
5056
self.driver.login()
51-
self.plugins = self._initialize_plugins(plugins)
57+
self.plugin_manager.initialize(self.driver, self.settings)
5258
self.event_handler = EventHandler(
53-
self.driver, settings=self.settings, plugins=self.plugins
59+
self.driver, settings=self.settings, plugin_manager=self.plugin_manager
5460
)
5561
self.webhook_server = None
5662

@@ -59,6 +65,16 @@ def __init__(
5965

6066
self.running = False
6167

68+
def _setup_plugin_manager(self, plugins):
69+
if plugins is None:
70+
self.plugin_manager = PluginManager(
71+
[HelpPlugin(), ExamplePlugin(), WebHookExample()]
72+
)
73+
elif isinstance(plugins, PluginManager):
74+
self.plugin_manager = plugins
75+
else:
76+
self.plugin_manager = PluginManager(plugins)
77+
6278
def _register_logger(self):
6379
logging.basicConfig(
6480
**{
@@ -80,11 +96,6 @@ def _register_logger(self):
8096
)
8197
logging.getLogger("").addHandler(self.console)
8298

83-
def _initialize_plugins(self, plugins: Sequence[Plugin]):
84-
for plugin in plugins:
85-
plugin.initialize(self.driver, self.settings)
86-
return plugins
87-
8899
def _initialize_webhook_server(self):
89100
self.webhook_server = WebHookServer(
90101
url=self.settings.WEBHOOK_HOST_URL, port=self.settings.WEBHOOK_HOST_PORT
@@ -110,8 +121,8 @@ def run(self):
110121
if self.settings.WEBHOOK_HOST_ENABLED:
111122
self.driver.threadpool.start_webhook_server_thread(self.webhook_server)
112123

113-
for plugin in self.plugins:
114-
plugin.on_start()
124+
# Trigger "start" methods on every plugin
125+
self.plugin_manager.start()
115126

116127
# Start listening for events
117128
self.event_handler.start()
@@ -129,9 +140,10 @@ def stop(self):
129140
return
130141

131142
log.info("Stopping bot.")
143+
132144
# Shutdown the running plugins
133-
for plugin in self.plugins:
134-
plugin.on_stop()
145+
self.plugin_manager.stop()
146+
135147
# Stop the threadpool
136148
self.driver.threadpool.stop()
137149
self.running = False

mmpy_bot/event_handler.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import logging
44
import queue
55
import re
6-
from collections import defaultdict
7-
from typing import Sequence
86

97
from mmpy_bot.driver import Driver
10-
from mmpy_bot.plugins import Plugin
8+
from mmpy_bot.plugins import PluginManager
119
from mmpy_bot.settings import Settings
1210
from mmpy_bot.webhook_server import NoResponse
1311
from mmpy_bot.wrappers import Message, WebHookEvent
@@ -20,27 +18,18 @@ def __init__(
2018
self,
2119
driver: Driver,
2220
settings: Settings,
23-
plugins: Sequence[Plugin],
21+
plugin_manager: PluginManager,
2422
ignore_own_messages=True,
2523
):
2624
"""The EventHandler class takes care of the connection to mattermost and calling
2725
the appropriate response function to each event."""
2826
self.driver = driver
2927
self.settings = settings
3028
self.ignore_own_messages = ignore_own_messages
31-
self.plugins = plugins
29+
self.plugin_manager = plugin_manager
3230

3331
self._name_matcher = re.compile(rf"^@?{self.driver.username}\:?\s?")
3432

35-
# Collect the listeners from all plugins
36-
self.message_listeners = defaultdict(list)
37-
self.webhook_listeners = defaultdict(list)
38-
for plugin in self.plugins:
39-
for matcher, functions in plugin.message_listeners.items():
40-
self.message_listeners[matcher].extend(functions)
41-
for matcher, functions in plugin.webhook_listeners.items():
42-
self.webhook_listeners[matcher].extend(functions)
43-
4433
def start(self):
4534
# This is blocking, will loop forever
4635
self.driver.init_websocket(self._handle_event)
@@ -87,7 +76,7 @@ async def _handle_post(self, post):
8776
# Find all the listeners that match this message, and have their plugins handle
8877
# the rest.
8978
tasks = []
90-
for matcher, functions in self.message_listeners.items():
79+
for matcher, functions in self.plugin_manager.message_listeners.items():
9180
match = matcher.search(message.text)
9281
if match:
9382
groups = list([group for group in match.groups() if group != ""])
@@ -107,7 +96,7 @@ async def _handle_webhook(self, event: WebHookEvent):
10796
# Find all the listeners that match this webhook id, and have their plugins
10897
# handle the rest.
10998
tasks = []
110-
for matcher, functions in self.webhook_listeners.items():
99+
for matcher, functions in self.plugin_manager.webhook_listeners.items():
111100
match = matcher.search(event.webhook_id)
112101
if match:
113102
for function in functions:

mmpy_bot/function.py

Lines changed: 26 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,25 @@
55
import logging
66
import re
77
from abc import ABC, abstractmethod
8-
from typing import Callable, Optional, Sequence, Union
8+
from typing import TYPE_CHECKING, Callable, Optional, Sequence, Union
99

1010
import click
1111

12-
from mmpy_bot.utils import completed_future, spaces
12+
from mmpy_bot.utils import completed_future
1313
from mmpy_bot.webhook_server import NoResponse
1414
from mmpy_bot.wrappers import Message, WebHookEvent
1515

16+
if TYPE_CHECKING:
17+
from mmpy_bot.plugins import Plugin
18+
19+
1620
log = logging.getLogger("mmpy.function")
1721

1822

1923
class Function(ABC):
2024
def __init__(
2125
self,
22-
function: Union[Function, click.command],
26+
function: Union[Function, click.Command],
2327
matcher: re.Pattern,
2428
**metadata,
2529
):
@@ -32,7 +36,11 @@ def __init__(
3236
self.siblings.append(function)
3337
function = function.function
3438

35-
# FIXME: After this while loop it is possible that function is not a Function, do we really want to assign self.function to something which is not a Function? Check if this is needed for the click.Command case
39+
if function is None:
40+
raise ValueError(
41+
"ERROR: Possible bug, inside the Function class function should not end up being None"
42+
)
43+
3644
self.function = function
3745
self.is_coroutine = asyncio.iscoroutinefunction(function)
3846
self.is_click_function: bool = False
@@ -46,21 +54,14 @@ def __init__(
4654
self.function.invoke = None
4755

4856
# To be set in the child class or from the parent plugin
49-
self.plugin = None
57+
self.plugin: Optional[Plugin] = None
5058
self.name: Optional[str] = None
51-
self.docstring: Optional[str] = None
59+
self.docstring = self.function.__doc__ or ""
5260

5361
@abstractmethod
5462
def __call__(self, *args):
5563
pass
5664

57-
def get_help_string(self):
58-
string = f"`{self.matcher.pattern}`:\n"
59-
# Add a docstring
60-
doc = self.docstring or "No description provided."
61-
string += f"{spaces(8)}{doc}\n"
62-
return string
63-
6465

6566
class MessageFunction(Function):
6667
"""Wrapper around a Plugin class method that should respond to certain Mattermost
@@ -95,7 +96,6 @@ def __init__(
9596

9697
# Default for non-click functions
9798
_function: Union[Callable, click.Command] = self.function
98-
self.docstring = self.function.__doc__
9999

100100
if self.is_click_function:
101101
_function = self.function.callback
@@ -109,9 +109,8 @@ def __init__(
109109
info_name=self.matcher.pattern.strip("^").split(" (.*)?")[0],
110110
) as ctx:
111111
# Get click help string and do some extra formatting
112-
self.docstring = self.function.get_help(ctx).replace(
113-
"\n", f"\n{spaces(8)}"
114-
)
112+
self.docstring += f"\n\n{self.function.get_help(ctx)}"
113+
115114
if _function is not None:
116115
self.name = _function.__qualname__
117116

@@ -169,38 +168,6 @@ def __call__(self, message: Message, *args):
169168

170169
return self.function(self.plugin, message, *args)
171170

172-
def get_help_string(self):
173-
string = super().get_help_string()
174-
if any(
175-
[
176-
self.needs_mention,
177-
self.direct_only,
178-
self.allowed_users,
179-
self.allowed_channels,
180-
self.silence_fail_msg,
181-
]
182-
):
183-
# Print some information describing the usage settings.
184-
string += f"{spaces(4)}Additional information:\n"
185-
if self.needs_mention:
186-
string += (
187-
f"{spaces(4)}- Needs to either mention @{self.plugin.driver.username}"
188-
" or be a direct message.\n"
189-
)
190-
if self.direct_only:
191-
string += f"{spaces(4)}- Needs to be a direct message.\n"
192-
193-
if self.allowed_users:
194-
string += f"{spaces(4)}- Restricted to certain users.\n"
195-
196-
if self.allowed_channels:
197-
string += f"{spaces(4)}- Restricted to certain channels.\n"
198-
199-
if self.silence_fail_msg:
200-
string += f"{spaces(4)}- If it should reply to a non privileged user / in a non privileged channel.\n"
201-
202-
return string
203-
204171

205172
def listen_to(
206173
regexp: str,
@@ -238,7 +205,7 @@ def wrapped_func(func):
238205
reg = f"^{reg.strip('^')} (.*)?" # noqa
239206

240207
pattern = re.compile(reg, regexp_flag)
241-
return MessageFunction(
208+
new_func = MessageFunction(
242209
func,
243210
matcher=pattern,
244211
direct_only=direct_only,
@@ -249,6 +216,10 @@ def wrapped_func(func):
249216
**metadata,
250217
)
251218

219+
# Preserve docstring
220+
new_func.__doc__ = func.__doc__
221+
return new_func
222+
252223
return wrapped_func
253224

254225

@@ -267,7 +238,6 @@ def __init__(
267238
)
268239

269240
self.name = self.function.__qualname__
270-
self.docstring = self.function.__doc__
271241

272242
argspec = list(inspect.signature(self.function).parameters.keys())
273243
if not argspec == ["self", "event"]:
@@ -305,10 +275,14 @@ def listen_webhook(
305275

306276
def wrapped_func(func):
307277
pattern = re.compile(regexp)
308-
return WebHookFunction(
278+
new_func = WebHookFunction(
309279
func,
310280
matcher=pattern,
311281
**metadata,
312282
)
313283

284+
# Preserve docstring
285+
new_func.__doc__ = func.__doc__
286+
return new_func
287+
314288
return wrapped_func

mmpy_bot/plugins/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from mmpy_bot.plugins.base import Plugin
1+
from mmpy_bot.plugins.base import Plugin, PluginManager
22
from mmpy_bot.plugins.example import ExamplePlugin
3+
from mmpy_bot.plugins.help_example import HelpPlugin
34
from mmpy_bot.plugins.webhook_example import WebHookExample
45

5-
__all__ = ["Plugin", "ExamplePlugin", "WebHookExample"]
6+
__all__ = ["Plugin", "PluginManager", "HelpPlugin", "ExamplePlugin", "WebHookExample"]

0 commit comments

Comments
 (0)