diff --git a/evennia/contrib/base_systems/components/dbfield.py b/evennia/contrib/base_systems/components/dbfield.py index c63ab1bebc..e653d8bc17 100644 --- a/evennia/contrib/base_systems/components/dbfield.py +++ b/evennia/contrib/base_systems/components/dbfield.py @@ -56,7 +56,13 @@ class NDBField(NAttributeProperty): class TagField: """ - Component Descriptor to add a tag to the host. + Component Tags Descriptor. + Allows you to set Tags related to a component on the class. + The tags are set with a prefixed category, so it can support + multiple tags or enforce a single one. + + Default value of a tag is added when the component is registered. + Tags are removed if the component itself is removed. """ def __init__(self, default=None, enforce_single=False): self._category_key = None @@ -65,9 +71,8 @@ class TagField: def __set_name__(self, owner, name): """ - Called when descriptor is first assigned to the class. It is called with - the name of the field. - + Called when descriptor is first assigned to the class. + It is called with the name of the field. """ self._category_key = f"{owner.name}__{name}" tag_fields = getattr(owner, "_tag_fields", None) @@ -89,7 +94,7 @@ class TagField: tag_handler.clear(category=self._category_key) tag_handler.add( - key=self._key, + key=value, category=self._category_key, ) diff --git a/evennia/contrib/base_systems/components/holder.py b/evennia/contrib/base_systems/components/holder.py index 651ae5840a..ddd606151d 100644 --- a/evennia/contrib/base_systems/components/holder.py +++ b/evennia/contrib/base_systems/components/holder.py @@ -64,12 +64,8 @@ class ComponentHandler: """ self._set_component(component) self.db_names.append(component.name) - self.host.tags.add(component.name, category="components") + self._add_component_tags(component) component.at_added(self.host) - 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 add_default(self, name): """ @@ -89,8 +85,19 @@ class ComponentHandler: new_component = component.default_create(self.host) self._set_component(new_component) self.db_names.append(name) - self.host.tags.add(name, category="components") + self._add_component_tags(new_component) new_component.at_added(self.host) + + 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: @@ -108,11 +115,9 @@ class ComponentHandler: """ 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.tags.remove(component_name, category="components") - for tag_field_name in component.tag_field_names: - self.host.tags.remove() del self._loaded_components[component_name] else: message = f"Cannot remove {component_name} from {self.host.name} as it is not registered." @@ -133,11 +138,24 @@ class ComponentHandler: message = f"Cannot remove {name} from {self.host.name} as it is not registered." raise ComponentIsNotRegistered(message) + self._remove_component_tags(instance) instance.at_removed(self.host) self.db_names.remove(name) - self.host.tags.remove(name, category="components") + 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. @@ -227,10 +245,6 @@ class ComponentHolderMixin(object): component = component_class.create(self, **values) component_names.append(component_name) self.components._loaded_components[component_name] = component - 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) self.db.component_names = component_names @@ -239,8 +253,8 @@ class ComponentHolderMixin(object): Method that add component related tags that were set using ComponentProperty. """ super().basetype_posthook_setup() - for component_name in self.db.component_names: - self.tags.add(component_name, category="components") + for component in self.components._loaded_components.values(): + self.components._add_component_tags(component) @property def components(self) -> ComponentHandler: diff --git a/evennia/contrib/base_systems/components/tests.py b/evennia/contrib/base_systems/components/tests.py index b9b18f28ba..aa81bdf818 100644 --- a/evennia/contrib/base_systems/components/tests.py +++ b/evennia/contrib/base_systems/components/tests.py @@ -1,4 +1,4 @@ -from evennia.contrib.base_systems.components import Component, DBField +from evennia.contrib.base_systems.components import Component, DBField, TagField from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin from evennia.objects.objects import DefaultCharacter from evennia.utils.test_resources import EvenniaTest @@ -14,12 +14,17 @@ class ComponentTestB(Component): name = "test_b" my_int = DBField(default=1) my_list = DBField(default=[]) + default_tag = TagField(default="initial_value") + single_tag = TagField(enforce_single=True) + multiple_tags = TagField() + default_single_tag = TagField(default="initial_value", enforce_single=True) class RuntimeComponentTestC(Component): name = "test_c" my_int = DBField(default=6) my_dict = DBField(default={}) + added_tag = TagField(default="added_value") class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter): @@ -111,7 +116,11 @@ class TestComponents(EvenniaTest): 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") def test_host_has_added_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -119,12 +128,16 @@ class TestComponents(EvenniaTest): 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" 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" def test_host_remove_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -134,6 +147,7 @@ class TestComponents(EvenniaTest): 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") def test_host_remove_by_name_component_tags(self): rct = RuntimeComponentTestC.create(self.char1) @@ -142,4 +156,33 @@ class TestComponents(EvenniaTest): assert 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") \ No newline at end of file + 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") + + 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") + + 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") + + def test_component_tags_support_multiple_values_by_default(self): + test_b = self.char1.components.get('test_b') + test_b.multiple_tags = "first value" + test_b.multiple_tags = "second value" + test_b.multiple_tags = "third value" + + assert 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")