First non-tested version of moving typeclasses to proxy models.

This commit is contained in:
Griatch 2014-12-19 16:29:41 +01:00
parent 8e8d85a4fe
commit 236c0d17d3
9 changed files with 403 additions and 622 deletions

View file

@ -3,17 +3,18 @@ Default Typeclass for Comms.
See objects.objects for more information on Typeclassing.
"""
from src.comms import Msg, TempMsg
from src.typeclasses.typeclass import TypeClass
from src.comms.models import Msg, TempMsg, ChannelDB
from src.typeclasses.models import TypeclassBase
from src.utils import logger
from src.utils.utils import make_iter
class Channel(TypeClass):
class Channel(ChannelDB):
"""
This is the base class for all Comms. Inherit from this to create different
types of communication channels.
"""
__metaclass__ = TypeclassBase
# helper methods, for easy overloading

View file

@ -134,7 +134,6 @@ class ObjectDB(TypedObject):
contents - other objects having this object as location
exits - exits from this object
"""
#
# ObjectDB Database model setup
#

View file

@ -16,7 +16,8 @@ they control by simply linking to a new object's user property.
"""
from django.conf import settings
from src.typeclasses.typeclass import TypeClass
from src.objects.models import ObjectDB
from src.typeclasses.models import TypeclassBase
from src.commands import cmdset, command
from src.utils.logger import log_depmsg
@ -31,11 +32,12 @@ _DA = object.__delattr__
# Base class to inherit from.
#
class Object(TypeClass):
class Object(ObjectDB):
"""
This is the base class for all in-game objects. Inherit from this
to create different types of objects in the game.
"""
__metaclass__ = TypeclassBase
# __init__ is only defined here in order to present docstring to API.
def __init__(self, dbobj):
"""

View file

@ -13,7 +13,8 @@ instead for most things).
import datetime
from django.conf import settings
from src.typeclasses.typeclass import TypeClass
from src.players.models import PlayerDB
from src.typeclasses.models import TypeclassBase
from src.comms.models import ChannelDB
from src.utils import logger
__all__ = ("Player",)
@ -23,10 +24,12 @@ _CMDSET_PLAYER = settings.CMDSET_PLAYER
_CONNECT_CHANNEL = None
class Player(TypeClass):
class Player(PlayerDB):
"""
Base typeclass for all Players.
"""
__metaclass__ = TypeclassBase
def __init__(self, dbobj):
"""
This is the base Typeclass for all Players. Players represent

View file

@ -9,7 +9,7 @@ from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.task import LoopingCall
from django.conf import settings
from django.utils.translation import ugettext as _
from src.typeclasses.typeclass import TypeClass
from src.typeclasses.models import TypeclassBase
from src.scripts.models import ScriptDB
from src.comms import channelhandler
from src.utils import logger
@ -108,11 +108,12 @@ class ExtendedLoopingCall(LoopingCall):
#
# Base script, inherit from Script below instead.
#
class ScriptBase(TypeClass):
class ScriptBase(ScriptDB):
"""
Base class for scripts. Don't inherit from this, inherit
from the class 'Script' instead.
"""
__metaclass__ = TypeclassBase
# private methods
def __eq__(self, other):

View file

@ -13,45 +13,91 @@ _GA = object.__getattribute__
_Tag = None
#
# helper functions for the TypedObjectManager.
# Decorators
#
def returns_typeclass_list(method):
"""
Decorator: Changes return of the decorated method (which are
TypeClassed objects) into object_classes(s) instead. Will always
return a list (may be empty).
Decorator: Always returns a list, even
if it is empty.
"""
def func(self, *args, **kwargs):
"decorator. Returns a list."
self.__doc__ = method.__doc__
matches = make_iter(method(self, *args, **kwargs))
return [(hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj
for dbobj in make_iter(matches)]
return list(method(self, *args, **kwargs))
return update_wrapper(func, method)
def returns_typeclass(method):
"""
Decorator: Will always return a single typeclassed result or None.
Decorator: Returns a single match or None
"""
def func(self, *args, **kwargs):
"decorator. Returns result or None."
self.__doc__ = method.__doc__
matches = method(self, *args, **kwargs)
dbobj = matches and make_iter(matches)[0] or None
if dbobj:
return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj
return None
query = method(self, *args, **kwargs)
return list(query)[0] if query else None
return update_wrapper(func, method)
# Managers
class TypedObjectManager(idmapper.manager.SharedMemoryManager):
"""
Common ObjectManager for all dbobjects.
"""
# common methods for all typed managers. These are used
# in other methods. Returns querysets.
def get(self, **kwargs):
"""
Overload the standard get. This will limit itself to only
return the current typeclass.
"""
kwargs.update({"db_typeclass_path":self.model.path})
return super(TypedObjectManager, self).get(**kwargs)
def filter(self, **kwargs):
"""
Overload of the standard filter function. This filter will
limit itself to only the current typeclass.
"""
kwargs.update({"db_typeclass_path":self.model.path})
return super(TypedObjectManager, self).filter(**kwargs)
def all(self, **kwargs):
"""
Overload method to return all matches, filtering for typeclass
"""
return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path=self.model.path)
def get_inherit(self, **kwargs):
"""
Variation of get that not only returns the current
typeclass but also all subclasses of that typeclass.
"""
paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
for cls in self.model.__subclasses__()]
kwargs.update({"db_typeclass_path__in":paths})
return super(TypedObjectManager, self).get(**kwargs)
def filter_inherit(self, **kwargs):
"""
Variation of filter that allows results both from typeclass
and from subclasses of typeclass
"""
# query, including all subclasses
paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
for cls in self.model.__subclasses__()]
kwargs.update({"db_typeclass_path__in":paths})
return super(TypedObjectManager, self).filter(**kwargs)
def all_inherit(self, **kwargs):
"""
Return all matches, allowing matches from all subclasses of
the typeclass.
"""
paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
for cls in self.model.__subclasses__()]
return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path__in=paths)
# Attribute manager methods
def get_attribute(self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None):

View file

@ -30,6 +30,7 @@ import sys
import re
import traceback
import weakref
from importlib import import_module
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
@ -738,6 +739,45 @@ class PermissionHandler(TagHandler):
#
#------------------------------------------------------------
# imported for access by other
from src.utils.idmapper.base import SharedMemoryModelBase
class TypeclassModelBase(SharedMemoryModelBase):
"""
Metaclass for typeclasses
"""
def __init__(cls, *args, **kwargs):
"""
We must define our Typeclasses as proxies. We also store the path
directly on the class, this is useful for managers.
"""
super(TypeclassModelBase, cls).__init__(*args, **kwargs)
class Meta:
proxy = True
cls.Meta = Meta
cls.typename = cls.__name__
cls.path = "%s.%s" % (cls.__module__, cls.__name__)
class TypeclassBase(SharedMemoryModelBase):
"""
Metaclass which should be set for the root of model proxies
that don't define any new fields, like Object, Script etc.
"""
def __init__(cls, *args, **kwargs):
"""
We must define our Typeclasses as proxies. We also store the path
directly on the class, this is useful for managers.
"""
super(TypeclassBase, cls).__init__(*args, **kwargs)
class Meta:
# this is the important bit
proxy = True
cls.Meta = Meta
# convenience for manager methods
cls.typename = cls.__name__
cls.path = "%s.%s" % (cls.__module__, cls.__name__)
class TypedObject(SharedMemoryModel):
"""
@ -795,11 +835,23 @@ class TypedObject(SharedMemoryModel):
# quick on-object typeclass cache for speed
_cached_typeclass = None
# lock handler self.locks
# typeclass mechanism
def _import_class(self, path):
path, clsname = path.rsplit(".", 1)
mod = import_module(path)
return getattr(mod, clsname)
def __init__(self, *args, **kwargs):
"We must initialize the parent first - important!"
typeclass_path = kwargs.pop("typeclass", None)
super(TypedObject, self).__init__(*args, **kwargs)
_SA(self, "dbobj", self) # this allows for self-reference
if typeclass_path:
self.__class__ = self._import_class(typeclass_path)
self.db_typclass_path = typeclass_path
elif self.db_typeclass_path:
self.__class__ = self._import_class(self.db_typeclass_path)
else:
self.db_typeclass_path = "%s.%s" % (self.__module__, self.__class__.__name__)
# initialize all handlers in a lazy fashion
@lazy_property
@ -872,37 +924,6 @@ class TypedObject(SharedMemoryModel):
def __unicode__(self):
return u"%s" % _GA(self, "db_key")
def __getattribute__(self, propname):
"""
Will predominantly look for an attribute
on this object, but if not found we will
check if it might exist on the typeclass instead. Since
the typeclass refers back to the databaseobject as well, we
have to be very careful to avoid loops.
"""
try:
return _GA(self, propname)
except AttributeError:
if propname.startswith('_'):
# don't relay private/special varname lookups to the typeclass
raise AttributeError("private property %s not found on db model (typeclass not searched)." % propname)
# check if the attribute exists on the typeclass instead
# (we make sure to not incur a loop by not triggering the
# typeclass' __getattribute__, since that one would
# try to look back to this very database object.)
return _GA(_GA(self, 'typeclass'), propname)
def _hasattr(self, obj, attrname):
"""
Loop-safe version of hasattr, to avoid running a lookup that
will be rerouted up the typeclass. Returns True/False.
"""
try:
_GA(obj, attrname)
return True
except AttributeError:
return False
#@property
def __dbid_get(self):
"""
@ -936,208 +957,6 @@ class TypedObject(SharedMemoryModel):
raise Exception("dbref cannot be deleted!")
dbref = property(__dbref_get, __dbref_set, __dbref_del)
# the latest error string will be stored here for accessing methods to access.
# It is set by _display_errmsg, which will print to log if error happens
# during server startup.
typeclass_last_errmsg = ""
# typeclass property
#@property
def __typeclass_get(self):
"""
Getter. Allows for value = self.typeclass.
The typeclass is a class object found at self.typeclass_path;
it allows for extending the Typed object for all different
types of objects that the game needs. This property
handles loading and initialization of the typeclass on the fly.
Note: The liberal use of _GA and __setattr__ (instead
of normal dot notation) is due to optimization: it avoids calling
the custom self.__getattribute__ more than necessary.
"""
path = _GA(self, "typeclass_path")
typeclass = _GA(self, "_cached_typeclass")
try:
if typeclass and _GA(typeclass, "path") == path:
# don't call at_init() when returning from cache
return typeclass
except AttributeError:
pass
errstring = ""
if not path:
# this means we should get the default obj without giving errors.
return _GA(self, "_get_default_typeclass")(cache=True, silent=True, save=True)
else:
# handle loading/importing of typeclasses, searching all paths.
# (self._typeclass_paths is a shortcut to settings.TYPECLASS_*_PATHS
# where '*' is either OBJECT, SCRIPT or PLAYER depending on the
# typed entities).
typeclass_paths = [path] + ["%s.%s" % (prefix, path)
for prefix in _GA(self, '_typeclass_paths')]
for tpath in typeclass_paths:
# try to import and analyze the result
typeclass = _GA(self, "_path_import")(tpath)
if callable(typeclass):
# we succeeded to import. Cache and return.
_SA(self, "typeclass_path", tpath)
typeclass = typeclass(self)
_SA(self, "_cached_typeclass", typeclass)
try:
typeclass.at_init()
except AttributeError:
logger.log_trace("\n%s: Error initializing typeclass %s. Using default." % (self, tpath))
break
except Exception:
logger.log_trace()
return typeclass
elif hasattr(typeclass, '__file__'):
errstring += "\n%s seems to be just the path to a module. You need" % tpath
errstring += " to specify the actual typeclass name inside the module too."
elif typeclass:
errstring += "\n%s" % typeclass.strip() # this will hold a growing error message.
if not errstring:
errstring = "\nMake sure the path is set correctly. Paths tested:\n"
errstring += ", ".join(typeclass_paths)
errstring += "\nTypeclass code was not found or failed to load."
# If we reach this point we couldn't import any typeclasses. Return
# default. It's up to the calling method to use e.g. self.is_typeclass()
# to detect that the result is not the one asked for.
_GA(self, "_display_errmsg")(errstring.strip())
return _GA(self, "_get_default_typeclass")(cache=False, silent=False, save=False)
#@typeclass.deleter
def __typeclass_del(self):
"Deleter. Disallow 'del self.typeclass'"
raise Exception("The typeclass property should never be deleted, only changed in-place!")
# typeclass property
typeclass = property(__typeclass_get, fdel=__typeclass_del)
def _path_import(self, path):
"""
Import a class from a python path of the
form src.objects.object.Object
"""
errstring = ""
if not path:
# this needs not be bad, it just means
# we should use defaults.
return None
try:
modpath, class_name = path.rsplit('.', 1)
module = __import__(modpath, fromlist=["none"])
return module.__dict__[class_name]
except ImportError:
trc = sys.exc_traceback
if not trc.tb_next:
# we separate between not finding the module, and finding
# a buggy one.
pass
#errstring = "Typeclass not found trying path '%s'." % path
else:
# a bug in the module is reported normally.
trc = traceback.format_exc().strip()
errstring = "\n%sError importing '%s'." % (trc, path)
except (ValueError, TypeError):
errstring = "Malformed typeclass path '%s'." % path
except KeyError:
errstring = "No class '%s' was found in module '%s'."
errstring = errstring % (class_name, modpath)
except Exception:
trc = traceback.format_exc().strip()
errstring = "\n%sException importing '%s'." % (trc, path)
# return the error.
return errstring
def _display_errmsg(self, message):
"""
Helper function to display error.
"""
_SA(self, "typeclass_last_errmsg", message)
if ServerConfig.objects.conf("server_starting_mode"):
print message
else:
logger.log_errmsg(message)
return
def _get_default_typeclass(self, cache=False, silent=False, save=False):
"""
This is called when a typeclass fails to
load for whatever reason.
Overload this in different entities.
Default operation is to load a default typeclass.
"""
defpath = _GA(self, "_default_typeclass_path")
typeclass = _GA(self, "_path_import")(defpath)
# if not silent:
# #errstring = "\n\nUsing Default class '%s'." % defpath
# _GA(self, "_display_errmsg")(errstring)
if not callable(typeclass):
# if typeclass still doesn't exist at this point, we're in trouble.
# fall back to hardcoded core class which is wrong for e.g.
# scripts/players etc.
failpath = defpath
defpath = "src.objects.objects.Object"
typeclass = _GA(self, "_path_import")(defpath)
if not silent:
#errstring = " %s\n%s" % (typeclass, errstring)
errstring = " Default class '%s' failed to load." % failpath
errstring += "\n Using Evennia's default root '%s'." % defpath
_GA(self, "_display_errmsg")(errstring.strip())
if not callable(typeclass):
# if this is still giving an error, Evennia is wrongly
# configured or buggy
raise Exception("CRITICAL ERROR: The final fallback typeclass %s cannot load!!" % defpath)
typeclass = typeclass(self)
if save:
_SA(self, 'db_typeclass_path', defpath)
_GA(self, 'save')()
if cache:
_SA(self, "_cached_db_typeclass_path", defpath)
_SA(self, "_cached_typeclass", typeclass)
try:
typeclass.at_init()
except Exception:
logger.log_trace()
return typeclass
def is_typeclass(self, typeclass, exact=True):
"""
Returns true if this object has this type
OR has a typeclass which is an subclass of
the given typeclass. This operates on the actually
loaded typeclass (this is important since a failing
typeclass may instead have its default currently loaded)
typeclass - can be a class object or the
python path to such an object to match against.
exact - returns true only if the object's
type is exactly this typeclass, ignoring
parents.
"""
try:
typeclass = _GA(typeclass, "path")
except AttributeError:
pass
typeclasses = [typeclass] + ["%s.%s" % (path, typeclass)
for path in _GA(self, "_typeclass_paths")]
if exact:
current_path = _GA(self.typeclass, "path") #"_GA(self, "_cached_db_typeclass_path")
return typeclass and any((current_path == typec for typec in typeclasses))
else:
# check parent chain
return any((cls for cls in self.typeclass.__class__.mro()
if any(("%s.%s" % (_GA(cls, "__module__"),
_GA(cls, "__name__")) == typec
for typec in typeclasses))))
#
# Object manipulation methods
#
@ -1428,168 +1247,3 @@ class TypedObject(SharedMemoryModel):
raise Exception("Cannot delete the ndb object!")
ndb = property(__ndb_get, __ndb_set, __ndb_del)
# #
# # ***** DEPRECATED METHODS BELOW *******
# #
#
# #
# # Full attr_obj attributes. You usually access these
# # through the obj.db.attrname method.
#
# # Helper methods for attr_obj attributes
#
# def has_attribute(self, attribute_name):
# """
# See if we have an attribute set on the object.
#
# attribute_name: (str) The attribute's name.
# """
# logger.log_depmsg("obj.has_attribute() is deprecated. Use obj.attributes.has().")
# return _GA(self, "attributes").has(attribute_name)
#
# def set_attribute(self, attribute_name, new_value=None, lockstring=""):
# """
# Sets an attribute on an object. Creates the attribute if need
# be.
#
# attribute_name: (str) The attribute's name.
# new_value: (python obj) The value to set the attribute to. If this is not
# a str, the object will be stored as a pickle.
# lockstring - this sets an access restriction on the attribute object. Note that
# this is normally NOT checked - use the secureattr() access method
# below to perform access-checked modification of attributes. Lock
# types checked by secureattr are 'attrread','attredit','attrcreate'.
# """
# logger.log_depmsg("obj.set_attribute() is deprecated. Use obj.db.attr=value or obj.attributes.add().")
# _GA(self, "attributes").add(attribute_name, new_value, lockstring=lockstring)
#
# def get_attribute_obj(self, attribute_name, default=None):
# """
# Get the actual attribute object named attribute_name
# """
# logger.log_depmsg("obj.get_attribute_obj() is deprecated. Use obj.attributes.get(..., return_obj=True)")
# return _GA(self, "attributes").get(attribute_name, default=default, return_obj=True)
#
# def get_attribute(self, attribute_name, default=None, raise_exception=False):
# """
# Returns the value of an attribute on an object. You may need to
# type cast the returned value from this function since the attribute
# can be of any type. Returns default if no match is found.
#
# attribute_name: (str) The attribute's name.
# default: What to return if no attribute is found
# raise_exception (bool) - raise an exception if no object exists instead of returning default.
# """
# logger.log_depmsg("obj.get_attribute() is deprecated. Use obj.db.attr or obj.attributes.get().")
# return _GA(self, "attributes").get(attribute_name, default=default, raise_exception=raise_exception)
#
# def del_attribute(self, attribute_name, raise_exception=False):
# """
# Removes an attribute entirely.
#
# attribute_name: (str) The attribute's name.
# raise_exception (bool) - raise exception if attribute to delete
# could not be found
# """
# logger.log_depmsg("obj.del_attribute() is deprecated. Use del obj.db.attr or obj.attributes.remove().")
# _GA(self, "attributes").remove(attribute_name, raise_exception=raise_exception)
#
# def get_all_attributes(self):
# """
# Returns all attributes defined on the object.
# """
# logger.log_depmsg("obj.get_all_attributes() is deprecated. Use obj.db.all() or obj.attributes.all().")
# return _GA(self, "attributes").all()
#
# def attr(self, attribute_name=None, value=None, delete=False):
# """
# This is a convenient wrapper for
# get_attribute, set_attribute, del_attribute
# and get_all_attributes.
# If value is None, attr will act like
# a getter, otherwise as a setter.
# set delete=True to delete the named attribute.
#
# Note that you cannot set the attribute
# value to None using this method. Use set_attribute.
# """
# logger.log_depmsg("obj.attr() is deprecated. Use handlers obj.db or obj.attributes.")
# if attribute_name is None:
# # act as a list method
# return _GA(self, "attributes").all()
# elif delete is True:
# _GA(self, "attributes").remove(attribute_name)
# elif value is None:
# # act as a getter.
# return _GA(self, "attributes").get(attribute_name)
# else:
# # act as a setter
# self._GA(self, "attributes").add(attribute_name, value)
#
# def secure_attr(self, accessing_object, attribute_name=None, value=None, delete=False,
# default_access_read=True, default_access_edit=True, default_access_create=True):
# """
# This is a version of attr that requires the accessing object
# as input and will use that to check eventual access locks on
# the Attribute before allowing any changes or reads.
#
# In the cases when this method wouldn't return, it will return
# True for a successful operation, None otherwise.
#
# locktypes checked on the Attribute itself:
# attrread - control access to reading the attribute value
# attredit - control edit/delete access
# locktype checked on the object on which the Attribute is/will be stored:
# attrcreate - control attribute create access (this is checked *on the object* not on the Attribute!)
#
# default_access_* defines which access is assumed if no
# suitable lock is defined on the Atttribute.
#
# """
# logger.log_depmsg("obj.secure_attr() is deprecated. Use obj.attributes methods, giving accessing_obj keyword.")
# if attribute_name is None:
# return _GA(self, "attributes").all(accessing_obj=accessing_object, default_access=default_access_read)
# elif delete is True:
# # act as deleter
# _GA(self, "attributes").remove(attribute_name, accessing_obj=accessing_object, default_access=default_access_edit)
# elif value is None:
# # act as getter
# return _GA(self, "attributes").get(attribute_name, accessing_obj=accessing_object, default_access=default_access_read)
# else:
# # act as setter
# attr = _GA(self, "attributes").get(attribute_name, return_obj=True)
# if attr:
# # attribute already exists
# _GA(self, "attributes").add(attribute_name, value, accessing_obj=accessing_object, default_access=default_access_edit)
# else:
# # creating a new attribute - check access on storing object!
# _GA(self, "attributes").add(attribute_name, value, accessing_obj=accessing_object, default_access=default_access_create)
#
# def nattr(self, attribute_name=None, value=None, delete=False):
# """
# This allows for assigning non-persistent data on the object using
# a method call. Will return None if trying to access a non-existing property.
# """
# logger.log_depmsg("obj.nattr() is deprecated. Use obj.nattributes instead.")
# if attribute_name is None:
# # act as a list method
# if callable(self.ndb.all):
# return self.ndb.all()
# else:
# return [val for val in self.ndb.__dict__.keys()
# if not val.startswith['_']]
# elif delete is True:
# if hasattr(self.ndb, attribute_name):
# _DA(_GA(self, "ndb"), attribute_name)
# elif value is None:
# # act as a getter.
# if hasattr(self.ndb, attribute_name):
# _GA(_GA(self, "ndb"), attribute_name)
# else:
# return None
# else:
# # act as a setter
# _SA(self.ndb, attribute_name, value)
#
#

View file

@ -1,190 +0,0 @@
"""
A typeclass is the companion of a TypedObject django model.
It 'decorates' the model without actually having to add new
fields to the model - transparently storing data onto its
associated model without the admin/user just having to deal
with a 'normal' Python class. The only restrictions is that
the typeclass must inherit from TypeClass and not reimplement
the get/setters defined below. There are also a few properties
that are protected, so as to not overwrite property names
used by the typesystem or django itself.
"""
from src.utils.logger import log_trace, log_errmsg
__all__ = ("TypeClass",)
# these are called so many times it's worth to avoid lookup calls
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
# To ensure the sanity of the model, there are a
# few property names we won't allow the admin to
# set on the typeclass just like that. Note that these are *not* related
# to *in-game* safety (if you can edit typeclasses you have
# full access anyway), so no protection against changing
# e.g. 'locks' or 'permissions' should go here.
PROTECTED = ('id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'db_player',
'attr', 'save', 'delete', 'db_model_name','attribute_class',
'typeclass_paths')
# If this is true, all non-protected property assignments
# are directly stored to a database attribute
class MetaTypeClass(type):
"""
This metaclass just makes sure the class object gets
printed in a nicer way (it might end up having no name at all
otherwise due to the magics being done with get/setattribute).
"""
def __init__(mcs, *args, **kwargs):
"""
Adds some features to typeclassed objects
"""
super(MetaTypeClass, mcs).__init__(*args, **kwargs)
mcs.typename = mcs.__name__
mcs.path = "%s.%s" % (mcs.__module__, mcs.__name__)
def __str__(cls):
return "%s" % cls.__name__
class TypeClass(object):
"""
This class implements a 'typeclass' object. This is connected
to a database object inheriting from TypedObject.
the TypeClass allows for all customization.
Most of the time this means that the admin never has to
worry about database access but only deal with extending
TypeClasses to create diverse objects in the game.
The ObjectType class has all functionality for wrapping a
database object transparently.
It's up to its child classes to implement eventual custom hooks
and other functions called by the engine.
"""
__metaclass__ = MetaTypeClass
def __init__(self, dbobj):
"""
Initialize the object class. There are two ways to call this class.
o = object_class(dbobj) : this is used to initialize dbobj with the
class name
o = dbobj.object_class(dbobj) : this is used when dbobj.object_class
is already set.
"""
# typecheck of dbobj - we can't allow it to be added here
# unless it's really a TypedObject.
dbobj_cls = _GA(dbobj, '__class__')
dbobj_mro = _GA(dbobj_cls, '__mro__')
if not any('src.typeclasses.models.TypedObject' in str(mro) for mro in dbobj_mro):
raise Exception("dbobj is not a TypedObject: %s: %s" % (dbobj_cls, dbobj_mro))
# we should always be able to use dbobj/typeclass to get back an object of the desired type
_SA(self, 'dbobj', dbobj)
_SA(self, 'typeclass', self)
def __getattribute__(self, propname):
"""
Change the normal property access to
transparently include the properties on
self.dbobj. Note that dbobj properties have
priority, so if you define a same-named
property on the class, it will NOT be
accessible through getattr.
"""
if propname.startswith('__') and propname.endswith('__'):
# python specials are parsed as-is (otherwise things like
# isinstance() fail to identify the typeclass)
return _GA(self, propname)
#print "get %s (dbobj:%s)" % (propname, type(dbobj))
try:
return _GA(self, propname)
except AttributeError:
try:
dbobj = _GA(self, 'dbobj')
except AttributeError:
log_trace("Typeclass CRITICAL ERROR! dbobj not found for Typeclass %s!" % self)
raise
try:
return _GA(dbobj, propname)
except AttributeError:
string = "Object: '%s' not found on %s(#%s), nor on its typeclass %s."
raise AttributeError(string % (propname, dbobj, _GA(dbobj, "dbid"), _GA(dbobj, "typeclass_path")))
def __setattr__(self, propname, value):
"""
Transparently save data. Use property on Typeclass only if
that property is already defined, otherwise relegate to the
dbobj object in all situations. Note that this does not
necessarily mean storing it to the database.
"""
#print "set %s -> %s" % (propname, value)
if propname in PROTECTED:
string = "%s: '%s' is a protected attribute name."
string += " (protected: [%s])" % (", ".join(PROTECTED))
log_errmsg(string % (self.name, propname))
return
try:
_GA(self, propname)
_SA(self, propname, value)
except AttributeError:
try:
dbobj = _GA(self, 'dbobj')
except AttributeError:
dbobj = None
if dbobj:
_SA(dbobj, propname, value)
else:
# only as a last resort do we save on the typeclass object
_SA(self, propname, value)
def __eq__(self, other):
"""
dbobj-recognized comparison
"""
try:
return _GA(_GA(self, "dbobj"), "dbid") == _GA(_GA(other, "dbobj"), "dbid")
except AttributeError:
return id(self) == id(other)
def __delattr__(self, propname):
"""
Transparently deletes data from the typeclass or dbobj by first
searching on the typeclass, secondly on the dbobj.db.
Will not allow deletion of properties stored directly on dbobj.
"""
if propname in PROTECTED:
string = "%s: '%s' is a protected attribute name."
string += " (protected: [%s])" % (", ".join(PROTECTED))
log_errmsg(string % (self.name, propname))
return
try:
_DA(self, propname)
except AttributeError:
# not on typeclass, try to delete on db/ndb
try:
dbobj = _GA(self, 'dbobj')
except AttributeError:
log_trace("This is probably due to an unsafe reload.")
return # ignore delete
try:
dbobj.del_attribute(propname, raise_exception=True)
except AttributeError:
string = "Object: '%s' not found on %s(#%s), nor on its typeclass %s."
raise AttributeError(string % (propname, dbobj,
dbobj.dbid,
dbobj.typeclass_path,))
def __str__(self):
"represent the object"
return self.key
def __unicode__(self):
return u"%s" % self.key

View file

@ -22,6 +22,21 @@ from manager import SharedMemoryManager
AUTO_FLUSH_MIN_INTERVAL = 60.0 * 5 # at least 5 mins between cache flushes
# django patch imports
import copy
import sys
from django.apps import apps
from django.db.models.base import subclass_exception
import warnings
from django.db.models.options import Options
from django.utils.deprecation import RemovedInDjango19Warning
from django.core.exceptions import (ObjectDoesNotExist,
MultipleObjectsReturned, FieldError)
from django.apps.config import MODELS_MODULE_NAME
from django.db.models.fields.related import OneToOneField
#/ django patch imports
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
@ -72,7 +87,7 @@ class SharedMemoryModelBase(ModelBase):
cls._idmapper_recache_protection = False
super(SharedMemoryModelBase, cls)._prepare()
def __new__(cls, classname, bases, classdict, *args, **kwargs):
def __new__(cls, name, bases, attrs):
"""
Field shortcut creation:
Takes field names db_* and creates property wrappers named without the db_ prefix. So db_key -> key
@ -158,23 +173,273 @@ class SharedMemoryModelBase(ModelBase):
fset = lambda cls, val: _set(cls, fieldname, val)
fdel = lambda cls: _del(cls, fieldname) if editable else _del_nonedit(cls,fieldname)
# assigning
classdict[wrappername] = property(fget, fset, fdel)
attrs[wrappername] = property(fget, fset, fdel)
#type(cls).__setattr__(cls, wrappername, property(fget, fset, fdel))#, doc))
# exclude some models that should not auto-create wrapper fields
if cls.__name__ in ("ServerConfig", "TypeNick"):
return
# dynamically create the wrapper properties for all fields not already handled (manytomanyfields are always handlers)
for fieldname, field in ((fname, field) for fname, field in classdict.items()
for fieldname, field in ((fname, field) for fname, field in attrs.items()
if fname.startswith("db_") and type(field).__name__ != "ManyToManyField"):
foreignkey = type(field).__name__ == "ForeignKey"
#print fieldname, type(field).__name__, field
wrappername = "dbid" if fieldname == "id" else fieldname.replace("db_", "", 1)
if wrappername not in classdict:
if wrappername not in attrs:
# makes sure not to overload manually created wrappers on the model
#print "wrapping %s -> %s" % (fieldname, wrappername)
create_wrapper(cls, fieldname, wrappername, editable=field.editable, foreignkey=foreignkey)
return super(SharedMemoryModelBase, cls).__new__(cls, classname, bases, classdict, *args, **kwargs)
# django patch
# Evennia mod, based on Django Ticket #11560: https://code.djangoproject.com/ticket/11560
# The actual patch is small and further down.
super_new = super(ModelBase, cls).__new__
# Also ensure initialization is only performed for subclasses of Model
# (excluding Model class itself).
parents = [b for b in bases if isinstance(b, ModelBase)]
if not parents:
return super_new(cls, name, bases, attrs)
# Create the class.
module = attrs.pop('__module__')
new_class = super_new(cls, name, bases, {'__module__': module})
attr_meta = attrs.pop('Meta', None)
abstract = getattr(attr_meta, 'abstract', False)
if not attr_meta:
meta = getattr(new_class, 'Meta', None)
else:
meta = attr_meta
base_meta = getattr(new_class, '_meta', None)
# Look for an application configuration to attach the model to.
app_config = apps.get_containing_app_config(module)
if getattr(meta, 'app_label', None) is None:
if app_config is None:
# If the model is imported before the configuration for its
# application is created (#21719), or isn't in an installed
# application (#21680), use the legacy logic to figure out the
# app_label by looking one level up from the package or module
# named 'models'. If no such package or module exists, fall
# back to looking one level up from the module this model is
# defined in.
# For 'django.contrib.sites.models', this would be 'sites'.
# For 'geo.models.places' this would be 'geo'.
msg = (
"Model class %s.%s doesn't declare an explicit app_label "
"and either isn't in an application in INSTALLED_APPS or "
"else was imported before its application was loaded. " %
(module, name))
if abstract:
msg += "Its app_label will be set to None in Django 1.9."
else:
msg += "This will no longer be supported in Django 1.9."
warnings.warn(msg, RemovedInDjango19Warning, stacklevel=2)
model_module = sys.modules[new_class.__module__]
package_components = model_module.__name__.split('.')
package_components.reverse() # find the last occurrence of 'models'
try:
app_label_index = package_components.index(MODELS_MODULE_NAME) + 1
except ValueError:
app_label_index = 1
kwargs = {"app_label": package_components[app_label_index]}
else:
kwargs = {"app_label": app_config.label}
else:
kwargs = {}
new_class.add_to_class('_meta', Options(meta, **kwargs))
if not abstract:
new_class.add_to_class(
'DoesNotExist',
subclass_exception(
str('DoesNotExist'),
tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (ObjectDoesNotExist,),
module,
attached_to=new_class))
new_class.add_to_class(
'MultipleObjectsReturned',
subclass_exception(
str('MultipleObjectsReturned'),
tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (MultipleObjectsReturned,),
module,
attached_to=new_class))
if base_meta and not base_meta.abstract:
# Non-abstract child classes inherit some attributes from their
# non-abstract parent (unless an ABC comes before it in the
# method resolution order).
if not hasattr(meta, 'ordering'):
new_class._meta.ordering = base_meta.ordering
if not hasattr(meta, 'get_latest_by'):
new_class._meta.get_latest_by = base_meta.get_latest_by
is_proxy = new_class._meta.proxy
# If the model is a proxy, ensure that the base class
# hasn't been swapped out.
if is_proxy and base_meta and base_meta.swapped:
raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped))
if getattr(new_class, '_default_manager', None):
if not is_proxy:
# Multi-table inheritance doesn't inherit default manager from
# parents.
new_class._default_manager = None
new_class._base_manager = None
else:
# Proxy classes do inherit parent's default manager, if none is
# set explicitly.
new_class._default_manager = new_class._default_manager._copy_to_model(new_class)
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
# Add all attributes to the class.
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
# All the fields of any type declared on this model
new_fields = (
new_class._meta.local_fields +
new_class._meta.local_many_to_many +
new_class._meta.virtual_fields
)
field_names = set(f.name for f in new_fields)
# Basic setup for proxy models.
if is_proxy:
base = None
for parent in [kls for kls in parents if hasattr(kls, '_meta')]:
if parent._meta.abstract:
if parent._meta.fields:
raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name)
else:
continue
# Evennia mod, based on Django Ticket #11560: https://code.djangoproject.com/ticket/11560
# This allows multiple inheritance for proxy models
while parent._meta.proxy:
parent = parent._meta.proxy_for_model
if base is not None and base is not parent:
#if base is not None:
raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name)
else:
base = parent
if base is None:
raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
new_class._meta.setup_proxy(base)
new_class._meta.concrete_model = base._meta.concrete_model
else:
new_class._meta.concrete_model = new_class
# Collect the parent links for multi-table inheritance.
parent_links = {}
for base in reversed([new_class] + parents):
# Conceptually equivalent to `if base is Model`.
if not hasattr(base, '_meta'):
continue
# Skip concrete parent classes.
if base != new_class and not base._meta.abstract:
continue
# Locate OneToOneField instances.
for field in base._meta.local_fields:
if isinstance(field, OneToOneField):
parent_links[field.rel.to] = field
# Do the appropriate setup for any model parents.
for base in parents:
original_base = base
if not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
continue
parent_fields = base._meta.local_fields + base._meta.local_many_to_many
# Check for clashes between locally declared fields and those
# on the base classes (we cannot handle shadowed fields at the
# moment).
for field in parent_fields:
if field.name in field_names:
raise FieldError(
'Local field %r in class %r clashes '
'with field of similar name from '
'base class %r' % (field.name, name, base.__name__)
)
if not base._meta.abstract:
# Concrete classes...
base = base._meta.concrete_model
if base in parent_links:
field = parent_links[base]
elif not is_proxy:
attr_name = '%s_ptr' % base._meta.model_name
field = OneToOneField(base, name=attr_name,
auto_created=True, parent_link=True)
# Only add the ptr field if it's not already present;
# e.g. migrations will already have it specified
if not hasattr(new_class, attr_name):
new_class.add_to_class(attr_name, field)
else:
field = None
new_class._meta.parents[base] = field
else:
# .. and abstract ones.
for field in parent_fields:
new_class.add_to_class(field.name, copy.deepcopy(field))
# Pass any non-abstract parent classes onto child.
new_class._meta.parents.update(base._meta.parents)
# Inherit managers from the abstract base classes.
new_class.copy_managers(base._meta.abstract_managers)
# Proxy models inherit the non-abstract managers from their base,
# unless they have redefined any of them.
if is_proxy:
new_class.copy_managers(original_base._meta.concrete_managers)
# Inherit virtual fields (like GenericForeignKey) from the parent
# class
for field in base._meta.virtual_fields:
if base._meta.abstract and field.name in field_names:
raise FieldError(
'Local field %r in class %r clashes '
'with field of similar name from '
'abstract base class %r' % (field.name, name, base.__name__)
)
new_class.add_to_class(field.name, copy.deepcopy(field))
if abstract:
# Abstract base models can't be instantiated and don't appear in
# the list of models for an app. We do the final setup for them a
# little differently from normal models.
attr_meta.abstract = False
new_class.Meta = attr_meta
return new_class
new_class._prepare()
new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
return new_class
class TypeclassModelBase(SharedMemoryModelBase):
"""
Metaclass for typeclasses
"""
def __init__(cls, *args, **kwargs):
"""
We must define our Typeclasses as proxies. We also store the path
directly on the class, this is useful for managers.
"""
super(TypeclassModelBase, cls).__init__(*args, **kwargs)
class Meta:
proxy = True
cls.Meta = Meta
cls.typename = cls.__name__
cls.path = "%s.%s" % (cls.__module__, cls.__name__)
class SharedMemoryModel(Model):