mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Added basic signal system with corresponding tests
This commit is contained in:
parent
8f1f604708
commit
c22a08851f
3 changed files with 348 additions and 3 deletions
|
|
@ -5,6 +5,7 @@ This file contains the classes that allow a typeclass to use components.
|
|||
"""
|
||||
|
||||
from evennia.contrib.base_systems import components
|
||||
from evennia.contrib.base_systems.components import signals
|
||||
|
||||
|
||||
class ComponentProperty:
|
||||
|
|
@ -66,6 +67,7 @@ class ComponentHandler:
|
|||
self.db_names.append(component.name)
|
||||
self._add_component_tags(component)
|
||||
component.at_added(self.host)
|
||||
self.host.signals.add_object_listeners_and_responders(component)
|
||||
|
||||
def add_default(self, name):
|
||||
"""
|
||||
|
|
@ -87,6 +89,7 @@ class ComponentHandler:
|
|||
self.db_names.append(name)
|
||||
self._add_component_tags(new_component)
|
||||
new_component.at_added(self.host)
|
||||
self.host.signals.add_object_listeners_and_responders(new_component)
|
||||
|
||||
def _add_component_tags(self, component):
|
||||
"""
|
||||
|
|
@ -118,6 +121,7 @@ class ComponentHandler:
|
|||
self._remove_component_tags(component)
|
||||
component.at_removed(self.host)
|
||||
self.db_names.remove(component_name)
|
||||
self.host.signals.remove_object_listeners_and_responders(component)
|
||||
del self._loaded_components[component_name]
|
||||
else:
|
||||
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
|
||||
|
|
@ -140,6 +144,7 @@ class ComponentHandler:
|
|||
|
||||
self._remove_component_tags(instance)
|
||||
instance.at_removed(self.host)
|
||||
self.host.signals.remove_object_listeners_and_responders(instance)
|
||||
self.db_names.remove(name)
|
||||
|
||||
del self._loaded_components[name]
|
||||
|
|
@ -192,6 +197,7 @@ class ComponentHandler:
|
|||
if component:
|
||||
component_instance = component.load(self.host)
|
||||
self._set_component(component_instance)
|
||||
self.host.signals.add_object_listeners_and_responders(component_instance)
|
||||
else:
|
||||
message = f"Could not initialize runtime component {component_name} of {self.host.name}"
|
||||
raise ComponentDoesNotExist(message)
|
||||
|
|
@ -214,7 +220,7 @@ class ComponentHandler:
|
|||
return self.get(name)
|
||||
|
||||
|
||||
class ComponentHolderMixin(object):
|
||||
class ComponentHolderMixin:
|
||||
"""
|
||||
Mixin to add component support to a typeclass
|
||||
|
||||
|
|
@ -229,7 +235,17 @@ class ComponentHolderMixin(object):
|
|||
"""
|
||||
super(ComponentHolderMixin, self).at_init()
|
||||
setattr(self, "_component_handler", ComponentHandler(self))
|
||||
setattr(self, "_signal_handler", signals.SignalsHandler(self))
|
||||
self.components.initialize()
|
||||
self.signals.trigger("at_after_init")
|
||||
|
||||
def at_post_puppet(self, *args, **kwargs):
|
||||
super().at_post_puppet(*args, **kwargs)
|
||||
self.signals.trigger("at_post_puppet", *args, **kwargs)
|
||||
|
||||
def at_post_unpuppet(self, *args, **kwargs):
|
||||
super().at_post_unpuppet(*args, **kwargs)
|
||||
self.signals.trigger("at_post_unpuppet", *args, **kwargs)
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
|
|
@ -239,14 +255,17 @@ class ComponentHolderMixin(object):
|
|||
super().basetype_setup()
|
||||
component_names = []
|
||||
setattr(self, "_component_handler", ComponentHandler(self))
|
||||
setattr(self, "_signal_handler", signals.SignalsHandler(self))
|
||||
class_components = getattr(self, "_class_components", ())
|
||||
for component_name, values in class_components:
|
||||
component_class = components.get_component_class(component_name)
|
||||
component = component_class.create(self, **values)
|
||||
component_names.append(component_name)
|
||||
self.components._loaded_components[component_name] = component
|
||||
self.signals.add_object_listeners_and_responders(component)
|
||||
|
||||
self.db.component_names = component_names
|
||||
self.signals.trigger("at_basetype_setup")
|
||||
|
||||
def basetype_posthook_setup(self):
|
||||
"""
|
||||
|
|
@ -274,6 +293,10 @@ class ComponentHolderMixin(object):
|
|||
"""
|
||||
return self.components
|
||||
|
||||
@property
|
||||
def signals(self) -> signals.SignalsHandler:
|
||||
return getattr(self, "_signal_handler", None)
|
||||
|
||||
|
||||
class ComponentDoesNotExist(Exception):
|
||||
pass
|
||||
|
|
|
|||
112
evennia/contrib/base_systems/components/signals.py
Normal file
112
evennia/contrib/base_systems/components/signals.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
def as_listener(func=None, signal_name=None):
|
||||
if not func and signal_name:
|
||||
def wrapper(func):
|
||||
func._listener_signal_name = signal_name
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
signal_name = func.__name__
|
||||
func._listener_signal_name = signal_name
|
||||
return func
|
||||
|
||||
|
||||
def as_responder(func=None, signal_name=None):
|
||||
if not func and signal_name:
|
||||
def wrapper(func):
|
||||
func._responder_signal_name = signal_name
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
signal_name = func.__name__
|
||||
func._responder_signal_name = signal_name
|
||||
return func
|
||||
|
||||
|
||||
class SignalsHandler(object):
|
||||
def __init__(self, host):
|
||||
self.host = host
|
||||
self.listeners = {}
|
||||
self.responders = {}
|
||||
self.add_object_listeners_and_responders(host)
|
||||
|
||||
def add_listener(self, signal_name, callback):
|
||||
signal_listeners = self.listeners.setdefault(signal_name, [])
|
||||
if callback not in signal_listeners:
|
||||
signal_listeners.append(callback)
|
||||
|
||||
def add_responder(self, signal_name, callback):
|
||||
signal_responders = self.responders.setdefault(signal_name, [])
|
||||
if callback not in signal_responders:
|
||||
signal_responders.append(callback)
|
||||
|
||||
def remove_listener(self, signal_name, callback):
|
||||
signal_listeners = self.listeners.get(signal_name)
|
||||
if not signal_listeners:
|
||||
return
|
||||
|
||||
if callback in signal_listeners:
|
||||
signal_listeners.remove(callback)
|
||||
|
||||
def remove_responder(self, signal_name, callback):
|
||||
signal_responders = self.responders.get(signal_name)
|
||||
if not signal_responders:
|
||||
return
|
||||
|
||||
if callback in signal_responders:
|
||||
signal_responders.remove(callback)
|
||||
|
||||
def trigger(self, signal_name, *args, **kwargs):
|
||||
""" This method fires a signal but does not return anything """
|
||||
callbacks = self.listeners.get(signal_name)
|
||||
if not callbacks:
|
||||
return
|
||||
|
||||
for callback in callbacks:
|
||||
callback(*args, **kwargs)
|
||||
|
||||
def query(self, signal_name, *args, default=None, aggregate_func=None, **kwargs):
|
||||
""" This method fires a signal query that retrieves values """
|
||||
callbacks = self.responders.get(signal_name)
|
||||
|
||||
if not callbacks:
|
||||
default = [] if default is None else default
|
||||
if aggregate_func:
|
||||
return aggregate_func(default)
|
||||
return default
|
||||
|
||||
responses = []
|
||||
for callback in callbacks:
|
||||
response = callback(*args, **kwargs)
|
||||
if response is not None:
|
||||
responses.append(response)
|
||||
|
||||
if aggregate_func and responses:
|
||||
return aggregate_func(responses)
|
||||
|
||||
return responses
|
||||
|
||||
def add_object_listeners_and_responders(self, obj):
|
||||
type_host = type(obj)
|
||||
for att_name, att_obj in type_host.__dict__.items():
|
||||
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
|
||||
if listener_signal_name:
|
||||
callback = getattr(obj, att_name)
|
||||
self.add_listener(signal_name=listener_signal_name, callback=callback)
|
||||
|
||||
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
|
||||
if responder_signal_name:
|
||||
callback = getattr(obj, att_name)
|
||||
self.add_responder(signal_name=responder_signal_name, callback=callback)
|
||||
|
||||
def remove_object_listeners_and_responders(self, obj):
|
||||
type_host = type(obj)
|
||||
for att_name, att_obj in type_host.__dict__.items():
|
||||
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
|
||||
if listener_signal_name:
|
||||
callback = getattr(obj, att_name)
|
||||
self.remove_listener(signal_name=listener_signal_name, callback=callback)
|
||||
|
||||
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
|
||||
if responder_signal_name:
|
||||
callback = getattr(obj, att_name)
|
||||
self.remove_responder(signal_name=responder_signal_name, callback=callback)
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from evennia.contrib.base_systems.components import Component, DBField, TagField
|
||||
from evennia.contrib.base_systems.components import Component, DBField, TagField, signals
|
||||
from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin
|
||||
from evennia.contrib.base_systems.components.signals import as_listener
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import EvenniaTest, BaseEvenniaTest
|
||||
|
||||
|
||||
class ComponentTestA(Component):
|
||||
|
|
@ -186,3 +188,211 @@ class TestComponents(EvenniaTest):
|
|||
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
|
||||
assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
|
||||
assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")
|
||||
|
||||
|
||||
class CharWithSignal(ComponentHolderMixin, DefaultCharacter):
|
||||
@signals.as_listener
|
||||
def my_signal(self):
|
||||
setattr(self, 'my_signal_is_called', True)
|
||||
|
||||
@signals.as_listener
|
||||
def my_other_signal(self):
|
||||
setattr(self, 'my_other_signal_is_called', True)
|
||||
|
||||
@signals.as_responder
|
||||
def my_response(self):
|
||||
return 1
|
||||
|
||||
@signals.as_responder
|
||||
def my_other_response(self):
|
||||
return 2
|
||||
|
||||
|
||||
class ComponentWithSignal(Component):
|
||||
name = "test_signal_a"
|
||||
|
||||
@signals.as_listener
|
||||
def my_signal(self):
|
||||
setattr(self, 'my_signal_is_called', True)
|
||||
|
||||
@signals.as_listener
|
||||
def my_other_signal(self):
|
||||
setattr(self, 'my_other_signal_is_called', True)
|
||||
|
||||
@signals.as_responder
|
||||
def my_response(self):
|
||||
return 1
|
||||
|
||||
@signals.as_responder
|
||||
def my_other_response(self):
|
||||
return 2
|
||||
|
||||
@signals.as_responder
|
||||
def my_component_response(self):
|
||||
return 3
|
||||
|
||||
|
||||
class TestComponentSignals(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.char1 = create.create_object(
|
||||
CharWithSignal, key="Char",
|
||||
)
|
||||
|
||||
def test_host_can_register_as_listener(self):
|
||||
self.char1.signals.trigger("my_signal")
|
||||
|
||||
assert self.char1.my_signal_is_called
|
||||
assert not getattr(self.char1, 'my_other_signal_is_called', None)
|
||||
|
||||
def test_host_can_register_as_responder(self):
|
||||
responses = self.char1.signals.query("my_response")
|
||||
|
||||
assert 1 in responses
|
||||
assert 2 not in responses
|
||||
|
||||
def test_component_can_register_as_listener(self):
|
||||
char = self.char1
|
||||
char.components.add(ComponentWithSignal.create(char))
|
||||
char.signals.trigger("my_signal")
|
||||
|
||||
component = char.cmp.test_signal_a
|
||||
assert component.my_signal_is_called
|
||||
assert not getattr(component, 'my_other_signal_is_called', None)
|
||||
|
||||
def test_component_can_register_as_responder(self):
|
||||
char = self.char1
|
||||
char.components.add(ComponentWithSignal.create(char))
|
||||
responses = char.signals.query("my_response")
|
||||
|
||||
assert 1 in responses
|
||||
assert 2 not in responses
|
||||
|
||||
def test_signals_can_add_listener(self):
|
||||
result = []
|
||||
|
||||
def my_fake_listener():
|
||||
result.append(True)
|
||||
|
||||
self.char1.signals.add_listener("my_fake_signal", my_fake_listener)
|
||||
self.char1.signals.trigger("my_fake_signal")
|
||||
|
||||
assert result
|
||||
|
||||
def test_signals_can_add_responder(self):
|
||||
def my_fake_responder():
|
||||
return 1
|
||||
|
||||
self.char1.signals.add_responder("my_fake_response", my_fake_responder)
|
||||
responses = self.char1.signals.query("my_fake_response")
|
||||
|
||||
assert 1 in responses
|
||||
|
||||
def test_signals_can_remove_listener(self):
|
||||
result = []
|
||||
|
||||
def my_fake_listener():
|
||||
result.append(True)
|
||||
|
||||
self.char1.signals.add_listener("my_fake_signal", my_fake_listener)
|
||||
self.char1.signals.remove_listener("my_fake_signal", my_fake_listener)
|
||||
self.char1.signals.trigger("my_fake_signal")
|
||||
|
||||
assert not result
|
||||
|
||||
def test_signals_can_remove_responder(self):
|
||||
def my_fake_responder():
|
||||
return 1
|
||||
|
||||
self.char1.signals.add_responder("my_fake_response", my_fake_responder)
|
||||
self.char1.signals.remove_responder("my_fake_response", my_fake_responder)
|
||||
responses = self.char1.signals.query("my_fake_response")
|
||||
|
||||
assert not responses
|
||||
|
||||
def test_signals_can_trigger_with_args(self):
|
||||
result = []
|
||||
|
||||
def my_fake_listener(arg1, kwarg1):
|
||||
result.append((arg1, kwarg1))
|
||||
|
||||
self.char1.signals.add_listener("my_fake_signal", my_fake_listener)
|
||||
self.char1.signals.trigger("my_fake_signal", 1, kwarg1=2)
|
||||
|
||||
assert (1, 2) in result
|
||||
|
||||
def test_signals_can_query_with_args(self):
|
||||
def my_fake_responder(arg1, kwarg1):
|
||||
return (arg1, kwarg1)
|
||||
|
||||
self.char1.signals.add_responder("my_fake_response", my_fake_responder)
|
||||
responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2)
|
||||
|
||||
assert (1, 2) in responses
|
||||
|
||||
def test_signals_trigger_does_not_fail_without_listener(self):
|
||||
self.char1.signals.trigger("some_unknown_signal")
|
||||
|
||||
def test_signals_query_does_not_fail_wihout_responders(self):
|
||||
self.char1.signals.query("no_responders_allowed")
|
||||
|
||||
def test_signals_query_with_aggregate(self):
|
||||
def my_fake_responder(arg1, kwarg1):
|
||||
return (arg1, kwarg1)
|
||||
|
||||
self.char1.signals.add_responder("my_fake_response", my_fake_responder)
|
||||
responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2)
|
||||
|
||||
assert (1, 2) in responses
|
||||
|
||||
def test_signals_can_add_object_listeners_and_responders(self):
|
||||
result = []
|
||||
|
||||
class FakeObj:
|
||||
@as_listener
|
||||
def my_signal(self):
|
||||
result.append(True)
|
||||
|
||||
self.char1.signals.add_object_listeners_and_responders(FakeObj())
|
||||
self.char1.signals.trigger("my_signal")
|
||||
|
||||
assert result
|
||||
|
||||
def test_signals_can_remove_object_listeners_and_responders(self):
|
||||
result = []
|
||||
|
||||
class FakeObj:
|
||||
@as_listener
|
||||
def my_signal(self):
|
||||
result.append(True)
|
||||
|
||||
obj = FakeObj()
|
||||
self.char1.signals.add_object_listeners_and_responders(obj)
|
||||
self.char1.signals.remove_object_listeners_and_responders(obj)
|
||||
self.char1.signals.trigger("my_signal")
|
||||
|
||||
assert not result
|
||||
|
||||
def test_component_handler_signals_connected_when_adding_default_component(self):
|
||||
char = self.char1
|
||||
char.components.add_default("test_signal_a")
|
||||
responses = char.signals.query("my_component_response")
|
||||
|
||||
assert 3 in responses
|
||||
|
||||
def test_component_handler_signals_disconnected_when_removing_component(self):
|
||||
char = self.char1
|
||||
comp = ComponentWithSignal.create(char)
|
||||
char.components.add(comp)
|
||||
char.components.remove(comp)
|
||||
responses = char.signals.query("my_component_response")
|
||||
|
||||
assert not responses
|
||||
|
||||
def test_component_handler_signals_disconnected_when_removing_component_by_name(self):
|
||||
char = self.char1
|
||||
char.components.add_default("test_signal_a")
|
||||
char.components.remove_by_name("test_signal_a")
|
||||
responses = char.signals.query("my_component_response")
|
||||
|
||||
assert not responses
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue