Skip to content

Commit

Permalink
Configurable (default) left-strip recv'd messages (#7)
Browse files Browse the repository at this point in the history
* Configurable (default) left-strip recv'd messages

* Remove asyncio.coroutine usages
  • Loading branch information
pirogoeth authored May 13, 2020
1 parent b3ed4c4 commit 1dd4fa2
Show file tree
Hide file tree
Showing 15 changed files with 75 additions and 96 deletions.
2 changes: 1 addition & 1 deletion machine/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
__description__ = "A sexy, simple, yet powerful and extendable Slack bot"
__uri__ = "https://github.com/DandyDev/slack-machine"

__version_info__ = (0, 20, 0)
__version_info__ = (0, 20, 1)
__version__ = ".".join(map(str, __version_info__))

__author__ = "Daan Debie"
Expand Down
1 change: 1 addition & 0 deletions machine/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ def _register_plugin_actions(
"class_name": plugin_class,
"function": fn,
"regex": regex,
"lstrip": config["lstrip"],
}
key = "{}-{}".format(fq_fn_name, regex.pattern)
self._plugin_actions[action][key] = event_handler
Expand Down
14 changes: 9 additions & 5 deletions machine/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,16 @@ def _check_bot_mention(self, event):

async def _dispatch_listeners(self, listeners, event):
handlers = []
for l in listeners:
matcher = l["regex"]
match = matcher.search(event.get("text", ""))
for listener in listeners:
matcher = listener["regex"]
text = event.get("text", "")
if listener["lstrip"]:
text = text.lstrip()

match = matcher.search(text)
if match:
message = self._gen_message(event, l["class_name"])
handlers.append(l["function"](message, **match.groupdict()))
message = self._gen_message(event, listener["class_name"])
handlers.append(listener["function"](message, **match.groupdict()))

if handlers:
await asyncio.gather(*handlers)
3 changes: 1 addition & 2 deletions machine/plugins/builtin/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
class HelpPlugin(MachineBasePlugin):
"""Getting Help"""

@respond_to(r"^help(?:\s+?(?P<topic>\w+)?)?")
@respond_to(r"^help(?:\s+?(?P<topic>.+)?)?")
async def help(self, msg: Message, topic: Optional[str]):
""" help [topic]: display this help text or display the manual for a topic/command
"""

manual = (await self.storage.get("manual", shared=True))["human"]
print(f"Topic {topic}")
if not topic:
help_text = self._gen_manual_overview(manual)
else:
Expand Down
44 changes: 20 additions & 24 deletions machine/plugins/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def process_decorator(f):
return process_decorator


def listen_to(regex, flags=re.IGNORECASE):
def listen_to(regex, flags=re.IGNORECASE, lstrip: bool = True):
"""Listen to messages matching a regex pattern
This decorator will enable a Plugin method to listen to messages that match a regex pattern.
Expand All @@ -46,22 +46,20 @@ def listen_to(regex, flags=re.IGNORECASE):

def listen_to_decorator(f):
f.metadata = getattr(f, "metadata", {})
f.metadata["plugin_actions"] = f.metadata.get("plugin_actions", {})
f.metadata["plugin_actions"]["listen_to"] = f.metadata["plugin_actions"].get(
"listen_to", {}
)
f.metadata["plugin_actions"]["listen_to"]["regex"] = f.metadata[
"plugin_actions"
]["listen_to"].get("regex", [])
f.metadata.setdefault("plugin_actions", {})
f.metadata["plugin_actions"].setdefault("listen_to", {})
f.metadata["plugin_actions"]["listen_to"].setdefault("regex", [])

f.metadata["plugin_actions"]["listen_to"]["regex"].append(
re.compile(regex, flags)
)
f.metadata["plugin_actions"]["listen_to"]["lstrip"] = lstrip
return f

return listen_to_decorator


def respond_to(regex, flags=re.IGNORECASE):
def respond_to(regex, flags=re.IGNORECASE, lstrip: bool = True):
"""Listen to messages mentioning the bot and matching a regex pattern
This decorator will enable a Plugin method to listen to messages that are directed to the bot
Expand All @@ -79,16 +77,14 @@ def respond_to(regex, flags=re.IGNORECASE):

def respond_to_decorator(f):
f.metadata = getattr(f, "metadata", {})
f.metadata["plugin_actions"] = f.metadata.get("plugin_actions", {})
f.metadata["plugin_actions"]["respond_to"] = f.metadata["plugin_actions"].get(
"respond_to", {}
)
f.metadata["plugin_actions"]["respond_to"]["regex"] = f.metadata[
"plugin_actions"
]["respond_to"].get("regex", [])
f.metadata.setdefault("plugin_actions", {})
f.metadata["plugin_actions"].setdefault("respond_to", {})
f.metadata["plugin_actions"]["respond_to"].setdefault("regex", [])

f.metadata["plugin_actions"]["respond_to"]["regex"].append(
re.compile(regex, flags)
)
f.metadata["plugin_actions"]["respond_to"]["lstrip"] = lstrip
return f

return respond_to_decorator
Expand Down Expand Up @@ -130,7 +126,9 @@ def schedule(

def schedule_decorator(f):
f.metadata = getattr(f, "metadata", {})
f.metadata["plugin_actions"] = f.metadata.get("plugin_actions", {})
f.metadata.setdefault("plugin_actions", {})
f.metadata["plugin_actions"].setdefault("respond_to", {})

f.metadata["plugin_actions"]["schedule"] = kwargs
return f

Expand Down Expand Up @@ -166,9 +164,8 @@ def required_settings(settings):

def required_settings_decorator(f_or_cls):
f_or_cls.metadata = getattr(f_or_cls, "metadata", {})
f_or_cls.metadata["required_settings"] = f_or_cls.metadata.get(
"required_settings", []
)
f_or_cls.metadata.setdefault("required_settings", [])

if isinstance(settings, list):
f_or_cls.metadata["required_settings"].extend(settings)
elif isinstance(settings, str):
Expand All @@ -189,10 +186,9 @@ def route(path, **kwargs):

def route_decorator(f):
f.metadata = getattr(f, "metadata", {})
f.metadata["plugin_actions"] = f.metadata.get("plugin_actions", {})
f.metadata["plugin_actions"]["route"] = f.metadata["plugin_actions"].get(
"route", []
)
f.metadata.setdefault("plugin_actions", {})
f.metadata["plugin_actions"].setdefault("route", [])

kwargs["path"] = path
f.metadata["plugin_actions"]["route"].append(kwargs)
return f
Expand Down
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ coverage==4.5.4
flake8==3.7.9
flake8-bugbear==19.8.0
pyroma==2.5
pytest-asyncio==0.12.0
pytest-cov==2.7.1
pytest-html==1.21.1
pytest-metadata==1.8.0
pytest-mock==1.11.2
pytest==5.2.2
pytest==5.4.2
sphinx-autobuild==0.7.1
tox==3.13.1
twine==2.0.0
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
aiohttp==3.6.2
aioredis==1.3.0
aioredis==1.3.1
apscheduler==3.6.1
async_lru==1.0.2
asyncblink==0.3.2
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def run(self):
setup_requires=["pytest-runner"],
tests_require=[
"pytest",
"pytest-asyncio",
"pytest-cov",
"pytest-html",
"pytest-metadata",
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-

from tests.helpers import expect
from tests.helpers.aio import async_test, coroutine_mock, make_coroutine_mock
from tests.helpers.aio import make_awaitable_result
40 changes: 8 additions & 32 deletions tests/helpers/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,16 @@
import asyncio
from functools import wraps
from typing import Any, Awaitable, Callable, NewType
from unittest.mock import Mock
from unittest.mock import MagicMock

CoroutineFunction = NewType("CoroutineFunction", Callable[..., Awaitable])


def async_test(fn: CoroutineFunction):
""" Wrapper around test methods, to allow them to be run async """
def make_awaitable_result(return_value: Any):
fut = asyncio.Future()
if isinstance(return_value, Exception):
fut.set_exception(return_value)
else:
fut.set_result(return_value)

@wraps(fn)
def wrapper(*args, **kwargs):
coro = asyncio.coroutine(fn)
future = coro(*args, **kwargs)
loop = asyncio.get_event_loop()
loop.run_until_complete(future)

return wrapper


def coroutine_mock():
""" Usable as a mock callable for patching async functions.
From https://stackoverflow.com/a/32505333
"""

coro = Mock(name="CoroutineResult")
corofunc = Mock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro))
corofunc.coro = coro
return corofunc


def make_coroutine_mock(return_value: Any):
""" Returns an coroutine mock with the given return_value.
The returned item is ready to be `await`ed
"""

mock = coroutine_mock()
mock.coro.return_value = return_value
return mock()
return fut
4 changes: 4 additions & 0 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,23 @@ def test_listen_to(listen_to_f):
assert "plugin_actions" in listen_to_f.metadata
assert "listen_to" in listen_to_f.metadata["plugin_actions"]
assert "regex" in listen_to_f.metadata["plugin_actions"]["listen_to"]
assert "lstrip" in listen_to_f.metadata["plugin_actions"]["listen_to"]
assert listen_to_f.metadata["plugin_actions"]["listen_to"]["regex"] == [
re.compile(r"hello", re.IGNORECASE)
]
assert listen_to_f.metadata["plugin_actions"]["listen_to"]["lstrip"] == True


def test_respond_to(respond_to_f):
assert hasattr(respond_to_f, "metadata")
assert "plugin_actions" in respond_to_f.metadata
assert "respond_to" in respond_to_f.metadata["plugin_actions"]
assert "regex" in respond_to_f.metadata["plugin_actions"]["respond_to"]
assert "lstrip" in respond_to_f.metadata["plugin_actions"]["respond_to"]
assert respond_to_f.metadata["plugin_actions"]["respond_to"]["regex"] == [
re.compile(r"hello", re.IGNORECASE)
]
assert respond_to_f.metadata["plugin_actions"]["respond_to"]["lstrip"] == True


def test_schedule(schedule_f):
Expand Down
9 changes: 5 additions & 4 deletions tests/test_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from machine.settings import import_settings

from tests.fake_plugins import FakePlugin
from tests.helpers import async_test


@pytest.fixture
Expand Down Expand Up @@ -51,6 +50,7 @@ def plugin_actions(fake_plugin):
"class_name": "tests.fake_plugins.FakePlugin",
"function": listen_fn,
"regex": re.compile("hi", re.IGNORECASE),
"lstrip": True,
}
},
"respond_to": {
Expand All @@ -59,6 +59,7 @@ def plugin_actions(fake_plugin):
"class_name": "tests.fake_plugins.FakePlugin",
"function": respond_fn,
"regex": re.compile("hello", re.IGNORECASE),
"lstrip": True,
}
},
"process": {
Expand Down Expand Up @@ -94,7 +95,7 @@ def dispatcher(mocker, plugin_actions, request):
return dispatch_instance


@async_test
@pytest.mark.asyncio
async def test_handle_event_process(dispatcher, fake_plugin):
await dispatcher.handle_event("some_event", data={})
assert fake_plugin.process_function.call_count == 1
Expand All @@ -111,7 +112,7 @@ def _assert_message(args, text):
assert args[0][0].text == text


@async_test
@pytest.mark.asyncio
async def test_handle_event_listen_to(dispatcher, fake_plugin):
msg_event = {"text": "hi", "channel": "C1", "user": "user1"}
await dispatcher.handle_event("message", data=msg_event)
Expand All @@ -121,7 +122,7 @@ async def test_handle_event_listen_to(dispatcher, fake_plugin):
_assert_message(args, "hi")


@async_test
@pytest.mark.asyncio
async def test_handle_event_respond_to(dispatcher, fake_plugin):
msg_event = {"text": "<@123> hello", "channel": "C1", "user": "user1"}
await dispatcher.handle_event("message", data=msg_event)
Expand Down
10 changes: 4 additions & 6 deletions tests/test_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@

from machine.storage.backends.memory import MemoryStorage

from tests.helpers import async_test


@pytest.fixture
def memory_storage():
return MemoryStorage({})


@async_test
@pytest.mark.asyncio
async def test_store_retrieve_values(memory_storage):
assert memory_storage._storage == {}
await memory_storage.set("key1", "value1")
assert memory_storage._storage == {"key1": ("value1", None)}
assert (await memory_storage.get("key1")) == "value1"


@async_test
@pytest.mark.asyncio
async def test_delete_values(memory_storage):
assert memory_storage._storage == {}
await memory_storage.set("key1", "value1")
Expand All @@ -34,7 +32,7 @@ async def test_delete_values(memory_storage):
assert memory_storage._storage == {"key1": ("value1", None)}


@async_test
@pytest.mark.asyncio
async def test_expire_values(memory_storage, mocker):
assert memory_storage._storage == {}
mocked_dt = mocker.patch("machine.storage.backends.memory.datetime", autospec=True)
Expand All @@ -48,7 +46,7 @@ async def test_expire_values(memory_storage, mocker):
assert (await memory_storage.get("key1")) is None


@async_test
@pytest.mark.asyncio
async def test_inclusion(memory_storage):
assert memory_storage._storage == {}
await memory_storage.set("key1", "value1")
Expand Down
Loading

0 comments on commit 1dd4fa2

Please sign in to comment.