From 043ebf72131bebc6227b3428b4203f905d0c6098 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 20 Dec 2014 19:04:49 +0100 Subject: [PATCH] Fixed metaclass to handle proxy correctly. Some issues with getting path properties set correctly. --- src/comms/comms.py | 3 +- src/objects/objects.py | 4 +- src/players/player.py | 3 +- src/scripts/scripts.py | 3 +- src/typeclasses/models.py | 298 ++++++++++++++++++++++++++++++++++--- src/utils/idmapper/base.py | 243 +----------------------------- 6 files changed, 289 insertions(+), 265 deletions(-) diff --git a/src/comms/comms.py b/src/comms/comms.py index 2c878058bb..8bfdb35dc1 100644 --- a/src/comms/comms.py +++ b/src/comms/comms.py @@ -3,6 +3,7 @@ Default Typeclass for Comms. See objects.objects for more information on Typeclassing. """ +from src.typeclasses.models import TypeclassBase from src.comms.models import Msg, TempMsg, ChannelDB from src.utils import logger from src.utils.utils import make_iter @@ -13,7 +14,7 @@ class Channel(ChannelDB): This is the base class for all Comms. Inherit from this to create different types of communication channels. """ - _is_typeclass = True + __metaclass__ = TypeclassBase # helper methods, for easy overloading diff --git a/src/objects/objects.py b/src/objects/objects.py index a989bb0af1..3c41841559 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -16,6 +16,7 @@ they control by simply linking to a new object's user property. """ from django.conf import settings +from src.typeclasses.models import TypeclassBase from src.objects.models import ObjectDB from src.commands import cmdset, command from src.utils.logger import log_depmsg @@ -35,8 +36,9 @@ 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. + """ - _is_typeclass = True + __metaclass__ = TypeclassBase # __init__ is only defined here in order to present docstring to API. def __init__(self): diff --git a/src/players/player.py b/src/players/player.py index 8a7a4761d7..d799810713 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -13,6 +13,7 @@ instead for most things). import datetime from django.conf import settings +from src.typeclasses.models import TypeclassBase from src.players.models import PlayerDB from src.comms.models import ChannelDB from src.utils import logger @@ -26,7 +27,7 @@ class Player(PlayerDB): """ Base typeclass for all Players. """ - _is_typeclass = True + __metaclass__ = TypeclassBase def __init__(self): """ diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index a301a7f647..9454e80922 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -8,6 +8,7 @@ It also defines a few common scripts. from twisted.internet.defer import Deferred, maybeDeferred from twisted.internet.task import LoopingCall from django.conf import settings +from src.typeclasses.models import TypeclassBase from django.utils.translation import ugettext as _ from src.scripts.models import ScriptDB from src.comms import channelhandler @@ -112,7 +113,7 @@ class ScriptBase(ScriptDB): Base class for scripts. Don't inherit from this, inherit from the class 'Script' instead. """ - _is_typeclass = True + __metaclass__ = TypeclassBase def __eq__(self, other): """ diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 4461c847f5..19edcd3d85 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -46,6 +46,7 @@ from src.server.models import ServerConfig from src.typeclasses import managers from src.locks.lockhandler import LockHandler from src.utils import logger +from django.db.models.base import ModelBase from src.utils.utils import ( make_iter, is_iter, to_str, inherits_from, lazy_property) from src.utils.dbserialize import to_pickle, from_pickle @@ -739,26 +740,285 @@ class PermissionHandler(TagHandler): # #------------------------------------------------------------ -# imported for access by other -#from src.utils.idmapper.base import SharedMemoryModelBase -#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 __new__(cls, name, bases, attrs): -# """ -# We must define our Typeclasses as proxies. We also store the path -# directly on the class, this is useful for managers. -# """ -# if hasattr(cls, "Meta"): -# cls.Meta.proxy = True -# else: -# class Meta: -# proxy = True -# cls.Meta = Meta -# return super(TypeclassBase, cls).__new__(name, bases, attrs) +# +# Meta class for typeclasses +# + +# django patch imports +import copy +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 MultipleObjectsReturned +from django.apps.config import MODELS_MODULE_NAME +from django.db.models.fields.related import OneToOneField +#/ django patch imports + +from src.utils.idmapper.base import SharedMemoryModelBase + +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 __new__(cls, name, bases, attrs): + """ + We must define our Typeclasses as proxies. We also store the path + directly on the class, this is useful for managers. + """ + # typeclass proxy setup + if "Meta" in attrs: + attrs["Meta"].proxy = True + else: + class Meta: + proxy = True + attrs["Meta"] = Meta + + + # patch start - django multi-inheritance + # this is a copy of django.db.models.base.__new__ + # with a few lines (marked patch below) changed + # as per https://code.djangoproject.com/ticket/11560 + + 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 + #if base is not None: # patch + while parent._meta.proxy: # patch + parent = parent._meta.proxy_for_model # patch + if base is not None and base is not parent: # patch + 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 + + # /patch end + + + +# +# Main TypedObject abstraction +# class TypedObject(SharedMemoryModel): diff --git a/src/utils/idmapper/base.py b/src/utils/idmapper/base.py index 47788059bc..47b6c12f5e 100755 --- a/src/utils/idmapper/base.py +++ b/src/utils/idmapper/base.py @@ -95,14 +95,6 @@ class SharedMemoryModelBase(ModelBase): document this auto-wrapping in the class header, this could seem very much like magic to the user otherwise. """ # set up the typeclass handling only if a variable _is_typeclass is set on the class - if "_is_typeclass" in attrs: - if "Meta" in attrs: - attrs["Meta"].proxy = True - else: - class Meta: - proxy = True - attrs["Meta"] = Meta - def create_wrapper(cls, fieldname, wrappername, editable=True, foreignkey=False): "Helper method to create property wrappers with unique names (must be in separate call)" def _get(cls, fname): @@ -197,240 +189,7 @@ class SharedMemoryModelBase(ModelBase): #print "wrapping %s -> %s" % (fieldname, wrappername) create_wrapper(cls, fieldname, wrappername, editable=field.editable, foreignkey=foreignkey) - # patch start - - 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 - 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: # patch - while parent._meta.proxy: # patch - parent = parent._meta.proxy_for_model # patch - if base is not None and base is not parent: # patch - 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 - - - # /patch end - #return super(SharedMemoryModelBase, cls).__new__(cls, name, bases, attrs, *args, **kwargs) + return super(SharedMemoryModelBase, cls).__new__(cls, name, bases, attrs) class SharedMemoryModel(Model):