Fix AttributeProperty access with mutable default. Resolve #3488
This commit is contained in:
parent
c8d75665d2
commit
b8e37f9cf2
3 changed files with 129 additions and 27 deletions
|
|
@ -14,10 +14,11 @@
|
||||||
- [Fix][pull3485]: Typo in `sethome` message (chiizujin)
|
- [Fix][pull3485]: Typo in `sethome` message (chiizujin)
|
||||||
- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no
|
- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no
|
||||||
arguments (InspectorCaracal)
|
arguments (InspectorCaracal)
|
||||||
- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization
|
- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch)
|
||||||
(Griatch)
|
|
||||||
- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on
|
- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on
|
||||||
a reload (regression).
|
a reload (regression).
|
||||||
|
- [Fix][issue3488]: `AttributeProperty(<default>, autocreate=False)`, where
|
||||||
|
`<default>` was mutable would not update/save properly in-place (Griatch)
|
||||||
- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe
|
- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe
|
||||||
the hooks called on server start/stop/reload (Griatch)
|
the hooks called on server start/stop/reload (Griatch)
|
||||||
- [Docs] Doc typo fixes (Griatch, chiizujin)
|
- [Docs] Doc typo fixes (Griatch, chiizujin)
|
||||||
|
|
@ -27,6 +28,7 @@
|
||||||
[pull3487]: https://github.com/evennia/evennia/pull/3487
|
[pull3487]: https://github.com/evennia/evennia/pull/3487
|
||||||
[issue3476]: https://github.com/evennia/evennia/issues/3476
|
[issue3476]: https://github.com/evennia/evennia/issues/3476
|
||||||
[issue3477]: https://github.com/evennia/evennia/issues/3477
|
[issue3477]: https://github.com/evennia/evennia/issues/3477
|
||||||
|
[issue3488]: https://github.com/evennia/evennia/issues/3488
|
||||||
[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html
|
[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,8 @@ from unittest import skip
|
||||||
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.typeclasses.attributes import AttributeProperty
|
from evennia.typeclasses.attributes import AttributeProperty
|
||||||
from evennia.typeclasses.tags import (
|
from evennia.typeclasses.tags import (AliasProperty, PermissionProperty,
|
||||||
AliasProperty,
|
TagCategoryProperty, TagProperty)
|
||||||
PermissionProperty,
|
|
||||||
TagCategoryProperty,
|
|
||||||
TagProperty,
|
|
||||||
)
|
|
||||||
from evennia.utils import create, search
|
from evennia.utils import create, search
|
||||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||||
|
|
||||||
|
|
@ -356,6 +352,10 @@ class TestObjectPropertiesClass(DefaultObject):
|
||||||
attr2 = AttributeProperty(default="attr2", category="attrcategory")
|
attr2 = AttributeProperty(default="attr2", category="attrcategory")
|
||||||
attr3 = AttributeProperty(default="attr3", autocreate=False)
|
attr3 = AttributeProperty(default="attr3", autocreate=False)
|
||||||
attr4 = SubAttributeProperty(default="attr4")
|
attr4 = SubAttributeProperty(default="attr4")
|
||||||
|
attr5 = AttributeProperty(default=list, autocreate=False)
|
||||||
|
attr6 = AttributeProperty(default=[None], autocreate=False)
|
||||||
|
attr7 = AttributeProperty(default=list)
|
||||||
|
attr8 = AttributeProperty(default=[None])
|
||||||
cusattr = CustomizedProperty(default=5)
|
cusattr = CustomizedProperty(default=5)
|
||||||
tag1 = TagProperty()
|
tag1 = TagProperty()
|
||||||
tag2 = TagProperty(category="tagcategory")
|
tag2 = TagProperty(category="tagcategory")
|
||||||
|
|
@ -541,3 +541,99 @@ class TestProperties(EvenniaTestCase):
|
||||||
|
|
||||||
obj1.delete()
|
obj1.delete()
|
||||||
obj2.delete()
|
obj2.delete()
|
||||||
|
|
||||||
|
def test_not_create_attribute_with_autocreate_false(self):
|
||||||
|
"""
|
||||||
|
Test that AttributeProperty with autocreate=False does not create an attribute in the database.
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj = create.create_object(TestObjectPropertiesClass, key="obj1")
|
||||||
|
|
||||||
|
self.assertEqual(obj.attr3, "attr3")
|
||||||
|
self.assertEqual(obj.attributes.get("attr3"), None)
|
||||||
|
|
||||||
|
self.assertEqual(obj.attr5, [])
|
||||||
|
self.assertEqual(obj.attributes.get("attr5"), None)
|
||||||
|
|
||||||
|
obj.delete()
|
||||||
|
|
||||||
|
def test_callable_defaults__autocreate_false(self):
|
||||||
|
"""
|
||||||
|
Test https://github.com/evennia/evennia/issues/3488, where a callable default value like `list`
|
||||||
|
would produce an infinitely empty result even when appended to.
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
|
||||||
|
obj2 = create.create_object(TestObjectPropertiesClass, key="obj2")
|
||||||
|
|
||||||
|
self.assertEqual(obj1.attr5, [])
|
||||||
|
obj1.attr5.append(1)
|
||||||
|
self.assertEqual(obj1.attr5, [1])
|
||||||
|
|
||||||
|
# check cross-instance sharing
|
||||||
|
self.assertEqual(obj2.attr5, [], "cross-instance sharing detected")
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutable_defaults__autocreate_false(self):
|
||||||
|
"""
|
||||||
|
Test https://github.com/evennia/evennia/issues/3488, where a mutable default value (like a
|
||||||
|
list `[]` or `[None]`) would not be updated in the database when appended to.
|
||||||
|
|
||||||
|
Note that using a mutable default value is not recommended, as the mutable will share the
|
||||||
|
same memory space across all instances of the class. This means that if one instance modifiesA
|
||||||
|
the mutable, all instances will be affected.
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
|
||||||
|
obj2 = create.create_object(TestObjectPropertiesClass, key="obj2")
|
||||||
|
|
||||||
|
self.assertEqual(obj1.attr6, [None])
|
||||||
|
obj1.attr6.append(1)
|
||||||
|
self.assertEqual(obj1.attr6, [None, 1])
|
||||||
|
|
||||||
|
obj1.attr6[1] = 2
|
||||||
|
self.assertEqual(obj1.attr6, [None, 2])
|
||||||
|
|
||||||
|
# check cross-instance sharing
|
||||||
|
self.assertEqual(obj2.attr6, [None], "cross-instance sharing detected")
|
||||||
|
|
||||||
|
obj1.delete()
|
||||||
|
obj2.delete()
|
||||||
|
|
||||||
|
def test_callable_defaults__autocreate_true(self):
|
||||||
|
"""
|
||||||
|
Test callables with autocreate=True.
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
|
||||||
|
obj2 = create.create_object(TestObjectPropertiesClass, key="obj1")
|
||||||
|
|
||||||
|
self.assertEqual(obj1.attr7, [])
|
||||||
|
obj1.attr7.append(1)
|
||||||
|
self.assertEqual(obj1.attr7, [1])
|
||||||
|
|
||||||
|
# check cross-instance sharing
|
||||||
|
self.assertEqual(obj2.attr7, [])
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutable_defaults__autocreate_true(self):
|
||||||
|
"""
|
||||||
|
Test mutable defaults with autocreate=True.
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj1 = create.create_object(TestObjectPropertiesClass, key="obj1")
|
||||||
|
obj2 = create.create_object(TestObjectPropertiesClass, key="obj2")
|
||||||
|
|
||||||
|
self.assertEqual(obj1.attr8, [None])
|
||||||
|
obj1.attr8.append(1)
|
||||||
|
self.assertEqual(obj1.attr8, [None, 1])
|
||||||
|
|
||||||
|
obj1.attr8[1] = 2
|
||||||
|
self.assertEqual(obj1.attr8, [None, 2])
|
||||||
|
|
||||||
|
# check cross-instance sharing
|
||||||
|
self.assertEqual(obj2.attr8, [None])
|
||||||
|
|
||||||
|
obj1.delete()
|
||||||
|
obj2.delete()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ which is a non-db version of Attributes.
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
|
||||||
from evennia.locks.lockhandler import LockHandler
|
from evennia.locks.lockhandler import LockHandler
|
||||||
from evennia.utils.dbserialize import from_pickle, to_pickle
|
from evennia.utils.dbserialize import from_pickle, to_pickle
|
||||||
from evennia.utils.idmapper.models import SharedMemoryModel
|
from evennia.utils.idmapper.models import SharedMemoryModel
|
||||||
|
|
@ -166,6 +166,7 @@ class AttributeProperty:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attrhandler_name = "attributes"
|
attrhandler_name = "attributes"
|
||||||
|
cached_default_name_template = "_property_attribute_default_{key}"
|
||||||
|
|
||||||
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True):
|
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True):
|
||||||
"""
|
"""
|
||||||
|
|
@ -207,21 +208,6 @@ class AttributeProperty:
|
||||||
self._autocreate = autocreate
|
self._autocreate = autocreate
|
||||||
self._key = ""
|
self._key = ""
|
||||||
|
|
||||||
@property
|
|
||||||
def _default(self):
|
|
||||||
"""
|
|
||||||
Tries returning a new instance of default if callable.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if callable(self.__default):
|
|
||||||
return self.__default()
|
|
||||||
|
|
||||||
return self.__default
|
|
||||||
|
|
||||||
@_default.setter
|
|
||||||
def _default(self, value):
|
|
||||||
self.__default = value
|
|
||||||
|
|
||||||
def __set_name__(self, cls, name):
|
def __set_name__(self, cls, name):
|
||||||
"""
|
"""
|
||||||
Called when descriptor is first assigned to the class. It is called with
|
Called when descriptor is first assigned to the class. It is called with
|
||||||
|
|
@ -230,17 +216,35 @@ class AttributeProperty:
|
||||||
"""
|
"""
|
||||||
self._key = name
|
self._key = name
|
||||||
|
|
||||||
|
def _get_and_cache_default(self, instance):
|
||||||
|
"""
|
||||||
|
Get and cache the default value for this attribute. We make sure to convert any mutables
|
||||||
|
into _Saver* equivalent classes here and cache the result on the instance's AttributeHandler.
|
||||||
|
|
||||||
|
"""
|
||||||
|
attrhandler = getattr(instance, self.attrhandler_name)
|
||||||
|
value = getattr(attrhandler, self.cached_default_name_template.format(key=self._key), None)
|
||||||
|
if not value:
|
||||||
|
if callable(self._default):
|
||||||
|
value = self._default()
|
||||||
|
else:
|
||||||
|
value = copy(self._default)
|
||||||
|
value = from_pickle(value, db_obj=instance)
|
||||||
|
setattr(attrhandler, self.cached_default_name_template.format(key=self._key), value)
|
||||||
|
return value
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
"""
|
"""
|
||||||
Called when the attrkey is retrieved from the instance.
|
Called when the attrkey is retrieved from the instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
value = self._default
|
value = self._get_and_cache_default(instance)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = self.at_get(
|
value = self.at_get(
|
||||||
getattr(instance, self.attrhandler_name).get(
|
getattr(instance, self.attrhandler_name).get(
|
||||||
key=self._key,
|
key=self._key,
|
||||||
default=self._default,
|
default=value,
|
||||||
category=self._category,
|
category=self._category,
|
||||||
strattr=self._strattr,
|
strattr=self._strattr,
|
||||||
raise_exception=self._autocreate,
|
raise_exception=self._autocreate,
|
||||||
|
|
@ -250,7 +254,7 @@ class AttributeProperty:
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if self._autocreate:
|
if self._autocreate:
|
||||||
# attribute didn't exist and autocreate is set
|
# attribute didn't exist and autocreate is set
|
||||||
self.__set__(instance, self._default)
|
self.__set__(instance, value)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue