Add TagProperty, AliasProperty, PermissionProperty. Default autocreate=True for AttributeProperty.

This commit is contained in:
Griatch 2022-04-09 15:39:39 +02:00
parent 8f1f604708
commit ef7280f55a
14 changed files with 334 additions and 160 deletions

View file

@ -176,7 +176,7 @@ class AttributeProperty:
attrhandler_name = "attributes"
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=False):
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True):
"""
Initialize an Attribute as a property descriptor.
@ -188,12 +188,12 @@ class AttributeProperty:
lockstring (str): This is not itself useful with the property, but only if
using the full AttributeHandler.get(accessing_obj=...) to access the
Attribute.
autocreate (bool): If an un-found Attr should lead to auto-creating the
Attribute (with the default value). If `False`, the property will
return the default value until it has been explicitly set. This means
less database accesses, but also means the property will have no
corresponding Attribute if wanting to access it directly via the
AttributeHandler (it will also not show up in `examine`).
autocreate (bool): True by default; this means Evennia makes sure to create a new
copy of the Attribute (with the default value) whenever a new object with this
property is created. If `False`, no Attribute will be created until the property
is explicitly assigned a value. This makes it more efficient while it retains
its default (there's no db access), but without an actual Attribute generated,
one cannot access it via .db, the AttributeHandler or see it with `examine`.
"""
self._default = default
@ -218,21 +218,20 @@ class AttributeProperty:
"""
value = self._default
try:
value = getattr(instance, self.attrhandler_name).get(
value = self.at_get(getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
category=self._category,
strattr=self._strattr,
raise_exception=self._autocreate,
)
))
except AttributeError:
if self._autocreate:
# attribute didn't exist and autocreate is set
self.__set__(instance, self._default)
else:
raise
finally:
return value
return value
def __set__(self, instance, value):
"""
@ -242,7 +241,7 @@ class AttributeProperty:
(
getattr(instance, self.attrhandler_name).add(
self._key,
value,
self.at_set(value),
category=self._category,
lockstring=self._lockstring,
strattr=self._strattr,
@ -251,10 +250,43 @@ class AttributeProperty:
def __delete__(self, instance):
"""
Called when running `del` on the field. Will remove/clear the Attribute.
Called when running `del` on the property. Will remove/clear the Attribute. Note that
the Attribute will be recreated next retrieval unless the AttributeProperty is also
removed in code!
"""
(getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category))
getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category)
def at_set(self, value):
"""
The value to set is passed through the method. It can be used to customize/validate
the input in a custom child class.
Args:
value (any): The value about to the stored in this Attribute.
Returns:
any: The value to store.
Raises:
AttributeError: If the value is invalid to store.
"""
return value
def at_get(self, value):
"""
The value returned from the Attribute is passed through this method. It can be used
to react to the retrieval or modify the result in some way.
Args:
value (any): Value returned from the Attribute.
Returns:
any: The value to return to the caller.
"""
return value
class NAttributeProperty(AttributeProperty):

View file

@ -325,6 +325,18 @@ class TypedObject(SharedMemoryModel):
super().__init__(*args, **kwargs)
self.set_class_from_typeclass(typeclass_path=typeclass_path)
def init_evennia_properties(self):
"""
Called by creation methods; makes sure to initialize Attribute/TagProperties
by fetching them once.
"""
for propkey, prop in self.__class__.__dict__.items():
if hasattr(prop, "__set_name__"):
try:
getattr(self, propkey)
except Exception:
log_trace()
# initialize all handlers in a lazy fashion
@lazy_property
def attributes(self):

View file

@ -96,6 +96,75 @@ class Tag(models.Model):
# Handlers making use of the Tags model
#
class TagProperty:
"""
Tag property descriptor. Allows for setting tags on an object as Django-like 'fields'
on the class level. Since Tags are almost always used for querying, Tags are always
created/assigned along with the object. Make sure the property/tagname does not collide
with an existing method/property on the class. If it does, you must use tags.add()
instead.
Example:
::
class Character(DefaultCharacter):
mytag = TagProperty() # category=None
mytag2 = TagProperty(category="tagcategory")
"""
taghandler_name = "tags"
def __init__(self, category=None, data=None):
self._category = category
self._data = data
self._key = ""
def __set_name__(self, cls, name):
"""
Called when descriptor is first assigned to the class (not the instance!).
It is called with the name of the field.
"""
self._key = name
def __get__(self, instance, owner):
"""
Called when accessing the tag as a property on the instance.
"""
try:
return getattr(instance, self.taghandler_name).get(
key=self._key,
category=self._category,
return_list=False,
raise_exception=True
)
except AttributeError:
self.__set__(instance, self._category)
def __set__(self, instance, category):
"""
Assign a new category to the tag. It's not possible to set 'data' this way.
"""
self._category = category
(
getattr(instance, self.taghandler_name).add(
key=self._key,
category=self._category,
data=self._data
)
)
def __delete__(self, instance):
"""
Called when running `del` on the property. Will disconnect the object from
the Tag. Note that the tag will be readded on next fetch unless the
TagProperty is also removed in code!
"""
getattr(instance, self.taghandler_name).remove(key=self._key, category=self._category)
class TagHandler(object):
"""
@ -361,7 +430,8 @@ class TagHandler(object):
return ret[0] if len(ret) == 1 else ret
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False):
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False,
raise_exception=False):
"""
Get the tag for the given key, category or combination of the two.
@ -376,6 +446,8 @@ class TagHandler(object):
instead of a string representation of the Tag.
return_list (bool, optional): Always return a list, regardless
of number of matches.
raise_exception (bool, optional): Raise AttributeError if no matches
are found.
Returns:
tags (list): The matches, either string
@ -383,6 +455,9 @@ class TagHandler(object):
depending on `return_tagobj`. If 'default' is set, this
will be a list with the default value as its only element.
Raises:
AttributeError: If finding no matches and `raise_exception` is True.
"""
ret = []
for keystr in make_iter(key):
@ -393,9 +468,14 @@ class TagHandler(object):
for tag in self._getcache(keystr, category)
]
)
if return_list:
return ret if ret else [default] if default is not None else []
return ret[0] if len(ret) == 1 else (ret if ret else default)
if not ret:
if raise_exception:
raise AttributeError(f"No tags found matching input {key}, {category}.")
elif return_list:
return [default] if default is not None else []
else:
return default
return ret if return_list else (ret[0] if len(ret) == 1 else ret)
def remove(self, key=None, category=None):
"""
@ -521,6 +601,21 @@ class TagHandler(object):
return ",".join(self.all())
class AliasProperty(TagProperty):
"""
Allows for setting aliases like Django fields:
::
class Character(DefaultCharacter):
# note that every character will get the alias bob. Make sure
# the alias property does not collide with an existing method
# or property on the class.
bob = AliasProperty()
"""
taghandler_name = "aliases"
class AliasHandler(TagHandler):
"""
A handler for the Alias Tag type.
@ -530,6 +625,20 @@ class AliasHandler(TagHandler):
_tagtype = "alias"
class PermissionProperty(TagProperty):
"""
Allows for setting permissions like Django fields:
::
class Character(DefaultCharacter):
# note that every character will get this permission! Make
# sure it doesn't collide with an existing method or property.
myperm = PermissionProperty()
"""
taghandler_name = "permissions"
class PermissionHandler(TagHandler):
"""
A handler for the Permission Tag type.

View file

@ -3,7 +3,7 @@ Unit tests for typeclass base system
"""
from django.test import override_settings
from evennia.utils.test_resources import BaseEvenniaTest
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
from evennia.typeclasses import attributes
from mock import patch
from parameterized import parameterized