Add TagCategoryProperty.
This commit is contained in:
parent
85a8cd613b
commit
030887dd21
11 changed files with 251 additions and 65 deletions
|
|
@ -51,6 +51,7 @@ Msg = None
|
|||
# Properties
|
||||
AttributeProperty = None
|
||||
TagProperty = None
|
||||
TagCategoryProperty = None
|
||||
|
||||
# commands
|
||||
Command = None
|
||||
|
|
@ -157,7 +158,7 @@ def _init(portal_mode=False):
|
|||
global GLOBAL_SCRIPTS, OPTION_CLASSES
|
||||
global EvMenu, EvTable, EvForm, EvMore, EvEditor
|
||||
global ANSIString
|
||||
global AttributeProperty, TagProperty
|
||||
global AttributeProperty, TagProperty, TagCategoryProperty
|
||||
|
||||
# Parent typeclasses
|
||||
# utilities
|
||||
|
|
@ -172,12 +173,7 @@ def _init(portal_mode=False):
|
|||
from .comms.models import ChannelDB, Msg
|
||||
from .locks import lockfuncs
|
||||
from .objects.models import ObjectDB
|
||||
from .objects.objects import (
|
||||
DefaultCharacter,
|
||||
DefaultExit,
|
||||
DefaultObject,
|
||||
DefaultRoom,
|
||||
)
|
||||
from .objects.objects import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
||||
from .prototypes.spawner import spawn
|
||||
from .scripts.models import ScriptDB
|
||||
from .scripts.monitorhandler import MONITOR_HANDLER
|
||||
|
|
@ -186,7 +182,7 @@ def _init(portal_mode=False):
|
|||
from .scripts.tickerhandler import TICKER_HANDLER
|
||||
from .server import signals
|
||||
from .typeclasses.attributes import AttributeProperty
|
||||
from .typeclasses.tags import TagProperty
|
||||
from .typeclasses.tags import TagCategoryProperty, TagProperty
|
||||
from .utils import ansi, gametime, logger
|
||||
from .utils.ansi import ANSIString
|
||||
|
||||
|
|
@ -218,22 +214,23 @@ def _init(portal_mode=False):
|
|||
search_script,
|
||||
search_tag,
|
||||
)
|
||||
|
||||
from .utils.utils import class_from_module
|
||||
|
||||
if portal_mode:
|
||||
# Set up the PortalSessionHandler
|
||||
from evennia.server.portal import portalsessionhandler
|
||||
|
||||
portal_sess_handler_class = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS)
|
||||
portalsessionhandler.PORTAL_SESSIONS = portal_sess_handler_class()
|
||||
else:
|
||||
# Create the ServerSesssionHandler
|
||||
from evennia.server import sessionhandler
|
||||
|
||||
sess_handler_class = class_from_module(settings.SERVER_SESSION_HANDLER_CLASS)
|
||||
sessionhandler.SESSIONS = sess_handler_class()
|
||||
sessionhandler.SESSION_HANDLER = sessionhandler.SESSIONS
|
||||
SESSION_HANDLER = sessionhandler.SESSIONS
|
||||
|
||||
|
||||
# API containers
|
||||
|
||||
class _EvContainer(object):
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.objects.objects import DefaultObject
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.typeclasses.tags import AliasProperty, PermissionProperty, TagProperty
|
||||
from evennia.typeclasses.tags import (
|
||||
AliasProperty,
|
||||
PermissionProperty,
|
||||
TagCategoryProperty,
|
||||
TagProperty,
|
||||
)
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||
|
||||
|
||||
class DefaultObjectTest(BaseEvenniaTest):
|
||||
|
||||
ip = "212.216.139.14"
|
||||
|
||||
def test_object_create(self):
|
||||
|
|
@ -60,7 +63,10 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
|
||||
def test_exit_create(self):
|
||||
description = "The steaming depths of the dumpster, ripe with refuse in various states of decomposition."
|
||||
description = (
|
||||
"The steaming depths of the dumpster, ripe with refuse in various states of"
|
||||
" decomposition."
|
||||
)
|
||||
obj, errors = DefaultExit.create(
|
||||
"in", self.room1, self.room2, account=self.account, description=description, ip=self.ip
|
||||
)
|
||||
|
|
@ -279,6 +285,8 @@ class TestObjectPropertiesClass(DefaultObject):
|
|||
testperm = PermissionProperty()
|
||||
awaretest = 5
|
||||
settest = 0
|
||||
tagcategory1 = TagCategoryProperty("category_tag1")
|
||||
tagcategory2 = TagCategoryProperty("category_tag1", "category_tag2", "category_tag3")
|
||||
|
||||
@property
|
||||
def base_property(self):
|
||||
|
|
@ -299,10 +307,7 @@ class TestProperties(EvenniaTestCase):
|
|||
def tearDown(self):
|
||||
self.obj.delete()
|
||||
|
||||
def test_properties(self):
|
||||
"""
|
||||
Test all properties assigned at class level.
|
||||
"""
|
||||
def test_attribute_properties(self):
|
||||
obj = self.obj
|
||||
|
||||
self.assertEqual(obj.db.attr1, "attr1")
|
||||
|
|
@ -326,6 +331,9 @@ class TestProperties(EvenniaTestCase):
|
|||
self.assertEqual(obj.db.attr3, "attr3b")
|
||||
self.assertTrue(obj.attributes.has("attr3"))
|
||||
|
||||
def test_tag_properties(self):
|
||||
obj = self.obj
|
||||
|
||||
self.assertTrue(obj.tags.has("tag1"))
|
||||
self.assertTrue(obj.tags.has("tag2", category="tagcategory"))
|
||||
self.assertTrue(obj.tags.has("tag3"))
|
||||
|
|
@ -337,6 +345,15 @@ class TestProperties(EvenniaTestCase):
|
|||
# only Attribute or TagProperties.
|
||||
self.assertFalse(hasattr(obj, "property_initialized"))
|
||||
|
||||
def test_tag_category_properties(self):
|
||||
obj = self.obj
|
||||
|
||||
self.assertFalse(obj.tags.has("category_tag1")) # no category
|
||||
self.assertTrue(obj.tags.has("category_tag1", category="tagcategory1"))
|
||||
self.assertTrue(obj.tags.has("category_tag1", category="tagcategory2"))
|
||||
self.assertTrue(obj.tags.has("category_tag2", category="tagcategory2"))
|
||||
self.assertTrue(obj.tags.has("category_tag3", category="tagcategory2"))
|
||||
|
||||
def test_object_awareness(self):
|
||||
"""Test the "object-awareness" of customized AttributeProperty getter/setters"""
|
||||
obj = self.obj
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ from django.db.models.base import ModelBase
|
|||
from django.urls import reverse
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.text import slugify
|
||||
|
||||
from evennia.locks.lockhandler import LockHandler
|
||||
from evennia.server.signals import SIGNAL_TYPED_OBJECT_POST_RENAME
|
||||
from evennia.typeclasses import managers
|
||||
|
|
@ -50,6 +49,7 @@ from evennia.typeclasses.tags import (
|
|||
AliasHandler,
|
||||
PermissionHandler,
|
||||
Tag,
|
||||
TagCategoryProperty,
|
||||
TagHandler,
|
||||
TagProperty,
|
||||
)
|
||||
|
|
@ -343,7 +343,7 @@ class TypedObject(SharedMemoryModel):
|
|||
by fetching them once.
|
||||
"""
|
||||
for propkey, prop in self.__class__.__dict__.items():
|
||||
if isinstance(prop, (AttributeProperty, TagProperty)):
|
||||
if isinstance(prop, (AttributeProperty, TagProperty, TagCategoryProperty)):
|
||||
try:
|
||||
getattr(self, propkey)
|
||||
except Exception:
|
||||
|
|
@ -626,7 +626,8 @@ class TypedObject(SharedMemoryModel):
|
|||
raise RuntimeError(
|
||||
"Cannot use swap_typeclass on time-dependent "
|
||||
"Script '%s'.\nStop and start a new Script of the "
|
||||
"right type instead." % self.key
|
||||
"right type instead."
|
||||
% self.key
|
||||
)
|
||||
|
||||
self.typeclass_path = new_typeclass.path
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from collections import defaultdict
|
|||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from evennia.locks.lockfuncs import perm as perm_lockfunc
|
||||
from evennia.utils.utils import make_iter, to_str
|
||||
|
||||
|
|
@ -99,28 +98,31 @@ class Tag(models.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.
|
||||
|
||||
Note that while you _can_ check e.g. `obj.tagname,this will give an AttributeError
|
||||
if the Tag is not set. Most often you want to use `obj.tags.get("tagname")` to check
|
||||
if a tag is set on an object.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
mytag = TagProperty() # category=None
|
||||
mytag2 = TagProperty(category="tagcategory")
|
||||
|
||||
Tag Property.
|
||||
"""
|
||||
|
||||
taghandler_name = "tags"
|
||||
|
||||
def __init__(self, category=None, data=None):
|
||||
"""
|
||||
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.
|
||||
|
||||
Note that while you _can_ check e.g. `obj.tagname,this will give an AttributeError
|
||||
if the Tag is not set. Most often you want to use `obj.tags.get("tagname")` to check
|
||||
if a tag is set on an object.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
mytag = TagProperty() # category=None
|
||||
mytag2 = TagProperty(category="tagcategory")
|
||||
"""
|
||||
|
||||
self._category = category
|
||||
self._data = data
|
||||
self._key = ""
|
||||
|
|
@ -167,6 +169,129 @@ class TagProperty:
|
|||
getattr(instance, self.taghandler_name).remove(key=self._key, category=self._category)
|
||||
|
||||
|
||||
class TagCategoryProperty:
|
||||
"""
|
||||
Tag Category Property.
|
||||
|
||||
"""
|
||||
|
||||
taghandler_name = "tags"
|
||||
|
||||
def __init__(self, *args):
|
||||
"""
|
||||
Assign a property for a Tag Category, with any number of Tag keys.
|
||||
This is often more useful than the `TagProperty` since it's common to want to check which
|
||||
tags of a particular category the object is a member of.
|
||||
|
||||
Args:
|
||||
*args (str or callable): Tag keys to assign to this property, using the category given
|
||||
by the name of the property. If a callable, it will be called without arguments
|
||||
to return the tag key. It is not possible to set tag `data` this way (use the
|
||||
Taghandler directly for that). Tag keys are not case sensitive.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input is not a valid tag key or tuple.
|
||||
|
||||
Notes:
|
||||
It is not possible to set Tags with a `None` category using a `TagCategoryProperty` -
|
||||
use `obj.tags.add()` instead.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
class RogueCharacter(DefaultCharacter):
|
||||
guild = TagProperty("thieves_guild", "merchant_guild")
|
||||
|
||||
"""
|
||||
self._category = ""
|
||||
self._tags = self._parse_tag_input(*args)
|
||||
|
||||
def _parse_tag_input(self, *args):
|
||||
"""
|
||||
Parse input to the property.
|
||||
|
||||
Args:
|
||||
*args (str or callable): Tags, either as strings or `callable`, which should return
|
||||
the tag key when called without arguments. Keys are not case sensitive.
|
||||
|
||||
Returns:
|
||||
list: A list of tag keys.
|
||||
|
||||
"""
|
||||
tags = []
|
||||
for tagkey in args:
|
||||
if callable(tagkey):
|
||||
tagkey = tagkey()
|
||||
tags.append((str(tagkey).lower()))
|
||||
return tags
|
||||
|
||||
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._category = name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
Called when accessing the tag as a property on the instance. Returns a list
|
||||
of tags under the given category.
|
||||
"""
|
||||
taghandler = getattr(instance, self.taghandler_name)
|
||||
|
||||
tags = []
|
||||
add_new = []
|
||||
for tagkey in self._tags:
|
||||
try:
|
||||
tag = taghandler.get(
|
||||
key=tagkey, category=self._category, return_list=False, raise_exception=True
|
||||
)
|
||||
except AttributeError:
|
||||
add_new.append(tagkey)
|
||||
else:
|
||||
tags.append(tag)
|
||||
if add_new:
|
||||
for new_tag in add_new:
|
||||
# we must remove this from the internal store or system will think it already
|
||||
# existed when determining the sets in __set__
|
||||
self._tags.remove(new_tag)
|
||||
self.__set__(instance, *add_new)
|
||||
|
||||
return tags
|
||||
|
||||
def __set__(self, instance, *args):
|
||||
"""
|
||||
Assign a new set of tags to the category. This replaces the previous set of tags.
|
||||
|
||||
"""
|
||||
taghandler = getattr(instance, self.taghandler_name)
|
||||
|
||||
old_tags = set(self._tags)
|
||||
new_tags = set(self._parse_tag_input(*args))
|
||||
|
||||
# new_tags could be a sub/superset of old tags
|
||||
removed_tags = old_tags - new_tags
|
||||
added_tags = new_tags - old_tags
|
||||
|
||||
# remove tags
|
||||
for tag in removed_tags:
|
||||
taghandler.remove(key=tag, category=self._category)
|
||||
|
||||
# add new tags (won't re-add if obj already had it)
|
||||
taghandler.batch_add(*[(tag, self._category) for tag in added_tags])
|
||||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Called when running `del` on the property. Will remove all tags of this
|
||||
category from the object. Note that the tags will be readded on next fetch
|
||||
unless the TagCategoryProperty is also removed in code!
|
||||
|
||||
"""
|
||||
for tagkey in self.tags:
|
||||
getattr(instance, self.taghandler_name).remove(key=self.tagkey, category=self._category)
|
||||
|
||||
|
||||
class TagHandler(object):
|
||||
"""
|
||||
Generic tag-handler. Accessed via TypedObject.tags.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue