Trunk: Merged the Devel-branch (branches/griatch) into /trunk. This constitutes a major refactoring of Evennia. Development will now continue in trunk. See the wiki and the past posts to the mailing list for info. /Griatch
This commit is contained in:
parent
df29defbcd
commit
f83c2bddf8
222 changed files with 22304 additions and 14371 deletions
9
src/utils/idmapper/LICENSE
Normal file
9
src/utils/idmapper/LICENSE
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Copyright (c) 2009, David Cramer <dcramer@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
41
src/utils/idmapper/__init__.py
Executable file
41
src/utils/idmapper/__init__.py
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
import os.path
|
||||
import warnings
|
||||
|
||||
__version__ = (0, 2)
|
||||
|
||||
def _get_git_revision(path):
|
||||
revision_file = os.path.join(path, 'refs', 'heads', 'master')
|
||||
if not os.path.exists(revision_file):
|
||||
return None
|
||||
fh = open(revision_file, 'r')
|
||||
try:
|
||||
return fh.read()
|
||||
finally:
|
||||
fh.close()
|
||||
|
||||
def get_revision():
|
||||
"""
|
||||
:returns: Revision number of this branch/checkout, if available. None if
|
||||
no revision number can be determined.
|
||||
"""
|
||||
package_dir = os.path.dirname(__file__)
|
||||
checkout_dir = os.path.normpath(os.path.join(package_dir, '..'))
|
||||
path = os.path.join(checkout_dir, '.git')
|
||||
if os.path.exists(path):
|
||||
return _get_git_revision(path)
|
||||
return None
|
||||
|
||||
__build__ = get_revision()
|
||||
|
||||
def lazy_object(location):
|
||||
def inner(*args, **kwargs):
|
||||
parts = location.rsplit('.', 1)
|
||||
warnings.warn('`idmapper.%s` is deprecated. Please use `%s` instead.' % (parts[1], location), DeprecationWarning)
|
||||
imp = __import__(parts[0], globals(), locals(), [parts[1]], -1)
|
||||
func = getattr(imp, parts[1])
|
||||
if callable(func):
|
||||
return func(*args, **kwargs)
|
||||
return func
|
||||
return inner
|
||||
|
||||
SharedMemoryModel = lazy_object('idmapper.models.SharedMemoryModel')
|
||||
129
src/utils/idmapper/base.py
Executable file
129
src/utils/idmapper/base.py
Executable file
|
|
@ -0,0 +1,129 @@
|
|||
from weakref import WeakValueDictionary
|
||||
|
||||
from django.db.models.base import Model, ModelBase
|
||||
|
||||
from manager import SharedMemoryManager
|
||||
|
||||
class SharedMemoryModelBase(ModelBase):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
super_new = super(ModelBase, cls).__new__
|
||||
parents = [b for b in bases if isinstance(b, SharedMemoryModelBase)]
|
||||
if not parents:
|
||||
# If this isn't a subclass of Model, don't do anything special.
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
return super(SharedMemoryModelBase, cls).__new__(cls, name, bases, attrs)
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
"""
|
||||
this method will either create an instance (by calling the default implementation)
|
||||
or try to retrieve one from the class-wide cache by infering the pk value from
|
||||
args and kwargs. If instance caching is enabled for this class, the cache is
|
||||
populated whenever possible (ie when it is possible to infer the pk value).
|
||||
"""
|
||||
def new_instance():
|
||||
return super(SharedMemoryModelBase, cls).__call__(*args, **kwargs)
|
||||
|
||||
instance_key = cls._get_cache_key(args, kwargs)
|
||||
# depending on the arguments, we might not be able to infer the PK, so in that case we create a new instance
|
||||
if instance_key is None:
|
||||
return new_instance()
|
||||
|
||||
cached_instance = cls.get_cached_instance(instance_key)
|
||||
if cached_instance is None:
|
||||
cached_instance = new_instance()
|
||||
cls.cache_instance(cached_instance)
|
||||
|
||||
return cached_instance
|
||||
|
||||
def _prepare(cls):
|
||||
cls.__instance_cache__ = WeakValueDictionary()
|
||||
super(SharedMemoryModelBase, cls)._prepare()
|
||||
|
||||
|
||||
|
||||
class SharedMemoryModel(Model):
|
||||
# XXX: this is creating a model and it shouldn't be.. how do we properly
|
||||
# subclass now?
|
||||
__metaclass__ = SharedMemoryModelBase
|
||||
|
||||
def _get_cache_key(cls, args, kwargs):
|
||||
"""
|
||||
This method is used by the caching subsystem to infer the PK value from the constructor arguments.
|
||||
It is used to decide if an instance has to be built or is already in the cache.
|
||||
"""
|
||||
result = None
|
||||
# Quick hack for my composites work for now.
|
||||
if hasattr(cls._meta, 'pks'):
|
||||
pk = cls._meta.pks[0]
|
||||
else:
|
||||
pk = cls._meta.pk
|
||||
# get the index of the pk in the class fields. this should be calculated *once*, but isn't atm
|
||||
pk_position = cls._meta.fields.index(pk)
|
||||
if len(args) > pk_position:
|
||||
# if it's in the args, we can get it easily by index
|
||||
result = args[pk_position]
|
||||
elif pk.attname in kwargs:
|
||||
# retrieve the pk value. Note that we use attname instead of name, to handle the case where the pk is a
|
||||
# a ForeignKey.
|
||||
result = kwargs[pk.attname]
|
||||
elif pk.name != pk.attname and pk.name in kwargs:
|
||||
# ok we couldn't find the value, but maybe it's a FK and we can find the corresponding object instead
|
||||
result = kwargs[pk.name]
|
||||
|
||||
if result is not None and isinstance(result, Model):
|
||||
# if the pk value happens to be a model instance (which can happen wich a FK), we'd rather use its own pk as the key
|
||||
result = result._get_pk_val()
|
||||
return result
|
||||
_get_cache_key = classmethod(_get_cache_key)
|
||||
|
||||
def get_cached_instance(cls, id):
|
||||
"""
|
||||
Method to retrieve a cached instance by pk value. Returns None when not found
|
||||
(which will always be the case when caching is disabled for this class). Please
|
||||
note that the lookup will be done even when instance caching is disabled.
|
||||
"""
|
||||
return cls.__instance_cache__.get(id)
|
||||
get_cached_instance = classmethod(get_cached_instance)
|
||||
|
||||
def cache_instance(cls, instance):
|
||||
"""
|
||||
Method to store an instance in the cache.
|
||||
"""
|
||||
if instance._get_pk_val() is not None:
|
||||
cls.__instance_cache__[instance._get_pk_val()] = instance
|
||||
cache_instance = classmethod(cache_instance)
|
||||
|
||||
def _flush_cached_by_key(cls, key):
|
||||
del cls.__instance_cache__[key]
|
||||
_flush_cached_by_key = classmethod(_flush_cached_by_key)
|
||||
|
||||
def flush_cached_instance(cls, instance):
|
||||
"""
|
||||
Method to flush an instance from the cache. The instance will always be flushed from the cache,
|
||||
since this is most likely called from delete(), and we want to make sure we don't cache dead objects.
|
||||
"""
|
||||
cls._flush_cached_by_key(instance._get_pk_val())
|
||||
flush_cached_instance = classmethod(flush_cached_instance)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(SharedMemoryModel, self).save(*args, **kwargs)
|
||||
self.__class__.cache_instance(self)
|
||||
|
||||
# TODO: This needs moved to the prepare stage (I believe?)
|
||||
objects = SharedMemoryManager()
|
||||
|
||||
from django.db.models.signals import pre_delete
|
||||
|
||||
# Use a signal so we make sure to catch cascades.
|
||||
def flush_singleton_cache(sender, instance, **kwargs):
|
||||
# XXX: Is this the best way to make sure we can flush?
|
||||
if isinstance(instance, SharedMemoryModel):
|
||||
instance.__class__.flush_cached_instance(instance)
|
||||
pre_delete.connect(flush_singleton_cache)
|
||||
|
||||
# XXX: It's to be determined if we should use this or not.
|
||||
# def update_singleton_cache(sender, instance, **kwargs):
|
||||
# if isinstance(instance.__class__, SharedMemoryModel):
|
||||
# instance.__class__.cache_instance(instance)
|
||||
# post_save.connect(flush_singleton_cache)
|
||||
15
src/utils/idmapper/manager.py
Executable file
15
src/utils/idmapper/manager.py
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
from django.db.models.manager import Manager
|
||||
|
||||
class SharedMemoryManager(Manager):
|
||||
# TODO: improve on this implementation
|
||||
# We need a way to handle reverse lookups so that this model can
|
||||
# still use the singleton cache, but the active model isn't required
|
||||
# to be a SharedMemoryModel.
|
||||
def get(self, **kwargs):
|
||||
items = kwargs.keys()
|
||||
inst = None
|
||||
if len(items) == 1 and items[0] in ('pk', self.model._meta.pk.attname):
|
||||
inst = self.model.get_cached_instance(kwargs[items[0]])
|
||||
if inst is None:
|
||||
inst = super(SharedMemoryManager, self).get(**kwargs)
|
||||
return inst
|
||||
2
src/utils/idmapper/models.py
Executable file
2
src/utils/idmapper/models.py
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
from django.db.models import *
|
||||
from base import SharedMemoryModel
|
||||
70
src/utils/idmapper/tests.py
Executable file
70
src/utils/idmapper/tests.py
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from base import SharedMemoryModel
|
||||
from django.db import models
|
||||
|
||||
class Category(SharedMemoryModel):
|
||||
name = models.CharField(max_length=32)
|
||||
|
||||
class RegularCategory(models.Model):
|
||||
name = models.CharField(max_length=32)
|
||||
|
||||
class Article(SharedMemoryModel):
|
||||
name = models.CharField(max_length=32)
|
||||
category = models.ForeignKey(Category)
|
||||
category2 = models.ForeignKey(RegularCategory)
|
||||
|
||||
class RegularArticle(models.Model):
|
||||
name = models.CharField(max_length=32)
|
||||
category = models.ForeignKey(Category)
|
||||
category2 = models.ForeignKey(RegularCategory)
|
||||
|
||||
class SharedMemorysTest(TestCase):
|
||||
# TODO: test for cross model relation (singleton to regular)
|
||||
|
||||
def setUp(self):
|
||||
n = 0
|
||||
category = Category.objects.create(name="Category %d" % (n,))
|
||||
regcategory = RegularCategory.objects.create(name="Category %d" % (n,))
|
||||
|
||||
for n in xrange(0, 10):
|
||||
Article.objects.create(name="Article %d" % (n,), category=category, category2=regcategory)
|
||||
RegularArticle.objects.create(name="Article %d" % (n,), category=category, category2=regcategory)
|
||||
|
||||
def testSharedMemoryReferences(self):
|
||||
article_list = Article.objects.all().select_related('category')
|
||||
last_article = article_list[0]
|
||||
for article in article_list[1:]:
|
||||
self.assertEquals(article.category is last_article.category, True)
|
||||
last_article = article
|
||||
|
||||
def testRegularReferences(self):
|
||||
article_list = RegularArticle.objects.all().select_related('category')
|
||||
last_article = article_list[0]
|
||||
for article in article_list[1:]:
|
||||
self.assertEquals(article.category2 is last_article.category2, False)
|
||||
last_article = article
|
||||
|
||||
def testMixedReferences(self):
|
||||
article_list = RegularArticle.objects.all().select_related('category')
|
||||
last_article = article_list[0]
|
||||
for article in article_list[1:]:
|
||||
self.assertEquals(article.category is last_article.category, True)
|
||||
last_article = article
|
||||
|
||||
article_list = Article.objects.all().select_related('category')
|
||||
last_article = article_list[0]
|
||||
for article in article_list[1:]:
|
||||
self.assertEquals(article.category2 is last_article.category2, False)
|
||||
last_article = article
|
||||
|
||||
def testObjectDeletion(self):
|
||||
# This must execute first so its guaranteed to be in memory.
|
||||
article_list = list(Article.objects.all().select_related('category'))
|
||||
|
||||
article = Article.objects.all()[0:1].get()
|
||||
pk = article.pk
|
||||
article.delete()
|
||||
self.assertEquals(pk not in Article.__instance_cache__, True)
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue