From 236c0d17d34e067dd04b9731ddd24208b52234a3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 19 Dec 2014 16:29:41 +0100 Subject: [PATCH] First non-tested version of moving typeclasses to proxy models. --- src/comms/comms.py | 7 +- src/objects/models.py | 1 - src/objects/objects.py | 6 +- src/players/player.py | 7 +- src/scripts/scripts.py | 5 +- src/typeclasses/managers.py | 78 ++++-- src/typeclasses/models.py | 456 +++++------------------------------ src/typeclasses/typeclass.py | 190 --------------- src/utils/idmapper/base.py | 275 ++++++++++++++++++++- 9 files changed, 403 insertions(+), 622 deletions(-) delete mode 100644 src/typeclasses/typeclass.py diff --git a/src/comms/comms.py b/src/comms/comms.py index 5e0aead3eb..c7284b7401 100644 --- a/src/comms/comms.py +++ b/src/comms/comms.py @@ -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 diff --git a/src/objects/models.py b/src/objects/models.py index b5e1302b77..2794a0cf20 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -134,7 +134,6 @@ class ObjectDB(TypedObject): contents - other objects having this object as location exits - exits from this object """ - # # ObjectDB Database model setup # diff --git a/src/objects/objects.py b/src/objects/objects.py index 56c50a3a9e..990c83d109 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -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): """ diff --git a/src/players/player.py b/src/players/player.py index 4ce3c37be5..30e566aeec 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -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 diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index f7749ffa12..78e79bf81a 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -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): diff --git a/src/typeclasses/managers.py b/src/typeclasses/managers.py index c9f62f95bb..c59b4d2d3b 100644 --- a/src/typeclasses/managers.py +++ b/src/typeclasses/managers.py @@ -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): diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 7757bceda3..6ff1050cc0 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -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) -# -# - diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py deleted file mode 100644 index a8fe2a02db..0000000000 --- a/src/typeclasses/typeclass.py +++ /dev/null @@ -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 diff --git a/src/utils/idmapper/base.py b/src/utils/idmapper/base.py index 122c5b7d2f..5922384ee8 100755 --- a/src/utils/idmapper/base.py +++ b/src/utils/idmapper/base.py @@ -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):