Skip to content
Browse files

removed __getattribute__ hack from PolymorphicModel.

A somewhat cleaner solution is now used (through __init__) which
also completely removes the performance impact of __getattribute__.
  • Loading branch information...
1 parent a87481b commit 6628145af7e894f6977e3a14a713ac14449f3a4e @bconstantin committed
Showing with 93 additions and 58 deletions.
  1. +1 −0 .gitignore
  2. +10 −11 pexp/management/commands/pcmd.py
  3. +7 −0 pexp/models.py
  4. +4 −2 polymorphic/base.py
  5. +69 −43 polymorphic/polymorphic_model.py
  6. +2 −2 polymorphic/tests.py
View
1 .gitignore
@@ -17,6 +17,7 @@ pbackup
mcmd.py
dbconfig_local.py
diffmanagement
+scrapbook.py
pip-log.txt
build
View
21 pexp/management/commands/pcmd.py
@@ -23,19 +23,18 @@ def handle_noargs(self, **options):
print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH
print
+ Project.objects.all().delete()
+ a=Project.objects.create(topic="John's gathering")
+ b=ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner")
+ c=ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
+ print Project.objects.all()
+ print
+
"""
ModelA.objects.all().delete()
- o=ModelA.objects.create(field1='A1')
- o=ModelB.objects.create(field1='B1', field2='B2')
- o=ModelC.objects.create(field1='C1', field2='C2', field3='C3')
+ a=ModelA.objects.create(field1='A1')
+ b=ModelB.objects.create(field1='B1', field2='B2')
+ c=ModelC.objects.create(field1='C1', field2='C2', field3='C3')
print ModelA.objects.all()
print
"""
-
- Project.objects.all().delete()
- o=Project.objects.create(topic="John's gathering")
- o=ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner")
- o=ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
- print Project.objects.all()
- print
-
View
7 pexp/models.py
@@ -20,6 +20,13 @@ class ModelB(ModelA):
class ModelC(ModelB):
field3 = models.CharField(max_length=10)
+class nModelA(models.Model):
+ field1 = models.CharField(max_length=10)
+class nModelB(nModelA):
+ field2 = models.CharField(max_length=10)
+class nModelC(nModelB):
+ field3 = models.CharField(max_length=10)
+
# for Django 1.2+, test models with same names in different apps
# (the other models with identical names are in polymorphic/tests.py)
View
6 polymorphic/base.py
@@ -73,6 +73,9 @@ def __new__(self, model_name, bases, attrs):
# validate resulting default manager
self.validate_model_manager(new_class._default_manager, model_name, '_default_manager')
+ # for __init__ function of this class (monkeypatching inheritance accessors)
+ new_class.polymorphic_super_sub_accessors_replaced = False
+
return new_class
def get_inherited_managers(self, attrs):
@@ -153,6 +156,7 @@ def validate_model_manager(self, manager, model_name, manager_name):
raise AssertionError(e)
return manager
+
# hack: a small patch to Django would be a better solution.
# Django's management command 'dumpdata' relies on non-polymorphic
# behaviour of the _default_manager. Therefore, we catch any access to _default_manager
@@ -174,5 +178,3 @@ def __getattribute__(self, name):
return super(PolymorphicModelBase, self).__getattribute__(name)
-
-
View
112 polymorphic/polymorphic_model.py
@@ -113,49 +113,75 @@ def get_real_instance(self):
real_model = self.get_real_instance_class()
if real_model == self.__class__: return self
return real_model.objects.get(pk=self.pk)
-
- # hack: a small patch to Django would be a better solution.
- # For base model back reference fields (like basemodel_ptr),
- # Django definitely must =not= use our polymorphic manager/queryset.
- # For now, we catch objects attribute access here and handle back reference fields manually.
- # This problem is triggered by delete(), like here:
- # django.db.models.base._collect_sub_objects: parent_obj = getattr(self, link.name)
- # TODO: investigate Django how this can be avoided
- def __getattribute__(self, name):
- if not name.startswith('__'): # do not intercept __class__ etc.
-
- # for efficiency: create a dict containing all model attribute names we need to intercept
- # (do this only once and store the result into self.__class__.inheritance_relation_fields_dict)
- if not self.__class__.__dict__.get('inheritance_relation_fields_dict', None):
-
- def add_if_regular_sub_or_super_class(model, as_ptr, result):
- if ( issubclass(model, models.Model) and model != models.Model
- and model != self.__class__ and model != PolymorphicModel):
- name = model.__name__.lower()
- if as_ptr: name+='_ptr'
- result[name] = model
-
- def add_all_base_models(model, result):
- add_if_regular_sub_or_super_class(model, True, result)
- for b in model.__bases__:
- add_all_base_models(b, result)
-
- def add_sub_models(model, result):
- for b in model.__subclasses__():
- add_if_regular_sub_or_super_class(b, False, result)
-
- result = {}
- add_all_base_models(self.__class__,result)
- add_sub_models(self.__class__,result)
- #print '##',self.__class__.__name__,' - ',result
- self.__class__.inheritance_relation_fields_dict = result
-
- model = self.__class__.inheritance_relation_fields_dict.get(name, None)
- if model:
- id = super(PolymorphicModel, self).__getattribute__('id')
- attr = model.base_objects.get(id=id)
- #print '---',self.__class__.__name__,name
+
+
+ def __init__(self, * args, ** kwargs):
+ """Replace Django's inheritance accessors member functions for our model
+ (self.__class__) with our own versions.
+ We monkey patch them until a patch can be added to Django
+ (which would probably be very small and make all of this obsolete).
+
+ If we have inheritance of the form ModelA -> ModelB ->ModelC then
+ Django creates accessors like this:
+ - ModelA: modelb
+ - ModelB: modela_ptr, modelb, modelc
+ - ModelC: modela_ptr, modelb, modelb_ptr, modelc
+
+ These accessors allow Django (and everyone else) to travel up and down
+ the inheritance tree for the db object at hand.
+
+ The original Django accessors use our polymorphic manager.
+ But they should not. So we replace them with our own accessors that use
+ our appropriate base_objects manager.
+ """
+ super(PolymorphicModel, self).__init__(*args, ** kwargs)
+ if self.__class__.polymorphic_super_sub_accessors_replaced: return
+ self.__class__.polymorphic_super_sub_accessors_replaced = True
+
+ def create_accessor_function_for_model(model):
+ def accessor_function(self):
+ attr = model.base_objects.get(pk=self.pk)
return attr
+ return accessor_function
+
+ subclasses_and_superclasses_accessors = self.get_inheritance_relation_fields_and_models()
+ #print '###',self.__class__.__name__,subclasses_and_superclasses_accessors
+
+ from django.db.models.fields.related import SingleRelatedObjectDescriptor, ReverseSingleRelatedObjectDescriptor
+
+ for name,model in subclasses_and_superclasses_accessors.iteritems():
+ orig_accessor = getattr(self.__class__, name, None)
+ if type(orig_accessor) in [SingleRelatedObjectDescriptor,ReverseSingleRelatedObjectDescriptor]:
+ #print 'replacing',name, orig_accessor
+ setattr(self.__class__, name, property(create_accessor_function_for_model(model)) )
+
+ def get_inheritance_relation_fields_and_models(self):
+ """helper function for __init__:
+ determine names of all Django inheritance accessor member functions for type(self)"""
+
+ def add_model(model, as_ptr, result):
+ name = model.__name__.lower()
+ if as_ptr: name+='_ptr'
+ result[name] = model
+
+ def add_model_if_regular(model, as_ptr, result):
+ if ( issubclass(model, models.Model) and model != models.Model
+ and model != self.__class__
+ and model != PolymorphicModel ):
+ add_model(model,as_ptr,result)
+
+ def add_all_super_models(model, result):
+ add_model_if_regular(model, True, result)
+ for b in model.__bases__:
+ add_all_super_models(b, result)
+
+ def add_all_sub_models(model, result):
+ for b in model.__subclasses__():
+ add_model_if_regular(b, False, result)
- return super(PolymorphicModel, self).__getattribute__(name)
+ result = {}
+ add_all_super_models(self.__class__,result)
+ add_all_sub_models(self.__class__,result)
+ return result
+
View
4 polymorphic/tests.py
@@ -249,8 +249,8 @@ def test_limit_choices_to(self):
>>> settings.DEBUG=True
->>> get_version()
-'1.0 rc1'
+#>>> get_version()
+#'1.0 rc1'
### simple inheritance

0 comments on commit 6628145

Please sign in to comment.
Something went wrong with that request. Please try again.