Merge pull request #3367 from ChrisLR/components-refactoring

[Components] Refactor Component registering with cherry-picked additions
This commit is contained in:
Griatch 2024-02-25 10:32:48 +01:00 committed by GitHub
commit 0edcebea0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 436 additions and 265 deletions

View file

@ -30,12 +30,20 @@ class Character(ComponentHolderMixin, DefaultCharacter):
# ... # ...
``` ```
Components need to inherit the Component class directly and require a name. Components need to inherit the Component class and require a unique name.
Components may inherit from other components but must specify another name.
You can assign the same 'slot' to both components to have alternative implementations.
```python ```python
from evennia.contrib.base_systems.components import Component from evennia.contrib.base_systems.components import Component
class Health(Component): class Health(Component):
name = "health" name = "health"
class ItemHealth(Health):
name = "item_health"
slot = "health"
``` ```
Components may define DBFields or NDBFields at the class level. Components may define DBFields or NDBFields at the class level.
@ -103,7 +111,10 @@ character.components.add(vampirism)
... ...
vampirism_from_elsewhere = character.components.get("vampirism") vampirism = character.components.get("vampirism")
# Alternatively
vampirism = character.cmp.vampirism
``` ```
Keep in mind that all components must be imported to be visible in the listing. Keep in mind that all components must be imported to be visible in the listing.
@ -128,6 +139,14 @@ from typeclasses.components import health
``` ```
Both of the above examples will work. Both of the above examples will work.
## Known Issues
Assigning mutable default values such as a list to a DBField will share it across instances.
To avoid this, you must set autocreate=True on the field, like this.
```python
health = DBField(default=[], autocreate=True)
```
## Full Example ## Full Example
```python ```python
from evennia.contrib.base_systems import components from evennia.contrib.base_systems import components

View file

@ -7,23 +7,16 @@ This helps writing isolated code and reusing it over multiple objects.
See the docs for more information. See the docs for more information.
""" """
from evennia.contrib.base_systems.components import exceptions
from evennia.contrib.base_systems.components.listing import COMPONENT_LISTING, get_component_class
from evennia.contrib.base_systems.components.component import Component from evennia.contrib.base_systems.components.component import Component
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField, TagField from evennia.contrib.base_systems.components.dbfield import (
DBField,
NDBField,
TagField
)
from evennia.contrib.base_systems.components.holder import ( from evennia.contrib.base_systems.components.holder import (
ComponentHolderMixin, ComponentHolderMixin,
ComponentProperty, ComponentProperty,
) )
def get_component_class(component_name):
subclasses = Component.__subclasses__()
component_class = next((sc for sc in subclasses if sc.name == component_name), None)
if component_class is None:
message = (
f"Component named {component_name} has not been found. "
f"Make sure it has been imported before being used."
)
raise Exception(message)
return component_class

View file

@ -3,10 +3,41 @@ Components - ChrisLR 2022
This file contains the base class to inherit for creating new components. This file contains the base class to inherit for creating new components.
""" """
import itertools
from evennia.commands.cmdset import CmdSet
from evennia.contrib.base_systems.components import COMPONENT_LISTING, exceptions
class Component: class BaseComponent(type):
"""
This is the metaclass for components,
responsible for registering components to the listing.
"""
@classmethod
def __new__(cls, *args):
"""
Every class that uses this metaclass will be registered
as a component in the Component Listing using its name.
All of them require a unique name.
"""
new_type = super().__new__(*args)
if new_type.__base__ == object:
return new_type
name = getattr(new_type, "name", None)
if not name:
raise ValueError(f"Component {new_type} requires a name.")
if existing_type := COMPONENT_LISTING.get(name):
if not str(new_type) == str(existing_type):
raise ValueError(f"Component name {name} is a duplicate, must be unique.")
else:
COMPONENT_LISTING[name] = new_type
return new_type
class Component(metaclass=BaseComponent):
""" """
This is the base class for components. This is the base class for components.
Any component must inherit from this class to be considered for usage. Any component must inherit from this class to be considered for usage.
@ -14,10 +45,15 @@ class Component:
Each Component must supply the name, it is used as a slot name but also part of the attribute key. Each Component must supply the name, it is used as a slot name but also part of the attribute key.
""" """
__slots__ = ('host',)
name = "" name = ""
slot = None
_fields = {}
def __init__(self, host=None): def __init__(self, host=None):
assert self.name, "All Components must have a Name" assert self.name, "All Components must have a name"
self.host = host self.host = host
@classmethod @classmethod
@ -61,8 +97,8 @@ class Component:
""" """
This deletes all component attributes from the host's db This deletes all component attributes from the host's db
""" """
for attribute in self._all_db_field_names: for name in self._fields.keys():
delattr(self, attribute) delattr(self, name)
@classmethod @classmethod
def load(cls, host): def load(cls, host):
@ -77,7 +113,6 @@ class Component:
Component: The loaded instance of the component Component: The loaded instance of the component
""" """
return cls(host) return cls(host)
def at_added(self, host): def at_added(self, host):
@ -88,12 +123,8 @@ class Component:
host (object): The host typeclass instance host (object): The host typeclass instance
""" """
if self.host and self.host != host:
if self.host: raise exceptions.InvalidComponentError("Components must not register twice!")
if self.host == host:
return
else:
raise ComponentRegisterError("Components must not register twice!")
self.host = host self.host = host
@ -106,7 +137,8 @@ class Component:
""" """
if host != self.host: if host != self.host:
raise ComponentRegisterError("Component attempted to remove from the wrong host.") raise ValueError("Component attempted to remove from the wrong host.")
self.host = None self.host = None
@property @property
@ -131,25 +163,14 @@ class Component:
""" """
return self.host.nattributes return self.host.nattributes
@property @classmethod
def _all_db_field_names(self): def add_field(cls, name, field):
return itertools.chain(self.db_field_names, self.ndb_field_names) cls._fields[name] = field
@property @classmethod
def db_field_names(self): def get_fields(cls):
db_fields = getattr(self, "_db_fields", {}) return tuple(cls._fields.values())
return db_fields.keys()
@property @classmethod
def ndb_field_names(self): def get_component_slot(cls):
ndb_fields = getattr(self, "_ndb_fields", {}) return cls.slot or cls.name
return ndb_fields.keys()
@property
def tag_field_names(self):
tag_fields = getattr(self, "_tag_fields", {})
return tag_fields.keys()
class ComponentRegisterError(Exception):
pass

View file

@ -3,8 +3,13 @@ Components - ChrisLR 2022
This file contains the Descriptors used to set Fields in Components This file contains the Descriptors used to set Fields in Components
""" """
import typing
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty
if typing.TYPE_CHECKING:
from evennia.contrib.base_systems.components import Component
class DBField(AttributeProperty): class DBField(AttributeProperty):
""" """
@ -13,21 +18,40 @@ class DBField(AttributeProperty):
It uses AttributeProperty under the hood but prefixes the key with the component name. It uses AttributeProperty under the hood but prefixes the key with the component name.
""" """
def __set_name__(self, owner, name): def __init__(self, default=None, autocreate=False, **kwargs):
super().__init__(default=default, autocreate=autocreate, **kwargs)
def __set_name__(self, owner: 'Component', name):
""" """
Called when descriptor is first assigned to the class. Called when descriptor is first assigned to the class.
Args: Args:
owner (object): The component classF on which this is set owner (Component): The component classF on which this is set
name (str): The name that was used to set the DBField. name (str): The name that was used to set the DBField.
""" """
key = f"{owner.name}::{name}" self._key = f"{owner.get_component_slot()}::{name}"
self._key = key owner.add_field(name, self)
db_fields = getattr(owner, "_db_fields", None)
if db_fields is None: def at_added(self, component):
db_fields = {} """
setattr(owner, "_db_fields", db_fields) Called when the parent component is added to a host.
db_fields[name] = self
Args:
component (Component): The component instance being added.
"""
if self._autocreate:
self.__get__(component, type(component))
def at_removed(self, component):
"""
Called when the parent component is removed from a host.
Args:
component (Component): The component instance being removed.
"""
self.__delete__(component)
class NDBField(NAttributeProperty): class NDBField(NAttributeProperty):
@ -37,21 +61,35 @@ class NDBField(NAttributeProperty):
It uses NAttributeProperty under the hood but prefixes the key with the component name. It uses NAttributeProperty under the hood but prefixes the key with the component name.
""" """
def __set_name__(self, owner, name): def __set_name__(self, owner: 'Component', name):
""" """
Called when descriptor is first assigned to the class. Called when descriptor is first assigned to the class.
Args: Args:
owner (object): The component class on which this is set owner (Component): The component class on which this is set
name (str): The name that was used to set the DBField. name (str): The name that was used to set the DBField.
""" """
key = f"{owner.name}::{name}" self._key = f"{owner.get_component_slot()}::{name}"
self._key = key owner.add_field(name, self)
ndb_fields = getattr(owner, "_ndb_fields", None)
if ndb_fields is None: def at_added(self, component):
ndb_fields = {} """
setattr(owner, "_ndb_fields", ndb_fields) Called when the parent component is added to a host.
ndb_fields[name] = self
Args:
component (Component): The component instance being added.
"""
if self._autocreate:
self.__set__(component, self._default)
def at_removed(self, component):
"""
Called when the parent component is removed from a host.
Args:
component (Component): The component instance being removed.
"""
self.__delete__(component)
class TagField: class TagField:
@ -70,17 +108,13 @@ class TagField:
self._default = default self._default = default
self._enforce_single = enforce_single self._enforce_single = enforce_single
def __set_name__(self, owner, name): def __set_name__(self, owner: 'Component', name):
""" """
Called when TagField is first assigned to the class. Called when TagField is first assigned to the class.
It is called with the component class and the name of the field. It is called with the component class and the name of the field.
""" """
self._category_key = f"{owner.name}::{name}" self._category_key = f"{owner.get_component_slot()}::{name}"
tag_fields = getattr(owner, "_tag_fields", None) owner.add_field(name, self)
if tag_fields is None:
tag_fields = {}
setattr(owner, "_tag_fields", tag_fields)
tag_fields[name] = self
def __get__(self, instance, owner): def __get__(self, instance, owner):
""" """
@ -114,3 +148,22 @@ class TagField:
It is called with the component instance. It is called with the component instance.
""" """
instance.host.tags.clear(category=self._category_key) instance.host.tags.clear(category=self._category_key)
def at_added(self, component):
"""
Called when the parent component is added to a host.
Args:
component (Component): The component instance being added.
"""
if self._default:
self.__set__(component, self._default)
def at_removed(self, component):
"""
Called when the parent component is removed from a host.
Args:
component (Component): The component instance being removed.
"""
self.__delete__(component)

View file

@ -0,0 +1,10 @@
class InvalidComponentError(ValueError):
pass
class ComponentDoesNotExist(ValueError):
pass
class ComponentIsNotRegistered(ValueError):
pass

View file

@ -5,7 +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 import components
from evennia.contrib.base_systems.components import signals from evennia.contrib.base_systems.components import signals, exceptions, get_component_class
class ComponentProperty: class ComponentProperty:
@ -17,19 +17,26 @@ class ComponentProperty:
Defaults can be overridden for this typeclass by passing kwargs Defaults can be overridden for this typeclass by passing kwargs
""" """
def __init__(self, component_name, **kwargs): def __init__(self, name, **kwargs):
""" """
Initializes the descriptor Initializes the descriptor
Args: Args:
component_name (str): The name of the component name (str): The name of the component
**kwargs (any): Key=Values overriding default values of the component **kwargs (any): Key=Values overriding default values of the component
""" """
self.component_name = component_name self.name = name
self.values = kwargs self.values = kwargs
self.component_class = None
self.slot_name = None
def __get__(self, instance, owner): def __get__(self, instance, owner):
component = instance.components.get(self.component_name) if not self.component_class:
component_class = get_component_class(self.name)
self.component_class = component_class
self.slot_name = component_class.get_component_slot()
component = instance.components.get(self.slot_name)
return component return component
def __set__(self, instance, value): def __set__(self, instance, value):
@ -37,13 +44,11 @@ class ComponentProperty:
def __set_name__(self, owner, name): def __set_name__(self, owner, name):
# Retrieve the class_components set on the direct class only # Retrieve the class_components set on the direct class only
class_components = owner.__dict__.get("_class_components") class_components = owner.__dict__.get("_class_components", [])
if not class_components: if not class_components:
# Create a new list, including inherited class components
class_components = list(getattr(owner, "_class_components", []))
setattr(owner, "_class_components", class_components) setattr(owner, "_class_components", class_components)
class_components.append((self.component_name, self.values)) class_components.append((self.name, self.values))
class ComponentHandler: class ComponentHandler:
@ -57,7 +62,7 @@ class ComponentHandler:
self.host = host self.host = host
self._loaded_components = {} self._loaded_components = {}
def add(self, component): def add(self, component: components.Component):
""" """
Method to add a Component to a host. Method to add a Component to a host.
It caches the loaded component and appends its name to the host's component name list. It caches the loaded component and appends its name to the host's component name list.
@ -67,16 +72,19 @@ class ComponentHandler:
component (object): The 'loaded' component instance to add. component (object): The 'loaded' component instance to add.
""" """
component_name = component.name
self.db_names.append(component_name)
self.host.tags.add(component_name, category="components")
self._set_component(component) self._set_component(component)
self.db_names.append(component.name) for field in component.get_fields():
self._add_component_tags(component) field.at_added(component)
component.at_added(self.host) component.at_added(self.host)
self.host.signals.add_object_listeners_and_responders(component)
def add_default(self, name): def add_default(self, name):
""" """
Method to add a Component initialized to default values on a host. Method to add a Component initialized to default values on a host.
It will retrieve the proper component and instanciate it with 'default_create'. It will retrieve the proper component and instantiate it with 'default_create'.
It will cache this new component and add it to its list. It will cache this new component and add it to its list.
It will also call the component's 'at_added' method, passing its host. It will also call the component's 'at_added' method, passing its host.
@ -84,33 +92,11 @@ class ComponentHandler:
name (str): The name of the component class to add. name (str): The name of the component class to add.
""" """
component = components.get_component_class(name) component_class = components.get_component_class(name)
if not component: component_instance = component_class.default_create(self.host)
raise ComponentDoesNotExist(f"Component {name} does not exist.") self.add(component_instance)
new_component = component.default_create(self.host) def remove(self, component: components.Component):
self._set_component(new_component)
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):
"""
Private method that adds the Tags set on a Component via TagFields
It will also add the name of the component so objects can be filtered
by the components the implement.
Args:
component (object): The component instance that is added.
"""
self.host.tags.add(component.name, category="components")
for tag_field_name in component.tag_field_names:
default_tag = type(component).__dict__[tag_field_name]._default
if default_tag:
setattr(component, tag_field_name, default_tag)
def remove(self, component):
""" """
Method to remove a component instance from a host. Method to remove a component instance from a host.
It removes the component from the cache and listing. It removes the component from the cache and listing.
@ -120,18 +106,24 @@ class ComponentHandler:
component (object): The component instance to remove. component (object): The component instance to remove.
""" """
component_name = component.name name = component.name
if component_name in self._loaded_components: slot_name = component.get_component_slot()
self._remove_component_tags(component) if not self.has(slot_name):
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 = ( message = (
f"Cannot remove {component_name} from {self.host.name} as it is not registered." f"Cannot remove {name} from {self.host.name} as it is not registered."
) )
raise ComponentIsNotRegistered(message) raise exceptions.ComponentIsNotRegistered(message)
for field in component.get_fields():
field.at_removed(component)
component.at_removed(self.host)
self.host.tags.remove(component.name, category="components")
self.host.signals.remove_object_listeners_and_responders(component)
self.db_names.remove(name)
del self._loaded_components[slot_name]
def remove_by_name(self, name): def remove_by_name(self, name):
""" """
@ -140,49 +132,25 @@ class ComponentHandler:
It will call the component's 'at_removed' method. It will call the component's 'at_removed' method.
Args: Args:
name (str): The name of the component to remove. name (str): The name of the component to remove or its slot.
""" """
instance = self.get(name) instance = self.get(name)
if not instance: if not instance:
message = f"Cannot remove {name} from {self.host.name} as it is not registered." message = f"Cannot remove {name} from {self.host.name} as it is not registered."
raise ComponentIsNotRegistered(message) raise exceptions.ComponentIsNotRegistered(message)
self._remove_component_tags(instance) self.remove(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] def get(self, name: str) -> components.Component | None:
def _remove_component_tags(self, component):
"""
Private method that will remove the Tags set on a Component via TagFields
It will also remove the component name tag.
Args:
component (object): The component instance that is removed.
"""
self.host.tags.remove(component.name, category="components")
for tag_field_name in component.tag_field_names:
delattr(component, tag_field_name)
def get(self, name):
"""
Method to retrieve a cached Component instance by its name.
Args:
name (str): The name of the component to retrieve.
"""
return self._loaded_components.get(name) return self._loaded_components.get(name)
def has(self, name): def has(self, name: str) -> bool:
""" """
Method to check if a component is registered and ready. Method to check if a component is registered and ready.
Args: Args:
name (str): The name of the component. name (str): The name of the component or the slot.
""" """
return name in self._loaded_components return name in self._loaded_components
@ -203,26 +171,35 @@ class ComponentHandler:
if component: if component:
component_instance = component.load(self.host) component_instance = component.load(self.host)
self._set_component(component_instance) self._set_component(component_instance)
self.host.signals.add_object_listeners_and_responders(component_instance)
else: else:
message = ( message = (
f"Could not initialize runtime component {component_name} of {self.host.name}" f"Could not initialize runtime component {component_name} of {self.host.name}"
) )
raise ComponentDoesNotExist(message) raise exceptions.ComponentDoesNotExist(message)
def _set_component(self, component): def _set_component(self, component):
self._loaded_components[component.name] = component """
Sets the loaded component in this instance.
"""
slot_name = component.get_component_slot()
self._loaded_components[slot_name] = component
self.host.signals.add_object_listeners_and_responders(component)
@property @property
def db_names(self): def db_names(self):
""" """
Property shortcut to retrieve the registered component names Property shortcut to retrieve the registered component keys
Returns: Returns:
component_names (iterable): The name of each component that is registered component_names (iterable): The name of each component that is registered
""" """
return self.host.attributes.get("component_names") names = self.host.attributes.get("component_names")
if names is None:
self.host.db.component_names = []
names = self.host.db.component_names
return names
def __getattr__(self, name): def __getattr__(self, name):
return self.get(name) return self.get(name)
@ -236,7 +213,6 @@ class ComponentHolderMixin:
All registered components are initialized on the typeclass. All registered components are initialized on the typeclass.
They will be of None value if not present in the class components or runtime components. They will be of None value if not present in the class components or runtime components.
""" """
def at_init(self): def at_init(self):
""" """
Method that initializes the ComponentHandler. Method that initializes the ComponentHandler.
@ -261,28 +237,16 @@ class ComponentHolderMixin:
components that were set on the typeclass using ComponentProperty. components that were set on the typeclass using ComponentProperty.
""" """
super().basetype_setup() super().basetype_setup()
component_names = []
setattr(self, "_component_handler", ComponentHandler(self)) setattr(self, "_component_handler", ComponentHandler(self))
setattr(self, "_signal_handler", signals.SignalsHandler(self)) setattr(self, "_signal_handler", signals.SignalsHandler(self))
class_components = getattr(self, "_class_components", ()) class_components = self._get_class_components()
for component_name, values in class_components: for component_name, values in class_components:
component_class = components.get_component_class(component_name) component_class = components.get_component_class(component_name)
component = component_class.create(self, **values) component = component_class.create(self, **values)
component_names.append(component_name) self.components.add(component)
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") self.signals.trigger("at_basetype_setup")
def basetype_posthook_setup(self):
"""
Method that add component related tags that were set using ComponentProperty.
"""
super().basetype_posthook_setup()
for component in self.components._loaded_components.values():
self.components._add_component_tags(component)
@property @property
def components(self) -> ComponentHandler: def components(self) -> ComponentHandler:
""" """
@ -305,10 +269,27 @@ class ComponentHolderMixin:
def signals(self) -> signals.SignalsHandler: def signals(self) -> signals.SignalsHandler:
return getattr(self, "_signal_handler", None) return getattr(self, "_signal_handler", None)
def _get_class_components(self):
class_components = {}
class ComponentDoesNotExist(Exception): def base_type_iterator():
pass base_stack = [type(self)]
while base_stack:
_base_type = base_stack.pop()
yield _base_type
base_stack.extend(_base_type.__bases__)
for base_type in base_type_iterator():
base_class_components = getattr(base_type, "_class_components", ())
for cmp_name, cmp_values in base_class_components:
cmp_class = get_component_class(cmp_name)
cmp_slot = cmp_class.get_component_slot()
class_components[cmp_slot] = (cmp_name, cmp_values)
class ComponentIsNotRegistered(Exception): instance_components = getattr(self, "_class_components", ())
pass for cmp_name, cmp_values in instance_components:
cmp_class = get_component_class(cmp_name)
cmp_slot = cmp_class.get_component_slot()
class_components[cmp_slot] = (cmp_name, cmp_values)
return tuple(class_components.values())

View file

@ -0,0 +1,20 @@
from evennia.contrib.base_systems.components import exceptions
COMPONENT_LISTING = {}
def get_component_class(name):
"""
Retrieves a component from the listing using a name
Args:
name (str): The unique name of the component
"""
component_class = COMPONENT_LISTING.get(name)
if component_class is None:
message = (
f"Component with name {name} has not been found. "
f"Make sure it has been imported before being used."
)
raise exceptions.ComponentDoesNotExist(message)
return component_class

View file

@ -179,8 +179,15 @@ class SignalsHandler(object):
Args: Args:
obj (object): The instance of an object to connect to this handler. obj (object): The instance of an object to connect to this handler.
""" """
type_host = type(obj) obj_type = type(obj)
for att_name, att_obj in type_host.__dict__.items(): for att_name in dir(obj_type):
if att_name.startswith("__"):
continue
att_obj = getattr(obj_type, att_name, None)
if att_obj is None:
continue
listener_signal_name = getattr(att_obj, "_listener_signal_name", None) listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name: if listener_signal_name:
callback = getattr(obj, att_name) callback = getattr(obj, att_name)
@ -198,8 +205,14 @@ class SignalsHandler(object):
Args: Args:
obj (object): The instance of an object to disconnect from this handler. obj (object): The instance of an object to disconnect from this handler.
""" """
type_host = type(obj) for att_name in dir(obj):
for att_name, att_obj in type_host.__dict__.items(): if att_name.startswith("__"):
continue
att_obj = getattr(obj, att_name, None)
if att_obj is None:
continue
listener_signal_name = getattr(att_obj, "_listener_signal_name", None) listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name: if listener_signal_name:
callback = getattr(obj, att_name) callback = getattr(obj, att_name)

View file

@ -17,13 +17,32 @@ from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest
class ComponentTestA(Component): class ComponentTestA(Component):
name = "test_a" name = "test_a"
my_int = DBField(default=1) my_int = DBField(default=1)
my_list = DBField(default=[]) my_list = DBField(default=[], autocreate=True)
class ShadowedComponentTestA(ComponentTestA):
name = "shadowed_test_a"
slot = 'ic_a'
class InheritedComponentTestA(ComponentTestA):
name = "inherited_test_a"
slot = 'ic_a'
my_other_int = DBField(default=2)
class ReplacementComponentTestA(InheritedComponentTestA):
name = "replacement_inherited_test_a"
slot = "ic_a"
replacement_field = DBField(default=6)
class ComponentTestB(Component): class ComponentTestB(Component):
name = "test_b" name = "test_b"
my_int = DBField(default=1) my_int = DBField(default=1)
my_list = DBField(default=[]) my_list = DBField(default=[], autocreate=True)
default_tag = TagField(default="initial_value") default_tag = TagField(default="initial_value")
single_tag = TagField(enforce_single=True) single_tag = TagField(enforce_single=True)
multiple_tags = TagField() multiple_tags = TagField()
@ -33,13 +52,31 @@ class ComponentTestB(Component):
class RuntimeComponentTestC(Component): class RuntimeComponentTestC(Component):
name = "test_c" name = "test_c"
my_int = DBField(default=6) my_int = DBField(default=6)
my_dict = DBField(default={}) my_dict = DBField(default={}, autocreate=True)
added_tag = TagField(default="added_value") added_tag = TagField(default="added_value")
class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter): class ComponentTestD(Component):
name = "test_d"
mixed_in = DBField(default=8)
class ShadowedCharacterMixin:
ic_a = ComponentProperty("shadowed_test_a")
class CharacterMixinWithComponents:
ic_a = ComponentProperty("inherited_test_a", my_other_int=33)
test_d = ComponentProperty('test_d')
class CharacterWithComponents(
ComponentHolderMixin, ShadowedCharacterMixin, CharacterMixinWithComponents, DefaultCharacter
):
test_a = ComponentProperty("test_a") test_a = ComponentProperty("test_a")
test_b = ComponentProperty("test_b", my_int=3, my_list=[1, 2, 3]) test_b = ComponentProperty("test_b", my_int=3, my_list=[1, 2, 3])
ic_a = ComponentProperty("inherited_test_a", my_other_int=4)
class InheritedTCWithComponents(CharacterWithComponents): class InheritedTCWithComponents(CharacterWithComponents):
@ -50,53 +87,69 @@ class TestComponents(EvenniaTest):
character_typeclass = CharacterWithComponents character_typeclass = CharacterWithComponents
def test_character_has_class_components(self): def test_character_has_class_components(self):
assert self.char1.test_a self.assertTrue(self.char1.test_a)
assert self.char1.test_b self.assertTrue(self.char1.test_b)
def test_inherited_typeclass_does_not_include_child_class_components(self): def test_inherited_typeclass_does_not_include_child_class_components(self):
char_with_c = create.create_object( char_with_c = create.create_object(
InheritedTCWithComponents, key="char_with_c", location=self.room1, home=self.room1 InheritedTCWithComponents, key="char_with_c", location=self.room1, home=self.room1
) )
assert self.char1.test_a self.assertTrue(self.char1.test_a)
assert not self.char1.cmp.get("test_c") self.assertFalse(self.char1.cmp.get("test_c"))
assert char_with_c.test_c self.assertTrue(char_with_c.test_c)
def test_character_instances_components_properly(self): def test_character_instances_components_properly(self):
assert isinstance(self.char1.test_a, ComponentTestA) self.assertIsInstance(self.char1.test_a, ComponentTestA)
assert isinstance(self.char1.test_b, ComponentTestB) self.assertIsInstance(self.char1.test_b, ComponentTestB)
def test_character_assigns_default_value(self): def test_character_assigns_default_value(self):
assert self.char1.test_a.my_int == 1 self.assertEquals(self.char1.test_a.my_int, 1)
assert self.char1.test_a.my_list == [] self.assertEquals(self.char1.test_a.my_list, [])
def test_character_assigns_default_provided_values(self): def test_character_assigns_default_provided_values(self):
assert self.char1.test_b.my_int == 3 self.assertEquals(self.char1.test_b.my_int, 3)
assert self.char1.test_b.my_list == [1, 2, 3] self.assertEquals(self.char1.test_b.my_list, [1, 2, 3])
def test_character_has_autocreated_values(self):
att_name = "test_b::my_list"
self.assertEquals(self.char1.attributes.get(att_name), [1, 2, 3])
def test_component_inheritance_properly_overrides_slots(self):
self.assertEquals(self.char1.ic_a.name, "inherited_test_a")
component_names = set(c[0] for c in self.char1._get_class_components())
self.assertNotIn("shadowed_test_a", component_names)
def test_component_inheritance_assigns_proper_values(self):
self.assertEquals(self.char1.ic_a.my_int, 1)
self.assertEquals(self.char1.ic_a.my_other_int, 4)
def test_host_mixins_assigns_components(self):
self.assertEquals(self.char1.test_d.mixed_in, 8)
def test_character_can_register_runtime_component(self): def test_character_can_register_runtime_component(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct) self.char1.components.add(rct)
test_c = self.char1.components.get("test_c") test_c = self.char1.components.get("test_c")
assert test_c self.assertTrue(test_c)
assert test_c.my_int == 6 self.assertEquals(test_c.my_int, 6)
assert test_c.my_dict == {} self.assertEquals(test_c.my_dict, {})
def test_handler_can_add_default_component(self): def test_handler_can_add_default_component(self):
self.char1.components.add_default("test_c") self.char1.components.add_default("test_c")
test_c = self.char1.components.get("test_c") test_c = self.char1.components.get("test_c")
assert test_c self.assertTrue(test_c)
assert test_c.my_int == 6 self.assertEquals(test_c.my_int, 6)
def test_handler_has_returns_true_for_any_components(self): def test_handler_has_returns_true_for_any_components(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
handler = self.char1.components handler = self.char1.components
handler.add(rct) handler.add(rct)
assert handler.has("test_a") self.assertTrue(handler.has("test_a"))
assert handler.has("test_b") self.assertTrue(handler.has("test_b"))
assert handler.has("test_c") self.assertTrue(handler.has("test_c"))
def test_can_remove_component(self): def test_can_remove_component(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
@ -104,9 +157,9 @@ class TestComponents(EvenniaTest):
handler.add(rct) handler.add(rct)
handler.remove(rct) handler.remove(rct)
assert handler.has("test_a") self.assertTrue(handler.has("test_a"))
assert handler.has("test_b") self.assertTrue(handler.has("test_b"))
assert not handler.has("test_c") self.assertFalse(handler.has("test_c"))
def test_can_remove_component_by_name(self): def test_can_remove_component_by_name(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
@ -114,9 +167,9 @@ class TestComponents(EvenniaTest):
handler.add(rct) handler.add(rct)
handler.remove_by_name("test_c") handler.remove_by_name("test_c")
assert handler.has("test_a") self.assertTrue(handler.has("test_a"))
assert handler.has("test_b") self.assertTrue(handler.has("test_b"))
assert not handler.has("test_c") self.assertFalse(handler.has("test_c"))
def test_cannot_replace_component(self): def test_cannot_replace_component(self):
with self.assertRaises(Exception): with self.assertRaises(Exception):
@ -127,76 +180,76 @@ class TestComponents(EvenniaTest):
handler = self.char1.components handler = self.char1.components
handler.add(rct) handler.add(rct)
assert handler.get("test_c") is rct self.assertIs(handler.get("test_c"), rct)
def test_can_access_component_regular_get(self): def test_can_access_component_regular_get(self):
assert self.char1.cmp.test_a is self.char1.components.get("test_a") self.assertIs(self.char1.cmp.test_a, self.char1.components.get("test_a"))
def test_returns_none_with_regular_get_when_no_attribute(self): def test_returns_none_with_regular_get_when_no_attribute(self):
assert self.char1.cmp.does_not_exist is None self.assertIs(self.char1.cmp.does_not_exist, None)
def test_host_has_class_component_tags(self): def test_host_has_class_component_tags(self):
assert self.char1.tags.has(key="test_a", category="components") self.assertTrue(self.char1.tags.has(key="test_a", category="components"))
assert self.char1.tags.has(key="test_b", category="components") self.assertTrue(self.char1.tags.has(key="test_b", category="components"))
assert self.char1.tags.has(key="initial_value", category="test_b::default_tag") self.assertTrue(self.char1.tags.has(key="initial_value", category="test_b::default_tag"))
assert self.char1.test_b.default_tag == "initial_value" self.assertTrue(self.char1.test_b.default_tag == "initial_value")
assert not self.char1.tags.has(key="test_c", category="components") self.assertFalse(self.char1.tags.has(key="test_c", category="components"))
assert not self.char1.tags.has(category="test_b::single_tag") self.assertFalse(self.char1.tags.has(category="test_b::single_tag"))
assert not self.char1.tags.has(category="test_b::multiple_tags") self.assertFalse(self.char1.tags.has(category="test_b::multiple_tags"))
def test_host_has_added_component_tags(self): def test_host_has_added_component_tags(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct) self.char1.components.add(rct)
test_c = self.char1.components.get("test_c") test_c = self.char1.components.get("test_c")
assert self.char1.tags.has(key="test_c", category="components") self.assertTrue(self.char1.tags.has(key="test_c", category="components"))
assert self.char1.tags.has(key="added_value", category="test_c::added_tag") self.assertTrue(self.char1.tags.has(key="added_value", category="test_c::added_tag"))
assert test_c.added_tag == "added_value" self.assertEquals(test_c.added_tag, "added_value")
def test_host_has_added_default_component_tags(self): def test_host_has_added_default_component_tags(self):
self.char1.components.add_default("test_c") self.char1.components.add_default("test_c")
test_c = self.char1.components.get("test_c") test_c = self.char1.components.get("test_c")
assert self.char1.tags.has(key="test_c", category="components") self.assertTrue(self.char1.tags.has(key="test_c", category="components"))
assert self.char1.tags.has(key="added_value", category="test_c::added_tag") self.assertTrue(self.char1.tags.has(key="added_value", category="test_c::added_tag"))
assert test_c.added_tag == "added_value" self.assertEquals(test_c.added_tag, "added_value")
def test_host_remove_component_tags(self): def test_host_remove_component_tags(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
handler = self.char1.components handler = self.char1.components
handler.add(rct) handler.add(rct)
assert self.char1.tags.has(key="test_c", category="components") self.assertTrue(self.char1.tags.has(key="test_c", category="components"))
handler.remove(rct) handler.remove(rct)
assert not self.char1.tags.has(key="test_c", category="components") self.assertFalse(self.char1.tags.has(key="test_c", category="components"))
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag") self.assertFalse(self.char1.tags.has(key="added_value", category="test_c::added_tag"))
def test_host_remove_by_name_component_tags(self): def test_host_remove_by_name_component_tags(self):
rct = RuntimeComponentTestC.create(self.char1) rct = RuntimeComponentTestC.create(self.char1)
handler = self.char1.components handler = self.char1.components
handler.add(rct) handler.add(rct)
assert self.char1.tags.has(key="test_c", category="components") self.assertTrue(self.char1.tags.has(key="test_c", category="components"))
handler.remove_by_name("test_c") handler.remove_by_name("test_c")
assert not self.char1.tags.has(key="test_c", category="components") self.assertFalse(self.char1.tags.has(key="test_c", category="components"))
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag") self.assertFalse(self.char1.tags.has(key="added_value", category="test_c::added_tag"))
def test_component_tags_only_hold_one_value_when_enforce_single(self): def test_component_tags_only_hold_one_value_when_enforce_single(self):
test_b = self.char1.components.get("test_b") test_b = self.char1.components.get("test_b")
test_b.single_tag = "first_value" test_b.single_tag = "first_value"
test_b.single_tag = "second value" test_b.single_tag = "second value"
assert self.char1.tags.has(key="second value", category="test_b::single_tag") self.assertTrue(self.char1.tags.has(key="second value", category="test_b::single_tag"))
assert test_b.single_tag == "second value" self.assertEquals(test_b.single_tag, "second value")
assert not self.char1.tags.has(key="first_value", category="test_b::single_tag") self.assertFalse(self.char1.tags.has(key="first_value", category="test_b::single_tag"))
def test_component_tags_default_value_is_overridden_when_enforce_single(self): def test_component_tags_default_value_is_overridden_when_enforce_single(self):
test_b = self.char1.components.get("test_b") test_b = self.char1.components.get("test_b")
test_b.default_single_tag = "second value" test_b.default_single_tag = "second value"
assert self.char1.tags.has(key="second value", category="test_b::default_single_tag") self.assertTrue(self.char1.tags.has(key="second value", category="test_b::default_single_tag"))
assert test_b.default_single_tag == "second value" self.assertTrue(test_b.default_single_tag == "second value")
assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag") self.assertFalse(self.char1.tags.has(key="first_value", category="test_b::default_single_tag"))
def test_component_tags_support_multiple_values_by_default(self): def test_component_tags_support_multiple_values_by_default(self):
test_b = self.char1.components.get("test_b") test_b = self.char1.components.get("test_b")
@ -204,12 +257,20 @@ class TestComponents(EvenniaTest):
test_b.multiple_tags = "second value" test_b.multiple_tags = "second value"
test_b.multiple_tags = "third value" test_b.multiple_tags = "third value"
assert all( self.assertTrue(all(
val in test_b.multiple_tags for val in ("first value", "second value", "third value") val in test_b.multiple_tags for val in ("first value", "second value", "third value")
) ))
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags") self.assertTrue(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") self.assertTrue(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") self.assertTrue(self.char1.tags.has(key="third value", category="test_b::multiple_tags"))
def test_mutables_are_not_shared_when_autocreate(self):
self.char1.test_a.my_list.append(1)
self.assertNotEquals(self.char1.test_a.my_list, self.char2.test_a.my_list)
def test_replacing_class_component_slot_with_runtime_component(self):
self.char1.components.add_default("replacement_inherited_test_a")
self.assertEquals(self.char1.ic_a.replacement_field, 6)
class CharWithSignal(ComponentHolderMixin, DefaultCharacter): class CharWithSignal(ComponentHolderMixin, DefaultCharacter):
@ -265,14 +326,14 @@ class TestComponentSignals(BaseEvenniaTest):
def test_host_can_register_as_listener(self): def test_host_can_register_as_listener(self):
self.char1.signals.trigger("my_signal") self.char1.signals.trigger("my_signal")
assert self.char1.my_signal_is_called self.assertTrue(self.char1.my_signal_is_called)
assert not getattr(self.char1, "my_other_signal_is_called", None) self.assertFalse(getattr(self.char1, "my_other_signal_is_called", None))
def test_host_can_register_as_responder(self): def test_host_can_register_as_responder(self):
responses = self.char1.signals.query("my_response") responses = self.char1.signals.query("my_response")
assert 1 in responses self.assertIn(1, responses)
assert 2 not in responses self.assertNotIn(2, responses)
def test_component_can_register_as_listener(self): def test_component_can_register_as_listener(self):
char = self.char1 char = self.char1
@ -280,16 +341,16 @@ class TestComponentSignals(BaseEvenniaTest):
char.signals.trigger("my_signal") char.signals.trigger("my_signal")
component = char.cmp.test_signal_a component = char.cmp.test_signal_a
assert component.my_signal_is_called self.assertTrue(component.my_signal_is_called)
assert not getattr(component, "my_other_signal_is_called", None) self.assertFalse(getattr(component, "my_other_signal_is_called", None))
def test_component_can_register_as_responder(self): def test_component_can_register_as_responder(self):
char = self.char1 char = self.char1
char.components.add(ComponentWithSignal.create(char)) char.components.add(ComponentWithSignal.create(char))
responses = char.signals.query("my_response") responses = char.signals.query("my_response")
assert 1 in responses self.assertIn(1, responses)
assert 2 not in responses self.assertNotIn(2, responses)
def test_signals_can_add_listener(self): def test_signals_can_add_listener(self):
result = [] result = []
@ -300,7 +361,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.add_listener("my_fake_signal", my_fake_listener) self.char1.signals.add_listener("my_fake_signal", my_fake_listener)
self.char1.signals.trigger("my_fake_signal") self.char1.signals.trigger("my_fake_signal")
assert result self.assertTrue(result)
def test_signals_can_add_responder(self): def test_signals_can_add_responder(self):
def my_fake_responder(): def my_fake_responder():
@ -309,7 +370,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.add_responder("my_fake_response", my_fake_responder) self.char1.signals.add_responder("my_fake_response", my_fake_responder)
responses = self.char1.signals.query("my_fake_response") responses = self.char1.signals.query("my_fake_response")
assert 1 in responses self.assertIn(1, responses)
def test_signals_can_remove_listener(self): def test_signals_can_remove_listener(self):
result = [] result = []
@ -321,7 +382,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.remove_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") self.char1.signals.trigger("my_fake_signal")
assert not result self.assertFalse(result)
def test_signals_can_remove_responder(self): def test_signals_can_remove_responder(self):
def my_fake_responder(): def my_fake_responder():
@ -331,7 +392,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.remove_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") responses = self.char1.signals.query("my_fake_response")
assert not responses self.assertFalse(responses)
def test_signals_can_trigger_with_args(self): def test_signals_can_trigger_with_args(self):
result = [] result = []
@ -342,7 +403,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.add_listener("my_fake_signal", my_fake_listener) self.char1.signals.add_listener("my_fake_signal", my_fake_listener)
self.char1.signals.trigger("my_fake_signal", 1, kwarg1=2) self.char1.signals.trigger("my_fake_signal", 1, kwarg1=2)
assert (1, 2) in result self.assertIn((1, 2), result)
def test_signals_can_query_with_args(self): def test_signals_can_query_with_args(self):
def my_fake_responder(arg1, kwarg1): def my_fake_responder(arg1, kwarg1):
@ -351,7 +412,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.add_responder("my_fake_response", my_fake_responder) self.char1.signals.add_responder("my_fake_response", my_fake_responder)
responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2) responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2)
assert (1, 2) in responses self.assertIn((1, 2), responses)
def test_signals_trigger_does_not_fail_without_listener(self): def test_signals_trigger_does_not_fail_without_listener(self):
self.char1.signals.trigger("some_unknown_signal") self.char1.signals.trigger("some_unknown_signal")
@ -366,7 +427,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.add_responder("my_fake_response", my_fake_responder) self.char1.signals.add_responder("my_fake_response", my_fake_responder)
responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2) responses = self.char1.signals.query("my_fake_response", 1, kwarg1=2)
assert (1, 2) in responses self.assertIn((1, 2), responses)
def test_signals_can_add_object_listeners_and_responders(self): def test_signals_can_add_object_listeners_and_responders(self):
result = [] result = []
@ -379,7 +440,7 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.add_object_listeners_and_responders(FakeObj()) self.char1.signals.add_object_listeners_and_responders(FakeObj())
self.char1.signals.trigger("my_signal") self.char1.signals.trigger("my_signal")
assert result self.assertTrue(result)
def test_signals_can_remove_object_listeners_and_responders(self): def test_signals_can_remove_object_listeners_and_responders(self):
result = [] result = []
@ -394,14 +455,14 @@ class TestComponentSignals(BaseEvenniaTest):
self.char1.signals.remove_object_listeners_and_responders(obj) self.char1.signals.remove_object_listeners_and_responders(obj)
self.char1.signals.trigger("my_signal") self.char1.signals.trigger("my_signal")
assert not result self.assertFalse(result)
def test_component_handler_signals_connected_when_adding_default_component(self): def test_component_handler_signals_connected_when_adding_default_component(self):
char = self.char1 char = self.char1
char.components.add_default("test_signal_a") char.components.add_default("test_signal_a")
responses = char.signals.query("my_component_response") responses = char.signals.query("my_component_response")
assert 3 in responses self.assertIn(3, responses)
def test_component_handler_signals_disconnected_when_removing_component(self): def test_component_handler_signals_disconnected_when_removing_component(self):
char = self.char1 char = self.char1
@ -410,7 +471,7 @@ class TestComponentSignals(BaseEvenniaTest):
char.components.remove(comp) char.components.remove(comp)
responses = char.signals.query("my_component_response") responses = char.signals.query("my_component_response")
assert not responses self.assertFalse(responses)
def test_component_handler_signals_disconnected_when_removing_component_by_name(self): def test_component_handler_signals_disconnected_when_removing_component_by_name(self):
char = self.char1 char = self.char1
@ -418,4 +479,4 @@ class TestComponentSignals(BaseEvenniaTest):
char.components.remove_by_name("test_signal_a") char.components.remove_by_name("test_signal_a")
responses = char.signals.query("my_component_response") responses = char.signals.query("my_component_response")
assert not responses self.assertFalse(responses)