Rework whole package structure (improved dependency handling)

This commit is contained in:
Kristóf Tóth
2019-07-24 15:17:16 +02:00
parent c6e01d294d
commit a23224aced
72 changed files with 74 additions and 75 deletions

View File

@ -0,0 +1,3 @@
from .event_handler_factory_base import EventHandlerFactoryBase
from .event_handler import EventHandler
from .fsm_aware_event_handler import FSMAwareEventHandler

View File

@ -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

View File

@ -0,0 +1,68 @@
from contextlib import suppress
from .event_handler import EventHandler
class EventHandlerFactoryBase:
def build(self, handler_stub, *, keys=None, event_handler_type=EventHandler):
builder = EventHandlerBuilder(handler_stub, keys, event_handler_type)
server_connector = self._build_server_connector()
event_handler = builder.build(server_connector)
handler_stub.server_connector = server_connector
with suppress(AttributeError):
handler_stub.start()
event_handler.start()
return event_handler
def _build_server_connector(self):
raise NotImplementedError()
class EventHandlerBuilder:
def __init__(self, event_handler, supplied_keys, event_handler_type):
self._analyzer = HandlerStubAnalyzer(event_handler, supplied_keys)
self._event_handler_type = event_handler_type
def build(self, server_connector):
event_handler = self._event_handler_type(server_connector)
server_connector.subscribe(*self._try_get_keys(event_handler))
event_handler.handle_event = self._analyzer.handle_event
with suppress(AttributeError):
event_handler.cleanup = self._analyzer.cleanup
return event_handler
def _try_get_keys(self, event_handler):
try:
return self._analyzer.keys
except ValueError:
with suppress(AttributeError):
return event_handler.keys
raise
class HandlerStubAnalyzer:
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

View File

@ -0,0 +1,37 @@
import logging
from tfw.internals.crypto import KeyManager, verify_message
LOG = logging.getLogger(__name__)
class FSMAware:
keys = ['fsm_update']
"""
Base class for stuff that has to be aware of the framework FSM.
This is done by processing 'fsm_update' messages.
"""
def __init__(self):
self.fsm_state = None
self.fsm_in_accepted_state = False
self.fsm_event_log = []
self._auth_key = KeyManager().auth_key
def process_message(self, message):
if message['key'] == 'fsm_update':
if verify_message(self._auth_key, message):
self._handle_fsm_update(message)
def _handle_fsm_update(self, message):
try:
new_state = message['current_state']
if self.fsm_state != new_state:
self.handle_fsm_step(message)
self.fsm_state = new_state
self.fsm_in_accepted_state = message['in_accepted_state']
self.fsm_event_log.append(message)
except KeyError:
LOG.error('Invalid fsm_update message received!')
def handle_fsm_step(self, message):
pass

View File

@ -0,0 +1,19 @@
from .event_handler import EventHandler
from .fsm_aware import FSMAware
class FSMAwareEventHandler(EventHandler, FSMAware):
# pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which automatically
keep track of the state of the TFW FSM.
"""
def __init__(self, server_connector):
EventHandler.__init__(self, server_connector)
FSMAware.__init__(self)
def _event_callback(self, message):
self.process_message(message)
def handle_fsm_step(self, message):
self.handle_event(message, self.server_connector)

View File

@ -0,0 +1,190 @@
# pylint: disable=redefined-outer-name,attribute-defined-outside-init
from secrets import token_urlsafe
from random import randint
import pytest
from .event_handler_factory_base 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 simulate_message(self, message):
self._on_message(message)
def register_callback(self, callback):
self._on_message = callback
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 MockEventHandlerStub:
def __init__(self):
self.server_connector = None
self.last_message = None
self.cleaned_up = False
self.started = False
def start(self):
self.started = True
def cleanup(self):
self.cleaned_up = True
class MockEventHandler(MockEventHandlerStub):
# pylint: disable=unused-argument
def handle_event(self, message, server_connector):
self.last_message = message
class MockCallable(MockEventHandlerStub):
def __call__(self, message, server_connector):
self.last_message = message
@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 = MockEventHandlerStub()
def handle_event(message, server_connector):
raise RuntimeError(message, server_connector.keys)
mock_eh.handle_event = handle_event
assert not mock_eh.started
eh = MockEventHandlerFactory().build(mock_eh, keys=test_keys)
assert mock_eh.started
assert mock_eh.server_connector is eh.server_connector
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, test_msg):
mock_eh = MockEventHandler()
mock_eh.keys = test_keys
assert not mock_eh.started
eh = MockEventHandlerFactory().build(mock_eh)
assert mock_eh.server_connector.keys == test_keys
assert eh.server_connector is mock_eh.server_connector
assert mock_eh.started
assert not mock_eh.last_message
eh.server_connector.simulate_message(test_msg)
assert mock_eh.last_message == test_msg
assert not mock_eh.cleaned_up
EventHandler.stop_all_instances()
assert mock_eh.cleaned_up
def test_build_from_simple_object(test_keys, test_msg):
class SimpleMockEventHandler:
# pylint: disable=no-self-use
def handle_event(self, message, server_connector):
raise RuntimeError(message, server_connector)
mock_eh = SimpleMockEventHandler()
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
def test_build_from_callable(test_keys, test_msg):
mock_eh = MockCallable()
assert not mock_eh.started
eh = MockEventHandlerFactory().build(mock_eh, keys=test_keys)
assert mock_eh.started
assert mock_eh.server_connector is eh.server_connector
assert eh.server_connector.keys == test_keys
assert not mock_eh.last_message
eh.server_connector.simulate_message(test_msg)
assert mock_eh.last_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(test_keys):
eh = MockEventHandler()
with pytest.raises(ValueError):
MockEventHandlerFactory().build(eh)
def handle_event(*_):
pass
with pytest.raises(ValueError):
MockEventHandlerFactory().build(handle_event)
with pytest.raises(ValueError):
MockEventHandlerFactory().build(lambda msg, sc: None)
WithKeysEventHandler = EventHandler
WithKeysEventHandler.keys = test_keys
MockEventHandlerFactory().build(eh, event_handler_type=WithKeysEventHandler)
eh.keys = test_keys
MockEventHandlerFactory().build(eh)