mirror of
				https://github.com/avatao-content/baseimage-tutorial-framework
				synced 2025-11-04 05:12:54 +00:00 
			
		
		
		
	Add new EventHandler stuff as per interface segregation principle
This commit is contained in:
		
							
								
								
									
										27
									
								
								lib/tfw/event_handlers/event_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/tfw/event_handlers/event_handler.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										51
									
								
								lib/tfw/event_handlers/event_handler_factory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								lib/tfw/event_handlers/event_handler_factory.py
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										147
									
								
								lib/tfw/event_handlers/test_event_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								lib/tfw/event_handlers/test_event_handler.py
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
		Reference in New Issue
	
	Block a user