Merge pull request #2692 from ChrisLR/components-tagfield

Add TagField to Contrib Components
This commit is contained in:
Griatch 2022-03-27 18:15:54 +02:00 committed by GitHub
commit 2aea48c850
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 10 deletions

View file

@ -50,7 +50,29 @@ class Health(Component):
health = DBField(default=1)
```
Note that default is optional and will default to None
Note that default is optional and will default to None.
Adding a component to a host will also a similarly named tag with 'components' as category.
A Component named health will appear as key="health, category="components".
This allows you to retrieve objects with specific components by searching with the tag.
It is also possible to add Component Tags the same way, using TagField.
TagField accepts a default value and can be used to store a single or multiple tags.
Default values are automatically added when the component is added.
Component Tags are cleared from the host if the component is removed.
Example:
```python
from evennia.contrib.base_systems.components import Component, TagField
class Health(Component):
resistances = TagField()
vulnerability = TagField(default="fire", enforce_single=True)
```
The 'resistances' field in this example can be set to multiple times and it will keep the added tags.
The 'vulnerability' field in this example will override the previous tag with the new one.
Each typeclass using the ComponentHolderMixin can declare its components

View file

@ -9,7 +9,7 @@ See the docs for more information.
"""
from evennia.contrib.base_systems.components.component import Component
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField, TagField
from evennia.contrib.base_systems.components.holder import ComponentHolderMixin, ComponentProperty

View file

@ -144,6 +144,11 @@ class Component:
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

View file

@ -52,3 +52,64 @@ class NDBField(NAttributeProperty):
ndb_fields = {}
setattr(owner, '_ndb_fields', ndb_fields)
ndb_fields[name] = self
class TagField:
"""
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
self._default = default
self._enforce_single = enforce_single
def __set_name__(self, owner, 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
def __get__(self, instance, owner):
"""
Called when retrieving the value of the TagField.
It is called with the component instance and the class.
"""
tag_value = instance.host.tags.get(
default=self._default,
category=self._category_key,
)
return tag_value
def __set__(self, instance, value):
"""
Called when setting a value on the TagField.
It is called with the component instance and the value.
"""
tag_handler = instance.host.tags
if self._enforce_single:
tag_handler.clear(category=self._category_key)
tag_handler.add(
key=value,
category=self._category_key,
)
def __delete__(self, instance):
"""
Used when 'del' is called on the TagField.
It is called with the component instance.
"""
instance.host.tags.clear(category=self._category_key)

View file

@ -64,7 +64,7 @@ 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)
def add_default(self, name):
@ -85,9 +85,24 @@ 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:
setattr(component, tag_field_name, default_tag)
def remove(self, component):
"""
Method to remove a component instance from a host.
@ -100,9 +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")
del self._loaded_components[component_name]
else:
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
@ -123,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.
@ -225,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:

View file

@ -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")
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")