From 217bd711e773e09578569834ac1eec1f6a5b2006 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Thu, 14 Dec 2023 12:30:53 -0500 Subject: [PATCH 01/18] Refactor Component registering with cherry-picked additions --- .../base_systems/components/__init__.py | 23 +-- .../base_systems/components/component.py | 81 ++++---- .../base_systems/components/dbfield.py | 61 +++--- .../base_systems/components/exceptions.py | 10 + .../contrib/base_systems/components/holder.py | 178 ++++++++---------- .../base_systems/components/listing.py | 15 ++ 6 files changed, 194 insertions(+), 174 deletions(-) create mode 100644 evennia/contrib/base_systems/components/exceptions.py create mode 100644 evennia/contrib/base_systems/components/listing.py diff --git a/evennia/contrib/base_systems/components/__init__.py b/evennia/contrib/base_systems/components/__init__.py index 3e31b214da..eb927ad6d5 100644 --- a/evennia/contrib/base_systems/components/__init__.py +++ b/evennia/contrib/base_systems/components/__init__.py @@ -7,23 +7,16 @@ This helps writing isolated code and reusing it over multiple objects. 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.dbfield import DBField, NDBField, TagField +from evennia.contrib.base_systems.components.dbfield import ( + DBField, + NDBField, + TagField +) + from evennia.contrib.base_systems.components.holder import ( ComponentHolderMixin, 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 diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index 4be5b86d15..dd2e7132fa 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -3,10 +3,32 @@ Components - ChrisLR 2022 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): + @classmethod + def __new__(cls, *args): + 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. Any component must inherit from this class to be considered for usage. @@ -14,10 +36,17 @@ class Component: Each Component must supply the name, it is used as a slot name but also part of the attribute key. """ + __slots__ = ('host',) + name = "" + slot = None + + cmd_set: CmdSet = None + + _fields = {} 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 @classmethod @@ -61,8 +90,8 @@ class Component: """ This deletes all component attributes from the host's db """ - for attribute in self._all_db_field_names: - delattr(self, attribute) + for name in self._fields.keys(): + delattr(self, name) @classmethod def load(cls, host): @@ -88,12 +117,11 @@ class Component: host (object): The host typeclass instance """ + if self.host and self.host != host: + raise exceptions.InvalidComponentError("Components must not register twice!") - if self.host: - if self.host == host: - return - else: - raise ComponentRegisterError("Components must not register twice!") + if self.cmd_set: + self.host.cmdset.add(self.cmd_set) self.host = host @@ -106,7 +134,11 @@ class Component: """ if host != self.host: - raise ComponentRegisterError("Component attempted to remove from the wrong host.") + raise ValueError("Component attempted to remove from the wrong host.") + + if self.cmd_set: + self.host.cmdset.remove(self.cmd_set) + self.host = None @property @@ -131,25 +163,10 @@ class Component: """ return self.host.nattributes - @property - def _all_db_field_names(self): - return itertools.chain(self.db_field_names, self.ndb_field_names) + @classmethod + def add_field(cls, name, field): + cls._fields[name] = field - @property - def db_field_names(self): - db_fields = getattr(self, "_db_fields", {}) - return db_fields.keys() - - @property - def ndb_field_names(self): - ndb_fields = getattr(self, "_ndb_fields", {}) - return ndb_fields.keys() - - @property - def tag_field_names(self): - tag_fields = getattr(self, "_tag_fields", {}) - return tag_fields.keys() - - -class ComponentRegisterError(Exception): - pass + @classmethod + def get_fields(cls): + return tuple(cls._fields.values()) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index fc3a104ffb..2129212456 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -3,8 +3,13 @@ Components - ChrisLR 2022 This file contains the Descriptors used to set Fields in Components """ +import typing + from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty +if typing.TYPE_CHECKING: + from evennia.contrib.base_systems.components import Component + class DBField(AttributeProperty): """ @@ -13,7 +18,10 @@ class DBField(AttributeProperty): 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. @@ -21,13 +29,15 @@ class DBField(AttributeProperty): owner (object): The component classF on which this is set name (str): The name that was used to set the DBField. """ - key = f"{owner.name}::{name}" - self._key = key - db_fields = getattr(owner, "_db_fields", None) - if db_fields is None: - db_fields = {} - setattr(owner, "_db_fields", db_fields) - db_fields[name] = self + self._key = f"{owner.slot or owner.name}::{name}" + owner.add_field(name, self) + + def at_added(self, instance): + if self._autocreate: + self.__set__(instance, self._default) + + def at_removed(self, instance): + self.__delete__(instance) class NDBField(NAttributeProperty): @@ -37,7 +47,7 @@ class NDBField(NAttributeProperty): 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. @@ -45,13 +55,15 @@ class NDBField(NAttributeProperty): owner (object): The component class on which this is set name (str): The name that was used to set the DBField. """ - key = f"{owner.name}::{name}" - self._key = key - ndb_fields = getattr(owner, "_ndb_fields", None) - if ndb_fields is None: - ndb_fields = {} - setattr(owner, "_ndb_fields", ndb_fields) - ndb_fields[name] = self + self._key = f"{owner.slot or owner.name}::{name}" + owner.add_field(name, self) + + def at_added(self, instance): + if self._autocreate: + self.__set__(instance, self._default) + + def at_removed(self, instance): + self.__delete__(instance) class TagField: @@ -70,17 +82,13 @@ class TagField: self._default = default 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. It is called with the component class and the name of the field. """ - self._category_key = f"{owner.name}::{name}" - tag_fields = getattr(owner, "_tag_fields", None) - if tag_fields is None: - tag_fields = {} - setattr(owner, "_tag_fields", tag_fields) - tag_fields[name] = self + self._category_key = f"{owner.slot or owner.name}::{name}" + owner.add_field(name, self) def __get__(self, instance, owner): """ @@ -114,3 +122,10 @@ class TagField: It is called with the component instance. """ instance.host.tags.clear(category=self._category_key) + + def at_added(self, instance): + if self._default: + self.__set__(instance, self._default) + + def at_removed(self, instance): + self.__delete__(instance) diff --git a/evennia/contrib/base_systems/components/exceptions.py b/evennia/contrib/base_systems/components/exceptions.py new file mode 100644 index 0000000000..7a2254c631 --- /dev/null +++ b/evennia/contrib/base_systems/components/exceptions.py @@ -0,0 +1,10 @@ +class InvalidComponentError(ValueError): + pass + + +class ComponentDoesNotExist(ValueError): + pass + + +class ComponentIsNotRegistered(ValueError): + pass diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 7d19d344b0..f4f4f7f037 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -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.components import signals +from evennia.contrib.base_systems.components import signals, exceptions class ComponentProperty: @@ -17,19 +17,19 @@ class ComponentProperty: Defaults can be overridden for this typeclass by passing kwargs """ - def __init__(self, component_name, **kwargs): + def __init__(self, name, **kwargs): """ Initializes the descriptor 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 """ - self.component_name = component_name + self.name = name self.values = kwargs def __get__(self, instance, owner): - component = instance.components.get(self.component_name) + component = instance.components.get(self.name) return component def __set__(self, instance, value): @@ -37,13 +37,11 @@ class ComponentProperty: def __set_name__(self, owner, name): # 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: - # Create a new list, including inherited class components - class_components = list(getattr(owner, "_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: @@ -57,7 +55,7 @@ class ComponentHandler: self.host = host self._loaded_components = {} - def add(self, component): + def add(self, component: components.Component): """ Method to add a Component to a host. It caches the loaded component and appends its name to the host's component name list. @@ -67,16 +65,19 @@ class ComponentHandler: 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.db_names.append(component.name) - self._add_component_tags(component) + for field in component.get_fields(): + field.at_added(self.host) + component.at_added(self.host) - self.host.signals.add_object_listeners_and_responders(component) def add_default(self, name): """ 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 also call the component's 'at_added' method, passing its host. @@ -84,33 +85,11 @@ class ComponentHandler: name (str): The name of the component class to add. """ - component = components.get_component_class(name) - if not component: - raise ComponentDoesNotExist(f"Component {name} does not exist.") + component_class = components.get_component_class(name) + component_instance = component_class.default_create(self.host) + self.add(component_instance) - new_component = component.default_create(self.host) - 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): + def remove(self, component: components.Component): """ Method to remove a component instance from a host. It removes the component from the cache and listing. @@ -120,18 +99,26 @@ class ComponentHandler: component (object): The component instance to remove. """ - component_name = component.name - if component_name in self._loaded_components: - 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: + name = component.name + slot_name = component.slot or name + if not self.has(slot_name): 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(self.host) + + component.at_removed(self.host) + if component.cmd_set: + self.host.cmdset.remove(component.cmd_set) + + 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): """ @@ -140,49 +127,25 @@ class ComponentHandler: It will call the component's 'at_removed' method. 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) if not instance: 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) - instance.at_removed(self.host) - self.host.signals.remove_object_listeners_and_responders(instance) - self.db_names.remove(name) + self.remove(instance) - del self._loaded_components[name] - - 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. - - """ + def get(self, name: str) -> components.Component | None: 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. Args: - name (str): The name of the component. + name (str): The name of the component or the slot. """ return name in self._loaded_components @@ -203,26 +166,35 @@ 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) + raise exceptions.ComponentDoesNotExist(message) def _set_component(self, component): - self._loaded_components[component.name] = component + """ + Sets the loaded component in this instance. + """ + slot_name = component.slot or component.name + self._loaded_components[slot_name] = component + self.host.signals.add_object_listeners_and_responders(component) @property def db_names(self): """ - Property shortcut to retrieve the registered component names + Property shortcut to retrieve the registered component keys Returns: 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): return self.get(name) @@ -236,7 +208,6 @@ class ComponentHolderMixin: All registered components are initialized on the typeclass. They will be of None value if not present in the class components or runtime components. """ - def at_init(self): """ Method that initializes the ComponentHandler. @@ -261,28 +232,16 @@ class ComponentHolderMixin: components that were set on the typeclass using ComponentProperty. """ super().basetype_setup() - component_names = [] setattr(self, "_component_handler", ComponentHandler(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: 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.components.add(component) - self.db.component_names = component_names 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 def components(self) -> ComponentHandler: """ @@ -305,10 +264,21 @@ class ComponentHolderMixin: def signals(self) -> signals.SignalsHandler: return getattr(self, "_signal_handler", None) + def _get_class_components(self): + class_components = {} -class ComponentDoesNotExist(Exception): - pass + def base_type_iterator(): + 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", ()) + class_components.update({cmp[0]: cmp[1] for cmp in base_class_components}) -class ComponentIsNotRegistered(Exception): - pass + instance_components = getattr(self, "_class_components", ()) + class_components.update({cmp[0]: cmp[1] for cmp in instance_components}) + + return tuple(class_components.items()) diff --git a/evennia/contrib/base_systems/components/listing.py b/evennia/contrib/base_systems/components/listing.py new file mode 100644 index 0000000000..4ca51842d5 --- /dev/null +++ b/evennia/contrib/base_systems/components/listing.py @@ -0,0 +1,15 @@ +from evennia.contrib.base_systems.components import exceptions + +COMPONENT_LISTING = {} + + +def get_component_class(name): + 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 From 74f8715d5aff7f6c42185bd47f3dc9155b29bb23 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 2 Jan 2024 10:25:17 -0500 Subject: [PATCH 02/18] Add cmd_set on load --- evennia/contrib/base_systems/components/component.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index dd2e7132fa..c042b25166 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -106,8 +106,11 @@ class Component(metaclass=BaseComponent): Component: The loaded instance of the component """ + inst = cls(host) + if inst.cmd_set: + host.cmdset.add(inst.cmd_set) - return cls(host) + return inst def at_added(self, host): """ From 2ff534c56af2938a6b072535c9e0c127895d5d2a Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 2 Jan 2024 10:26:04 -0500 Subject: [PATCH 03/18] Fix ComponentProperty --- evennia/contrib/base_systems/components/holder.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index f4f4f7f037..e4dbc3ef71 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -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.components import signals, exceptions +from evennia.contrib.base_systems.components import signals, exceptions, get_component_class class ComponentProperty: @@ -27,9 +27,16 @@ class ComponentProperty: """ self.name = name self.values = kwargs + self.component_class = None + self.slot_name = None def __get__(self, instance, owner): - component = instance.components.get(self.name) + if not self.component_class: + component_class = get_component_class(self.name) + self.component_class = component_class + self.slot_name = component_class.slot or component_class.name + + component = instance.components.get(self.slot_name) return component def __set__(self, instance, value): From 476df0ea3e7def3eb3145685a01c9ff4a6d37376 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 2 Jan 2024 10:41:52 -0500 Subject: [PATCH 04/18] Fix Field at_added call --- evennia/contrib/base_systems/components/dbfield.py | 8 ++++---- evennia/contrib/base_systems/components/holder.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 2129212456..1fda22a35a 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -123,9 +123,9 @@ class TagField: """ instance.host.tags.clear(category=self._category_key) - def at_added(self, instance): + def at_added(self, component): if self._default: - self.__set__(instance, self._default) + self.__set__(component, self._default) - def at_removed(self, instance): - self.__delete__(instance) + def at_removed(self, component): + self.__delete__(component) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index e4dbc3ef71..9053bd3e69 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -77,7 +77,7 @@ class ComponentHandler: self.host.tags.add(component_name, category="components") self._set_component(component) for field in component.get_fields(): - field.at_added(self.host) + field.at_added(component) component.at_added(self.host) @@ -115,7 +115,7 @@ class ComponentHandler: raise exceptions.ComponentIsNotRegistered(message) for field in component.get_fields(): - field.at_removed(self.host) + field.at_removed(component) component.at_removed(self.host) if component.cmd_set: From 4071ae7b86cafdd1366f1339560c87f0e7c7d086 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 2 Jan 2024 11:21:04 -0500 Subject: [PATCH 05/18] Use __get__ instead when autocreating to avoid overriding initial values --- evennia/contrib/base_systems/components/dbfield.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 1fda22a35a..6eaf0eceef 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -32,12 +32,12 @@ class DBField(AttributeProperty): self._key = f"{owner.slot or owner.name}::{name}" owner.add_field(name, self) - def at_added(self, instance): + def at_added(self, component): if self._autocreate: - self.__set__(instance, self._default) + self.__get__(component, type(component)) - def at_removed(self, instance): - self.__delete__(instance) + def at_removed(self, component): + self.__delete__(component) class NDBField(NAttributeProperty): From 5f60075fdffc1e3b1c3f6a5dfdcd045c37471f1b Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 2 Jan 2024 11:21:12 -0500 Subject: [PATCH 06/18] Add new tests --- .../contrib/base_systems/components/tests.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index 0bb662f218..c4ff6a95c9 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -17,13 +17,20 @@ from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest class ComponentTestA(Component): name = "test_a" my_int = DBField(default=1) - my_list = DBField(default=[]) + my_list = DBField(default=[], autocreate=True) + + +class InheritedComponentTestA(ComponentTestA): + name = "inherited_test_a" + slot = 'ic_a' + + my_other_int = DBField(default=2) class ComponentTestB(Component): name = "test_b" my_int = DBField(default=1) - my_list = DBField(default=[]) + my_list = DBField(default=[], autocreate=True) default_tag = TagField(default="initial_value") single_tag = TagField(enforce_single=True) multiple_tags = TagField() @@ -33,13 +40,24 @@ class ComponentTestB(Component): class RuntimeComponentTestC(Component): name = "test_c" my_int = DBField(default=6) - my_dict = DBField(default={}) + my_dict = DBField(default={}, autocreate=True) added_tag = TagField(default="added_value") -class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter): +class ComponentTestD(Component): + name = "test_d" + + mixed_in = DBField(default=8) + + +class CharacterMixinWithComponents: + test_d = ComponentProperty('test_d') + + +class CharacterWithComponents(ComponentHolderMixin, CharacterMixinWithComponents, DefaultCharacter): test_a = ComponentProperty("test_a") 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): @@ -73,6 +91,13 @@ class TestComponents(EvenniaTest): assert self.char1.test_b.my_int == 3 assert self.char1.test_b.my_list == [1, 2, 3] + 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): rct = RuntimeComponentTestC.create(self.char1) self.char1.components.add(rct) @@ -211,6 +236,10 @@ class TestComponents(EvenniaTest): 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") + 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) + class CharWithSignal(ComponentHolderMixin, DefaultCharacter): @signals.as_listener From 37e70cc7fa713bf87d37e8d4966b91bdd1dac49b Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Tue, 2 Jan 2024 11:35:11 -0500 Subject: [PATCH 07/18] Rewrote test assertions to django style --- .../contrib/base_systems/components/tests.py | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index c4ff6a95c9..79ccf1bbbd 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -68,28 +68,28 @@ class TestComponents(EvenniaTest): character_typeclass = CharacterWithComponents def test_character_has_class_components(self): - assert self.char1.test_a - assert self.char1.test_b + self.assertTrue(self.char1.test_a) + self.assertTrue(self.char1.test_b) def test_inherited_typeclass_does_not_include_child_class_components(self): char_with_c = create.create_object( InheritedTCWithComponents, key="char_with_c", location=self.room1, home=self.room1 ) - assert self.char1.test_a - assert not self.char1.cmp.get("test_c") - assert char_with_c.test_c + self.assertTrue(self.char1.test_a) + self.assertFalse(self.char1.cmp.get("test_c")) + self.assertTrue(char_with_c.test_c) def test_character_instances_components_properly(self): - assert isinstance(self.char1.test_a, ComponentTestA) - assert isinstance(self.char1.test_b, ComponentTestB) + self.assertIsInstance(self.char1.test_a, ComponentTestA) + self.assertIsInstance(self.char1.test_b, ComponentTestB) def test_character_assigns_default_value(self): - assert self.char1.test_a.my_int == 1 - assert self.char1.test_a.my_list == [] + self.assertEquals(self.char1.test_a.my_int, 1) + self.assertEquals(self.char1.test_a.my_list, []) def test_character_assigns_default_provided_values(self): - assert self.char1.test_b.my_int == 3 - assert self.char1.test_b.my_list == [1, 2, 3] + self.assertEquals(self.char1.test_b.my_int, 3) + self.assertEquals(self.char1.test_b.my_list, [1, 2, 3]) def test_component_inheritance_assigns_proper_values(self): self.assertEquals(self.char1.ic_a.my_int, 1) @@ -103,25 +103,25 @@ class TestComponents(EvenniaTest): self.char1.components.add(rct) test_c = self.char1.components.get("test_c") - assert test_c - assert test_c.my_int == 6 - assert test_c.my_dict == {} + self.assertTrue(test_c) + self.assertEquals(test_c.my_int, 6) + self.assertEquals(test_c.my_dict, {}) def test_handler_can_add_default_component(self): self.char1.components.add_default("test_c") test_c = self.char1.components.get("test_c") - assert test_c - assert test_c.my_int == 6 + self.assertTrue(test_c) + self.assertEquals(test_c.my_int, 6) def test_handler_has_returns_true_for_any_components(self): rct = RuntimeComponentTestC.create(self.char1) handler = self.char1.components handler.add(rct) - assert handler.has("test_a") - assert handler.has("test_b") - assert handler.has("test_c") + self.assertTrue(handler.has("test_a")) + self.assertTrue(handler.has("test_b")) + self.assertTrue(handler.has("test_c")) def test_can_remove_component(self): rct = RuntimeComponentTestC.create(self.char1) @@ -129,9 +129,9 @@ class TestComponents(EvenniaTest): handler.add(rct) handler.remove(rct) - assert handler.has("test_a") - assert handler.has("test_b") - assert not handler.has("test_c") + self.assertTrue(handler.has("test_a")) + self.assertTrue(handler.has("test_b")) + self.assertFalse(handler.has("test_c")) def test_can_remove_component_by_name(self): rct = RuntimeComponentTestC.create(self.char1) @@ -139,9 +139,9 @@ class TestComponents(EvenniaTest): handler.add(rct) handler.remove_by_name("test_c") - assert handler.has("test_a") - assert handler.has("test_b") - assert not handler.has("test_c") + self.assertTrue(handler.has("test_a")) + self.assertTrue(handler.has("test_b")) + self.assertFalse(handler.has("test_c")) def test_cannot_replace_component(self): with self.assertRaises(Exception): @@ -152,76 +152,76 @@ class TestComponents(EvenniaTest): handler = self.char1.components 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): - 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): - 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): - assert self.char1.tags.has(key="test_a", category="components") - assert self.char1.tags.has(key="test_b", category="components") - assert self.char1.tags.has(key="initial_value", category="test_b::default_tag") - assert self.char1.test_b.default_tag == "initial_value" - assert not self.char1.tags.has(key="test_c", category="components") - assert not self.char1.tags.has(category="test_b::single_tag") - assert not self.char1.tags.has(category="test_b::multiple_tags") + self.assertTrue(self.char1.tags.has(key="test_a", category="components")) + self.assertTrue(self.char1.tags.has(key="test_b", category="components")) + self.assertTrue(self.char1.tags.has(key="initial_value", category="test_b::default_tag")) + self.assertTrue(self.char1.test_b.default_tag == "initial_value") + self.assertFalse(self.char1.tags.has(key="test_c", category="components")) + self.assertFalse(self.char1.tags.has(category="test_b::single_tag")) + self.assertFalse(self.char1.tags.has(category="test_b::multiple_tags")) def test_host_has_added_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) self.char1.components.add(rct) test_c = self.char1.components.get("test_c") - assert self.char1.tags.has(key="test_c", category="components") - assert self.char1.tags.has(key="added_value", category="test_c::added_tag") - assert test_c.added_tag == "added_value" + self.assertTrue(self.char1.tags.has(key="test_c", category="components")) + self.assertTrue(self.char1.tags.has(key="added_value", category="test_c::added_tag")) + self.assertEquals(test_c.added_tag, "added_value") def test_host_has_added_default_component_tags(self): self.char1.components.add_default("test_c") test_c = self.char1.components.get("test_c") - assert self.char1.tags.has(key="test_c", category="components") - assert self.char1.tags.has(key="added_value", category="test_c::added_tag") - assert test_c.added_tag == "added_value" + self.assertTrue(self.char1.tags.has(key="test_c", category="components")) + self.assertTrue(self.char1.tags.has(key="added_value", category="test_c::added_tag")) + self.assertEquals(test_c.added_tag, "added_value") def test_host_remove_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) handler = self.char1.components 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) - assert not 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="test_c", category="components")) + self.assertFalse(self.char1.tags.has(key="added_value", category="test_c::added_tag")) def test_host_remove_by_name_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) handler = self.char1.components 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") - assert not 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="test_c", category="components")) + 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): test_b = self.char1.components.get("test_b") test_b.single_tag = "first_value" test_b.single_tag = "second value" - assert self.char1.tags.has(key="second value", category="test_b::single_tag") - assert test_b.single_tag == "second value" - assert not self.char1.tags.has(key="first_value", category="test_b::single_tag") + self.assertTrue(self.char1.tags.has(key="second value", category="test_b::single_tag")) + self.assertEquals(test_b.single_tag, "second value") + 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): test_b = self.char1.components.get("test_b") test_b.default_single_tag = "second value" - assert self.char1.tags.has(key="second value", category="test_b::default_single_tag") - assert test_b.default_single_tag == "second value" - assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag") + self.assertTrue(self.char1.tags.has(key="second value", category="test_b::default_single_tag")) + self.assertTrue(test_b.default_single_tag == "second value") + 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): test_b = self.char1.components.get("test_b") @@ -229,12 +229,12 @@ class TestComponents(EvenniaTest): test_b.multiple_tags = "second 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") - ) - 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") + )) + self.assertTrue(self.char1.tags.has(key="first value", category="test_b::multiple_tags")) + self.assertTrue(self.char1.tags.has(key="second 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) @@ -294,14 +294,14 @@ class TestComponentSignals(BaseEvenniaTest): 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) + self.assertTrue(self.char1.my_signal_is_called) + self.assertFalse(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 + self.assertIn(1, responses) + self.assertNotIn(2, responses) def test_component_can_register_as_listener(self): char = self.char1 @@ -309,16 +309,16 @@ class TestComponentSignals(BaseEvenniaTest): 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) + self.assertTrue(component.my_signal_is_called) + self.assertFalse(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 + self.assertIn(1, responses) + self.assertNotIn(2, responses) def test_signals_can_add_listener(self): result = [] @@ -329,7 +329,7 @@ class TestComponentSignals(BaseEvenniaTest): self.char1.signals.add_listener("my_fake_signal", my_fake_listener) self.char1.signals.trigger("my_fake_signal") - assert result + self.assertTrue(result) def test_signals_can_add_responder(self): def my_fake_responder(): @@ -338,7 +338,7 @@ class TestComponentSignals(BaseEvenniaTest): self.char1.signals.add_responder("my_fake_response", my_fake_responder) responses = self.char1.signals.query("my_fake_response") - assert 1 in responses + self.assertIn(1, responses) def test_signals_can_remove_listener(self): result = [] @@ -350,7 +350,7 @@ class TestComponentSignals(BaseEvenniaTest): self.char1.signals.remove_listener("my_fake_signal", my_fake_listener) self.char1.signals.trigger("my_fake_signal") - assert not result + self.assertFalse(result) def test_signals_can_remove_responder(self): def my_fake_responder(): @@ -360,7 +360,7 @@ class TestComponentSignals(BaseEvenniaTest): self.char1.signals.remove_responder("my_fake_response", my_fake_responder) responses = self.char1.signals.query("my_fake_response") - assert not responses + self.assertFalse(responses) def test_signals_can_trigger_with_args(self): result = [] @@ -371,7 +371,7 @@ class TestComponentSignals(BaseEvenniaTest): 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 + self.assertIn((1, 2), result) def test_signals_can_query_with_args(self): def my_fake_responder(arg1, kwarg1): @@ -380,7 +380,7 @@ class TestComponentSignals(BaseEvenniaTest): 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 + self.assertIn((1, 2), responses) def test_signals_trigger_does_not_fail_without_listener(self): self.char1.signals.trigger("some_unknown_signal") @@ -395,7 +395,7 @@ class TestComponentSignals(BaseEvenniaTest): 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 + self.assertIn((1, 2), responses) def test_signals_can_add_object_listeners_and_responders(self): result = [] @@ -408,7 +408,7 @@ class TestComponentSignals(BaseEvenniaTest): self.char1.signals.add_object_listeners_and_responders(FakeObj()) self.char1.signals.trigger("my_signal") - assert result + self.assertTrue(result) def test_signals_can_remove_object_listeners_and_responders(self): result = [] @@ -423,14 +423,14 @@ class TestComponentSignals(BaseEvenniaTest): self.char1.signals.remove_object_listeners_and_responders(obj) self.char1.signals.trigger("my_signal") - assert not result + self.assertFalse(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 + self.assertIn(3, responses) def test_component_handler_signals_disconnected_when_removing_component(self): char = self.char1 @@ -439,7 +439,7 @@ class TestComponentSignals(BaseEvenniaTest): char.components.remove(comp) 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): char = self.char1 @@ -447,4 +447,4 @@ class TestComponentSignals(BaseEvenniaTest): char.components.remove_by_name("test_signal_a") responses = char.signals.query("my_component_response") - assert not responses + self.assertFalse(responses) From 6ad6522fa466f78cecc0209e5c988bfbb5aa773d Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sun, 14 Jan 2024 12:58:26 -0500 Subject: [PATCH 08/18] Add docstrings --- .../base_systems/components/component.py | 9 ++++ .../base_systems/components/dbfield.py | 52 ++++++++++++++++--- .../base_systems/components/listing.py | 5 ++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index c042b25166..d5055c233d 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -9,8 +9,17 @@ from evennia.contrib.base_systems.components import COMPONENT_LISTING, exception 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 diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 6eaf0eceef..709548acf3 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -26,17 +26,31 @@ class DBField(AttributeProperty): Called when descriptor is first assigned to the class. 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. """ self._key = f"{owner.slot or owner.name}::{name}" owner.add_field(name, self) 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._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) @@ -52,18 +66,30 @@ class NDBField(NAttributeProperty): Called when descriptor is first assigned to the class. 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. """ self._key = f"{owner.slot or owner.name}::{name}" owner.add_field(name, self) - def at_added(self, instance): - if self._autocreate: - self.__set__(instance, self._default) + def at_added(self, component): + """ + Called when the parent component is added to a host. - def at_removed(self, instance): - self.__delete__(instance) + 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: @@ -124,8 +150,20 @@ class TagField: 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) diff --git a/evennia/contrib/base_systems/components/listing.py b/evennia/contrib/base_systems/components/listing.py index 4ca51842d5..11647e9425 100644 --- a/evennia/contrib/base_systems/components/listing.py +++ b/evennia/contrib/base_systems/components/listing.py @@ -4,6 +4,11 @@ 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 = ( From 2875674bafeac6cd1e17a532fd8a92804a65e9b7 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sun, 14 Jan 2024 13:33:23 -0500 Subject: [PATCH 09/18] Update docs --- .../contrib/base_systems/components/README.md | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/base_systems/components/README.md b/evennia/contrib/base_systems/components/README.md index 0a7d0d01e5..aa3391687c 100644 --- a/evennia/contrib/base_systems/components/README.md +++ b/evennia/contrib/base_systems/components/README.md @@ -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 from evennia.contrib.base_systems.components import Component + class Health(Component): name = "health" + + +class ItemHealth(Health): + name = "item_health" + slot = "health" ``` 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. @@ -128,6 +139,14 @@ from typeclasses.components import health ``` 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 ```python from evennia.contrib.base_systems import components From 191be0365ccb464860b3c9bbb392fdebfc9915ea Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Fri, 23 Feb 2024 13:51:16 -0500 Subject: [PATCH 10/18] Fix Signals registration --- .../base_systems/components/signals.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/evennia/contrib/base_systems/components/signals.py b/evennia/contrib/base_systems/components/signals.py index eff5137c64..b5fce84e36 100644 --- a/evennia/contrib/base_systems/components/signals.py +++ b/evennia/contrib/base_systems/components/signals.py @@ -179,8 +179,15 @@ class SignalsHandler(object): Args: obj (object): The instance of an object to connect to this handler. """ - type_host = type(obj) - for att_name, att_obj in type_host.__dict__.items(): + obj_type = type(obj) + 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) if listener_signal_name: callback = getattr(obj, att_name) @@ -198,8 +205,14 @@ class SignalsHandler(object): Args: obj (object): The instance of an object to disconnect from this handler. """ - type_host = type(obj) - for att_name, att_obj in type_host.__dict__.items(): + for att_name in dir(obj): + 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) if listener_signal_name: callback = getattr(obj, att_name) From 47be47c00917b39167ceeab37873ba2fd92aacdc Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Fri, 23 Feb 2024 13:51:38 -0500 Subject: [PATCH 11/18] Add get_component_slot helper method --- evennia/contrib/base_systems/components/component.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index c042b25166..68bd50e492 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -173,3 +173,7 @@ class Component(metaclass=BaseComponent): @classmethod def get_fields(cls): return tuple(cls._fields.values()) + + @classmethod + def get_component_slot(cls): + return cls.slot or cls.name From 8ff8234f461d44b846b7fb04cadbd259ad221974 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Fri, 23 Feb 2024 13:52:35 -0500 Subject: [PATCH 12/18] Fix inherited component registration with different names but identical slots --- evennia/contrib/base_systems/components/holder.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 9053bd3e69..65fb51d22d 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -283,9 +283,16 @@ class ComponentHolderMixin: for base_type in base_type_iterator(): base_class_components = getattr(base_type, "_class_components", ()) - class_components.update({cmp[0]: cmp[1] for cmp in base_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) + # TODO Is this necessary? instance_components = getattr(self, "_class_components", ()) - class_components.update({cmp[0]: cmp[1] for cmp in instance_components}) + 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.items()) + return tuple(class_components.values()) From 8bda7c10f71a2e27467bac9c50c3a08a3958b7ff Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 24 Feb 2024 11:09:44 -0500 Subject: [PATCH 13/18] Remove cmd_set from component --- .../contrib/base_systems/components/component.py | 14 +------------- evennia/contrib/base_systems/components/holder.py | 2 -- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/evennia/contrib/base_systems/components/component.py b/evennia/contrib/base_systems/components/component.py index d2ea83fac5..ec07e54e94 100644 --- a/evennia/contrib/base_systems/components/component.py +++ b/evennia/contrib/base_systems/components/component.py @@ -50,8 +50,6 @@ class Component(metaclass=BaseComponent): name = "" slot = None - cmd_set: CmdSet = None - _fields = {} def __init__(self, host=None): @@ -115,11 +113,7 @@ class Component(metaclass=BaseComponent): Component: The loaded instance of the component """ - inst = cls(host) - if inst.cmd_set: - host.cmdset.add(inst.cmd_set) - - return inst + return cls(host) def at_added(self, host): """ @@ -132,9 +126,6 @@ class Component(metaclass=BaseComponent): if self.host and self.host != host: raise exceptions.InvalidComponentError("Components must not register twice!") - if self.cmd_set: - self.host.cmdset.add(self.cmd_set) - self.host = host def at_removed(self, host): @@ -148,9 +139,6 @@ class Component(metaclass=BaseComponent): if host != self.host: raise ValueError("Component attempted to remove from the wrong host.") - if self.cmd_set: - self.host.cmdset.remove(self.cmd_set) - self.host = None @property diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 65fb51d22d..55d00f028c 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -118,8 +118,6 @@ class ComponentHandler: field.at_removed(component) component.at_removed(self.host) - if component.cmd_set: - self.host.cmdset.remove(component.cmd_set) self.host.tags.remove(component.name, category="components") self.host.signals.remove_object_listeners_and_responders(component) From 0729de1b91bd251bb401b1f584a5a24239dfed56 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 24 Feb 2024 11:12:49 -0500 Subject: [PATCH 14/18] Use get_component_slot --- evennia/contrib/base_systems/components/dbfield.py | 6 +++--- evennia/contrib/base_systems/components/holder.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index 709548acf3..eae4285c17 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -29,7 +29,7 @@ class DBField(AttributeProperty): owner (Component): The component classF on which this is set name (str): The name that was used to set the DBField. """ - self._key = f"{owner.slot or owner.name}::{name}" + self._key = f"{owner.get_component_slot()}::{name}" owner.add_field(name, self) def at_added(self, component): @@ -69,7 +69,7 @@ class NDBField(NAttributeProperty): owner (Component): The component class on which this is set name (str): The name that was used to set the DBField. """ - self._key = f"{owner.slot or owner.name}::{name}" + self._key = f"{owner.get_component_slot()}::{name}" owner.add_field(name, self) def at_added(self, component): @@ -113,7 +113,7 @@ class TagField: Called when TagField is first assigned to the class. It is called with the component class and the name of the field. """ - self._category_key = f"{owner.slot or owner.name}::{name}" + self._category_key = f"{owner.get_component_slot()}::{name}" owner.add_field(name, self) def __get__(self, instance, owner): diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 55d00f028c..a09844f457 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -34,7 +34,7 @@ class ComponentProperty: if not self.component_class: component_class = get_component_class(self.name) self.component_class = component_class - self.slot_name = component_class.slot or component_class.name + self.slot_name = component_class.get_component_slot() component = instance.components.get(self.slot_name) return component @@ -107,7 +107,7 @@ class ComponentHandler: """ name = component.name - slot_name = component.slot or name + slot_name = component.get_component_slot() if not self.has(slot_name): message = ( f"Cannot remove {name} from {self.host.name} as it is not registered." @@ -181,7 +181,7 @@ class ComponentHandler: """ Sets the loaded component in this instance. """ - slot_name = component.slot or component.name + slot_name = component.get_component_slot() self._loaded_components[slot_name] = component self.host.signals.add_object_listeners_and_responders(component) From c4ec977b9cf79c218574dfcdb30f20bfb1619a52 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 24 Feb 2024 14:31:01 -0500 Subject: [PATCH 15/18] Refactor _get_class_components --- .../base_systems/components/exceptions.py | 4 ++ .../contrib/base_systems/components/holder.py | 46 ++++++++----------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/evennia/contrib/base_systems/components/exceptions.py b/evennia/contrib/base_systems/components/exceptions.py index 7a2254c631..c331e7a608 100644 --- a/evennia/contrib/base_systems/components/exceptions.py +++ b/evennia/contrib/base_systems/components/exceptions.py @@ -8,3 +8,7 @@ class ComponentDoesNotExist(ValueError): class ComponentIsNotRegistered(ValueError): pass + + +class ComponentSlotRegisteredTwice(ValueError): + pass diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index a09844f457..238cc6b35f 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -42,14 +42,6 @@ class ComponentProperty: def __set__(self, instance, value): raise Exception("Cannot set a class property") - def __set_name__(self, owner, name): - # Retrieve the class_components set on the direct class only - class_components = owner.__dict__.get("_class_components", []) - if not class_components: - setattr(owner, "_class_components", class_components) - - class_components.append((self.name, self.values)) - class ComponentHandler: """ @@ -270,27 +262,29 @@ class ComponentHolderMixin: return getattr(self, "_signal_handler", None) def _get_class_components(self): - class_components = {} + self_class = type(self) + class_components = getattr(self_class, "_class_components", None) + if class_components is not None: + return class_components - def base_type_iterator(): - base_stack = [type(self)] - while base_stack: - _base_type = base_stack.pop() - yield _base_type - base_stack.extend(_base_type.__bases__) + class_components_by_slot = {} + for att_name in dir(self_class): + if att_name.startswith("__"): + continue - for base_type in base_type_iterator(): - base_class_components = getattr(base_type, "_class_components", ()) - for cmp_name, cmp_values in base_class_components: + att_obj = getattr(self_class, att_name, None) + if isinstance(att_obj, ComponentProperty): + cmp_name = att_obj.name cmp_class = get_component_class(cmp_name) cmp_slot = cmp_class.get_component_slot() - class_components[cmp_slot] = (cmp_name, cmp_values) + if cmp_slot in class_components_by_slot: + raise exceptions.ComponentSlotRegisteredTwice( + f"Component slot={cmp_slot} is registered twice on class={self_class}" + ) - # TODO Is this necessary? - instance_components = getattr(self, "_class_components", ()) - 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) + class_components_by_slot[cmp_slot] = (cmp_name, att_obj.values) - return tuple(class_components.values()) + class_components = tuple(class_components_by_slot.values()) + setattr(self_class, "_class_components", class_components) + + return class_components From 5f9064c7384e4e4eebb010b64389461576fc948c Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 24 Feb 2024 14:31:07 -0500 Subject: [PATCH 16/18] Add tests --- .../contrib/base_systems/components/tests.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index 79ccf1bbbd..6fd2664d2b 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -20,6 +20,11 @@ class ComponentTestA(Component): 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' @@ -27,6 +32,13 @@ class InheritedComponentTestA(ComponentTestA): 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): name = "test_b" my_int = DBField(default=1) @@ -50,11 +62,18 @@ class ComponentTestD(Component): 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, CharacterMixinWithComponents, DefaultCharacter): +class CharacterWithComponents( + ComponentHolderMixin, ShadowedCharacterMixin, CharacterMixinWithComponents, DefaultCharacter +): test_a = ComponentProperty("test_a") test_b = ComponentProperty("test_b", my_int=3, my_list=[1, 2, 3]) ic_a = ComponentProperty("inherited_test_a", my_other_int=4) @@ -91,6 +110,15 @@ class TestComponents(EvenniaTest): self.assertEquals(self.char1.test_b.my_int, 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) @@ -240,6 +268,10 @@ class TestComponents(EvenniaTest): 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): @signals.as_listener From cca93b032f82cb678cb47781ec0290a6e9ef3a83 Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 24 Feb 2024 14:35:46 -0500 Subject: [PATCH 17/18] Revert "Refactor _get_class_components" This reverts commit c4ec977b9cf79c218574dfcdb30f20bfb1619a52. --- .../base_systems/components/exceptions.py | 4 -- .../contrib/base_systems/components/holder.py | 46 +++++++++++-------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/evennia/contrib/base_systems/components/exceptions.py b/evennia/contrib/base_systems/components/exceptions.py index c331e7a608..7a2254c631 100644 --- a/evennia/contrib/base_systems/components/exceptions.py +++ b/evennia/contrib/base_systems/components/exceptions.py @@ -8,7 +8,3 @@ class ComponentDoesNotExist(ValueError): class ComponentIsNotRegistered(ValueError): pass - - -class ComponentSlotRegisteredTwice(ValueError): - pass diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 238cc6b35f..a09844f457 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -42,6 +42,14 @@ class ComponentProperty: def __set__(self, instance, value): raise Exception("Cannot set a class property") + def __set_name__(self, owner, name): + # Retrieve the class_components set on the direct class only + class_components = owner.__dict__.get("_class_components", []) + if not class_components: + setattr(owner, "_class_components", class_components) + + class_components.append((self.name, self.values)) + class ComponentHandler: """ @@ -262,29 +270,27 @@ class ComponentHolderMixin: return getattr(self, "_signal_handler", None) def _get_class_components(self): - self_class = type(self) - class_components = getattr(self_class, "_class_components", None) - if class_components is not None: - return class_components + class_components = {} - class_components_by_slot = {} - for att_name in dir(self_class): - if att_name.startswith("__"): - continue + def base_type_iterator(): + base_stack = [type(self)] + while base_stack: + _base_type = base_stack.pop() + yield _base_type + base_stack.extend(_base_type.__bases__) - att_obj = getattr(self_class, att_name, None) - if isinstance(att_obj, ComponentProperty): - cmp_name = att_obj.name + 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() - if cmp_slot in class_components_by_slot: - raise exceptions.ComponentSlotRegisteredTwice( - f"Component slot={cmp_slot} is registered twice on class={self_class}" - ) + class_components[cmp_slot] = (cmp_name, cmp_values) - class_components_by_slot[cmp_slot] = (cmp_name, att_obj.values) + # TODO Is this necessary? + instance_components = getattr(self, "_class_components", ()) + 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) - class_components = tuple(class_components_by_slot.values()) - setattr(self_class, "_class_components", class_components) - - return class_components + return tuple(class_components.values()) From bdeedf8c03b5c8536ebea92e0ff366560397ecbe Mon Sep 17 00:00:00 2001 From: ChrisLR Date: Sat, 24 Feb 2024 14:38:41 -0500 Subject: [PATCH 18/18] Remove todo --- evennia/contrib/base_systems/components/holder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index a09844f457..bdaf22b21b 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -286,7 +286,6 @@ class ComponentHolderMixin: cmp_slot = cmp_class.get_component_slot() class_components[cmp_slot] = (cmp_name, cmp_values) - # TODO Is this necessary? instance_components = getattr(self, "_class_components", ()) for cmp_name, cmp_values in instance_components: cmp_class = get_component_class(cmp_name)