diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 99a5d9dded..7132943959 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -598,6 +598,7 @@ class AttributeHandler(object): [attr.delete() for attr in self._cache.values()] self._cache = {} self._catcache = {} + self._cache_complete = False def all(self, accessing_obj=None, default_access=True): """ diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index c915ca1bfb..84edcf729d 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -30,12 +30,12 @@ class Tag(models.Model): any number of tags, stored via its db_tags property. Tagging similar objects will make it easier to quickly locate the group later (such as when implementing zones). The main advantage of - tagging as opposed to using Attributes is speed; a tag is very + tagging as opposed to using tags is speed; a tag is very limited in what data it can hold, and the tag key+category is indexed for efficient lookup in the database. Tags are shared between objects - a new tag is only created if the key+category combination did not previously exist, making them unsuitable for - storing object-related data (for this a full Attribute should be + storing object-related data (for this a full tag should be used). The 'db_data' field is intended as a documentation field for the @@ -65,10 +65,10 @@ class Tag(models.Model): index_together = (('db_key', 'db_category', 'db_tagtype'),) def __unicode__(self): - return u"%s" % self.db_key + return u"" % self.db_key def __str__(self): - return str(self.db_key) + return str("" % self.db_key) # @@ -96,19 +96,122 @@ class TagHandler(object): self.obj = obj self._objid = obj.id self._model = obj.__dbclass__.__name__.lower() - self._cache = None + self._cache = {} + # store category names fully cached + self._catcache = {} + # full cache was run on all tags + self._cache_complete = False - def _recache(self): - """ - Cache all tags of this object. - - """ + def _fullcache(self): + "Cache all tags of this object" query = {"%s__id" % self._model : self._objid, "tag__db_tagtype" : self._tagtype} - tagobjs = [conn.tag for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] - self._cache = dict(("%s-%s" % (to_str(tagobj.db_key).lower(), - tagobj.db_category.lower() if tagobj.db_category else None), - tagobj) for tagobj in tagobjs) + tags = [conn.tag for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)] + self._cache = dict(("%s-%s" % (to_str(tag.db_key).lower(), + tag.db_category.lower() if tag.db_category else None), + tag) for tag in tags) + self._cache_complete = True + + def _getcache(self, key=None, category=None): + """ + Retrieve from cache or database (always caches) + + Args: + key (str, optional): Tag key to query for + category (str, optional): Tag category + + Returns: + args (list): Returns a list of zero or more matches + found from cache or database. + Notes: + When given a category only, a search for all objects + of that category is done and a the category *name* is is + stored. This tells the system on subsequent calls that the + list of cached tags of this category is up-to-date + and that the cache can be queried for category matches + without missing any. + The TYPECLASS_AGGRESSIVE_CACHE=False setting will turn off + caching, causing each tag access to trigger a + database lookup. + + """ + key = key.strip().lower() if key else None + category = category.strip().lower() if category else None + if key: + cachekey = "%s-%s" % (key, category) + tag = _TYPECLASS_AGGRESSIVE_CACHE and self._cache.get(cachekey, None) + if tag: + return [tag] # return cached entity + else: + query = {"%s__id" % self._model : self._objid, + "tag__db_tagtype" : self._tagtype, + "tag__db_key__iexact" : key.lower(), + "tag__db_category__iexact" : category.lower() if category else None} + conn = getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query) + if conn: + tag = conn[0].tag + self._cache[cachekey] = tag + return [tag] + else: + # only category given (even if it's None) - we can't + # assume the cache to be complete unless we have queried + # for this category before + catkey = "-%s" % category + if _TYPECLASS_AGGRESSIVE_CACHE and catkey in self._catcache: + return [tag for key, tag in self._cache.items() if key.endswith(catkey)] + else: + # we have to query to make this category up-date in the cache + query = {"%s__id" % self._model : self._objid, + "tag__db_tagtype" : self._tagtype, + "tag__db_category__iexact" : category.lower() if category else None} + tags = [conn.tag for conn in getattr(self.obj, + self._m2m_fieldname).through.objects.filter(**query)] + for tag in tags: + cachekey = "%s-%s" % (tag.db_key, category) + self._cache[cachekey] = tag + # mark category cache as up-to-date + self._catcache[catkey] = True + return tags + return [] + + def _setcache(self, key, category, tag_obj): + """ + Update cache. + + Args: + key (str): A cleaned key string + category (str or None): A cleaned category name + tag_obj (tag): The newly saved tag + + """ + if not key: # don't allow an empty key in cache + return + cachekey = "%s-%s" % (key, category) + catkey = "-%s" % category + self._cache[cachekey] = tag_obj + # mark that the category cache is no longer up-to-date + self._catcache.pop(catkey, None) + self._cache_complete = False + + def _delcache(self, key, category): + """ + Remove tag from cache + + Args: + key (str): A cleaned key string + category (str or None): A cleaned category name + + """ + catkey = "-%s" % category + if key: + cachekey = "%s-%s" % (key, category) + self._cache.pop(cachekey, None) + else: + [self._cache.pop(key, None) for key in self._cache if key.endswith(catkey)] + # mark that the category cache is no longer up-to-date + self._catcache.pop(catkey, None) + self._cache_complete = False + def add(self, tag=None, category=None, data=None): """ @@ -142,12 +245,9 @@ class TagHandler(object): tagobj = self.obj.__class__.objects.create_tag(key=tagstr, category=category, data=data, tagtype=self._tagtype) getattr(self.obj, self._m2m_fieldname).add(tagobj) - if self._cache is None: - self._recache() - cachestring = "%s-%s" % (tagstr, category) - self._cache[cachestring] = tagobj + self._setcache(tagstr, category, tagobj) - def get(self, key, default=None, category=None, return_tagobj=False): + def get(self, key=None, default=None, category=None, return_tagobj=False): """ Get the tag for the given key or list of tags. @@ -166,13 +266,10 @@ class TagHandler(object): depending on `return_tagobj`. """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() ret = [] - category = category.strip().lower() if category is not None else None - searchkey = ["%s-%s" % (key.strip().lower(), category) if key is not None else None for key in make_iter(key)] - ret = [val for val in (self._cache.get(keystr) for keystr in searchkey) if val] - ret = [to_str(tag.db_data) for tag in ret] if return_tagobj else ret + for keystr in make_iter(key): + ret.extend([tag if return_tagobj else tag.db_key + for tag in self._getcache(key, category)]) return ret[0] if len(ret) == 1 else (ret if ret else default) def remove(self, key, category=None): @@ -197,7 +294,7 @@ class TagHandler(object): tagobj = self.obj.db_tags.filter(db_key=tagstr, db_category=category) if tagobj: getattr(self.obj, self._m2m_fieldname).remove(tagobj[0]) - self._recache() + self._delcache(key, category) def clear(self, category=None): """ @@ -213,7 +310,9 @@ class TagHandler(object): getattr(self.obj, self._m2m_fieldname).clear() else: getattr(self.obj, self._m2m_fieldname).filter(db_category=category).delete() - self._recache() + self._cache = {} + self._catcache = {} + self._cache_complete = False def all(self, category=None, return_key_and_category=False): """ @@ -232,21 +331,14 @@ class TagHandler(object): `return_key_and_category` is set. """ - if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: - self._recache() - if category: - category = category.strip().lower() if category is not None else None - matches = [tag for tag in self._cache.values() if tag.db_category == category] - else: - matches = self._cache.values() - - if matches: - matches = sorted(matches, key=lambda o: o.id) - if return_key_and_category: + if not self._cache_complete: + self._fullcache() + tags = sorted(self._getcache(None, category), key=lambda o:o.id) + if return_key_and_category: # return tuple (key, category) - return [(to_str(p.db_key), to_str(p.db_category)) for p in matches] - else: - return [to_str(p.db_key) for p in matches] + return [(to_str(tag.db_key), to_str(tag.db_category)) for tag in tags] + else: + return [to_str(tag.db_key) for tag in tags] return [] def __str__(self):