Add TagProperty, AliasProperty, PermissionProperty. Default autocreate=True for AttributeProperty.
This commit is contained in:
parent
8f1f604708
commit
ef7280f55a
14 changed files with 334 additions and 160 deletions
|
|
@ -1253,6 +1253,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self.basetype_setup()
|
||||
self.at_account_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
permissions = [settings.PERMISSION_ACCOUNT_DEFAULT]
|
||||
if hasattr(self, "_createdict"):
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self.basetype_setup()
|
||||
self.at_channel_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this is only set if the channel was created
|
||||
# with the utils.create.create_channel function.
|
||||
|
|
|
|||
|
|
@ -1228,6 +1228,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self.basetype_setup()
|
||||
self.at_object_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create function
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.typeclasses.tags import TagProperty, AliasProperty, PermissionProperty
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.objects.objects import DefaultObject
|
||||
from evennia.utils import create
|
||||
|
||||
|
||||
|
|
@ -227,3 +230,53 @@ class TestContentHandler(BaseEvenniaTest):
|
|||
self.obj2.move_to(self.room1)
|
||||
self.obj2.move_to(self.room2)
|
||||
self.assertEqual(self.room2.contents, [self.obj1, self.obj2])
|
||||
|
||||
|
||||
class TestObjectPropertiesClass(DefaultObject):
|
||||
attr1 = AttributeProperty(default="attr1")
|
||||
attr2 = AttributeProperty(default="attr2", category="attrcategory")
|
||||
attr3 = AttributeProperty(default="attr3", autocreate=False)
|
||||
tag1 = TagProperty()
|
||||
tag2 = TagProperty(category="tagcategory")
|
||||
testalias = AliasProperty()
|
||||
testperm = PermissionProperty()
|
||||
|
||||
class TestProperties(EvenniaTestCase):
|
||||
"""
|
||||
Test Properties.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.obj = create.create_object(TestObjectPropertiesClass, key="testobj")
|
||||
|
||||
def tearDown(self):
|
||||
self.obj.delete()
|
||||
|
||||
def test_properties(self):
|
||||
"""
|
||||
Test all properties assigned at class level.
|
||||
"""
|
||||
obj = self.obj
|
||||
|
||||
self.assertEqual(obj.db.attr1, "attr1")
|
||||
self.assertEqual(obj.attributes.get("attr1"), "attr1")
|
||||
self.assertEqual(obj.attr1, "attr1")
|
||||
|
||||
self.assertEqual(obj.attributes.get("attr2", category="attrcategory"), "attr2")
|
||||
self.assertEqual(obj.db.attr2, None) # category mismatch
|
||||
self.assertEqual(obj.attr2, "attr2")
|
||||
|
||||
self.assertEqual(obj.db.attr3, None) # non-autocreate, so not in db yet
|
||||
self.assertFalse(obj.attributes.has("attr3"))
|
||||
self.assertEqual(obj.attr3, "attr3")
|
||||
|
||||
obj.attr3 = "attr3b" # stores it in db!
|
||||
|
||||
self.assertEqual(obj.db.attr3, "attr3b")
|
||||
self.assertTrue(obj.attributes.has("attr3"))
|
||||
|
||||
self.assertTrue(obj.tags.has("tag1"))
|
||||
self.assertTrue(obj.tags.has("tag2", category="tagcategory"))
|
||||
|
||||
self.assertTrue(obj.aliases.has("testalias"))
|
||||
self.assertTrue(obj.permissions.has("testperm"))
|
||||
|
|
|
|||
|
|
@ -400,7 +400,10 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
overriding the call (unused by default).
|
||||
|
||||
"""
|
||||
self.basetype_setup()
|
||||
self.at_script_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create_script
|
||||
|
|
@ -471,6 +474,14 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
super().delete()
|
||||
return True
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
Changes fundamental aspects of the type. Usually changes are made in at_script creation
|
||||
instead.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
Called when the Script is cached in the idmapper. This is usually more reliable
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue