Refactor Component registering with cherry-picked additions

This commit is contained in:
ChrisLR 2023-12-14 12:30:53 -05:00
parent 6e096fa3cb
commit 217bd711e7
6 changed files with 194 additions and 174 deletions

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,32 @@ 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):
@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. 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 +36,17 @@ 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
cmd_set: CmdSet = 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 +90,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):
@ -88,12 +117,11 @@ class Component:
host (object): The host typeclass instance 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.cmd_set:
if self.host == host: self.host.cmdset.add(self.cmd_set)
return
else:
raise ComponentRegisterError("Components must not register twice!")
self.host = host self.host = host
@ -106,7 +134,11 @@ 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.")
if self.cmd_set:
self.host.cmdset.remove(self.cmd_set)
self.host = None self.host = None
@property @property
@ -131,25 +163,10 @@ 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
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

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,7 +18,10 @@ 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.
@ -21,13 +29,15 @@ class DBField(AttributeProperty):
owner (object): The component classF on which this is set owner (object): 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.slot or owner.name}::{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, instance):
db_fields = {} if self._autocreate:
setattr(owner, "_db_fields", db_fields) self.__set__(instance, self._default)
db_fields[name] = self
def at_removed(self, instance):
self.__delete__(instance)
class NDBField(NAttributeProperty): class NDBField(NAttributeProperty):
@ -37,7 +47,7 @@ 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.
@ -45,13 +55,15 @@ class NDBField(NAttributeProperty):
owner (object): The component class on which this is set owner (object): 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.slot or owner.name}::{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, instance):
ndb_fields = {} if self._autocreate:
setattr(owner, "_ndb_fields", ndb_fields) self.__set__(instance, self._default)
ndb_fields[name] = self
def at_removed(self, instance):
self.__delete__(instance)
class TagField: class TagField:
@ -70,17 +82,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.slot or owner.name}::{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 +122,10 @@ 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, instance):
if self._default:
self.__set__(instance, self._default)
def at_removed(self, instance):
self.__delete__(instance)

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
class ComponentProperty: class ComponentProperty:
@ -17,19 +17,19 @@ 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
def __get__(self, instance, owner): def __get__(self, instance, owner):
component = instance.components.get(self.component_name) component = instance.components.get(self.name)
return component return component
def __set__(self, instance, value): def __set__(self, instance, value):
@ -37,13 +37,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 +55,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 +65,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(self.host)
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 +85,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 +99,26 @@ 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.slot or name
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(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): def remove_by_name(self, name):
""" """
@ -140,49 +127,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 +166,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.slot or component.name
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 +208,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 +232,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 +264,21 @@ 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", ())
class_components.update({cmp[0]: cmp[1] for cmp in base_class_components})
class ComponentIsNotRegistered(Exception): instance_components = getattr(self, "_class_components", ())
pass class_components.update({cmp[0]: cmp[1] for cmp in instance_components})
return tuple(class_components.items())

View file

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