diff --git a/lib/tfw/event_handlers/event_handler.py b/lib/tfw/event_handlers/event_handler.py new file mode 100644 index 0000000..a75381e --- /dev/null +++ b/lib/tfw/event_handlers/event_handler.py @@ -0,0 +1,27 @@ +class EventHandler: + _instances = set() + + def __init__(self, server_connector): + type(self)._instances.add(self) + self.server_connector = server_connector + + def start(self): + self.server_connector.register_callback(self._event_callback) + + def _event_callback(self, message): + self.handle_event(message, self.server_connector) + + def handle_event(self, message, server_connector): + raise NotImplementedError() + + @classmethod + def stop_all_instances(cls): + for instance in cls._instances: + instance.stop() + + def stop(self): + self.server_connector.close() + self.cleanup() + + def cleanup(self): + pass diff --git a/lib/tfw/event_handlers/event_handler_factory.py b/lib/tfw/event_handlers/event_handler_factory.py new file mode 100644 index 0000000..c3a6b18 --- /dev/null +++ b/lib/tfw/event_handlers/event_handler_factory.py @@ -0,0 +1,51 @@ +from contextlib import suppress + +from .event_handler import EventHandler + + +class EventHandlerFactoryBase: + def build(self, event_handler, *, keys=None): + analyzer = EventHandlerAnalyzer(event_handler, keys) + event_handler = self._build_from_callable(analyzer) + event_handler.start() + return event_handler + + def _build_from_callable(self, analyzer): + server_connector = self._build_server_connector() + server_connector.subscribe(analyzer.keys) + event_handler = EventHandler(server_connector) + event_handler.handle_event = analyzer.handle_event + with suppress(AttributeError): + event_handler.cleanup = analyzer.cleanup + return event_handler + + def _build_server_connector(self): + raise NotImplementedError() + + +class EventHandlerAnalyzer: + def __init__(self, event_handler, supplied_keys): + self._event_handler = event_handler + self._supplied_keys = supplied_keys + + @property + def keys(self): + if self._supplied_keys is None: + try: + return self._event_handler.keys + except AttributeError: + raise ValueError('No keys supplied!') + return self._supplied_keys + + @property + def handle_event(self): + try: + return self._event_handler.handle_event + except AttributeError: + if callable(self._event_handler): + return self._event_handler + raise ValueError('Object must implement handle_event or be a callable!') + + @property + def cleanup(self): + return self._event_handler.cleanup diff --git a/lib/tfw/event_handlers/test_event_handler.py b/lib/tfw/event_handlers/test_event_handler.py new file mode 100644 index 0000000..2005f51 --- /dev/null +++ b/lib/tfw/event_handlers/test_event_handler.py @@ -0,0 +1,147 @@ +# pylint: disable=redefined-outer-name +from secrets import token_urlsafe +from random import randint + +import pytest + +from .event_handler_factory import EventHandlerFactoryBase +from .event_handler import EventHandler + + +class MockEventHandlerFactory(EventHandlerFactoryBase): + def _build_server_connector(self): + return MockServerConnector() + + +class MockServerConnector: + def __init__(self): + self.keys = [] + self._on_message = None + + def register_callback(self, callback): + self._on_message = callback + + def simulate_message(self, message): + self._on_message(message) + + def subscribe(self, keys): + self.keys.extend(keys) + + def unsubscribe(self, keys): + for key in keys: + self.keys.remove(key) + + def send_message(self, message, scope=None): + pass + + def close(self): + pass + + +class MockEventHandler: + def __init__(self): + self.cleaned_up = False + + def handle_event(self, message, server_connector): + pass + + def cleanup(self): + self.cleaned_up = True + + +@pytest.fixture +def test_msg(): + yield token_urlsafe(randint(16, 64)) + + +@pytest.fixture +def test_keys(): + yield [ + token_urlsafe(randint(2, 8)) + for _ in range(randint(16, 32)) + ] + + +def test_build_from_object(test_keys, test_msg): + mock_eh = MockEventHandler() + def test_handle_event(message, server_connector): + raise RuntimeError(message, server_connector.keys) + mock_eh.handle_event = test_handle_event + eh = MockEventHandlerFactory().build(mock_eh, keys=test_keys) + + with pytest.raises(RuntimeError) as err: + eh.server_connector.simulate_message(test_msg) + msg, keys = err.args + assert msg == test_msg + assert keys == test_keys + assert not mock_eh.cleaned_up + eh.stop() + assert mock_eh.cleaned_up + + +def test_build_from_object_with_keys(test_keys): + mock_eh = MockEventHandler() + mock_eh.keys = test_keys # pylint: disable=attribute-defined-outside-init + eh = MockEventHandlerFactory().build(mock_eh) + + assert not mock_eh.cleaned_up + EventHandler.stop_all_instances() + assert mock_eh.cleaned_up + assert eh.server_connector.keys == test_keys + + +def test_build_from_callable(test_keys, test_msg): + class SomeCallable: + def __init__(self): + self.message = None + self.cleaned_up = False + def __call__(self, message, server_connector): + self.message = message + def cleanup(self): + self.cleaned_up = True + + mock_eh = SomeCallable() + eh = MockEventHandlerFactory().build(mock_eh, keys=test_keys) + + assert eh.server_connector.keys == test_keys + assert not mock_eh.message + eh.server_connector.simulate_message(test_msg) + assert mock_eh.message == test_msg + assert not mock_eh.cleaned_up + eh.stop() + assert mock_eh.cleaned_up + + +def test_build_from_function(test_keys, test_msg): + def some_function(message, server_connector): + raise RuntimeError(message, server_connector.keys) + eh = MockEventHandlerFactory().build(some_function, keys=test_keys) + + assert eh.server_connector.keys == test_keys + with pytest.raises(RuntimeError) as err: + eh.server_connector.simulate_message(test_msg) + msg, keys = err.args + assert msg == test_msg + assert keys == test_keys + + +def test_build_from_lambda(test_keys, test_msg): + def assert_messages_equal(msg): + assert msg == test_msg + fun = lambda msg, sc: assert_messages_equal(msg) + eh = MockEventHandlerFactory().build(fun, keys=test_keys) + eh.server_connector.simulate_message(test_msg) + + +def test_build_raises_if_no_key(): + eh = MockEventHandler() + with pytest.raises(ValueError): + MockEventHandlerFactory().build(eh) + + def test_handle_event(*_): + pass + with pytest.raises(ValueError): + MockEventHandlerFactory().build(test_handle_event) + + with pytest.raises(ValueError): + MockEventHandlerFactory().build(lambda msg, sc: None)