From 6628145af7e894f6977e3a14a713ac14449f3a4e Mon Sep 17 00:00:00 2001 From: Bert Constantin Date: Mon, 18 Oct 2010 12:06:50 +0200 Subject: [PATCH] removed __getattribute__ hack from PolymorphicModel. A somewhat cleaner solution is now used (through __init__) which also completely removes the performance impact of __getattribute__. --- .gitignore | 1 + pexp/management/commands/pcmd.py | 23 ++++--- pexp/models.py | 7 +++ polymorphic/base.py | 6 +- polymorphic/polymorphic_model.py | 100 +++++++++++++++++++------------ polymorphic/tests.py | 4 +- 6 files changed, 88 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index c31d5c7..910a534 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ pbackup mcmd.py dbconfig_local.py diffmanagement +scrapbook.py pip-log.txt build diff --git a/pexp/management/commands/pcmd.py b/pexp/management/commands/pcmd.py index 5a53db4..48fa4b7 100644 --- a/pexp/management/commands/pcmd.py +++ b/pexp/management/commands/pcmd.py @@ -23,19 +23,18 @@ class Command(NoArgsCommand): print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH 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') - 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") + 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() + 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 + """ diff --git a/pexp/models.py b/pexp/models.py index 3471b64..cc9ba54 100644 --- a/pexp/models.py +++ b/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) diff --git a/polymorphic/base.py b/polymorphic/base.py index 558bd29..7210558 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -73,6 +73,9 @@ class PolymorphicModelBase(ModelBase): # 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 @@ class PolymorphicModelBase(ModelBase): 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 @@ class PolymorphicModelBase(ModelBase): return super(PolymorphicModelBase, self).__getattribute__(name) - - diff --git a/polymorphic/polymorphic_model.py b/polymorphic/polymorphic_model.py index 2b7bdd4..b39cbec 100644 --- a/polymorphic/polymorphic_model.py +++ b/polymorphic/polymorphic_model.py @@ -113,49 +113,75 @@ class PolymorphicModel(models.Model): 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 __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). - 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) + 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 - def add_sub_models(model, result): - for b in model.__subclasses__(): - add_if_regular_sub_or_super_class(b, False, result) + These accessors allow Django (and everyone else) to travel up and down + the inheritance tree for the db object at hand. - 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 + 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 - 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 create_accessor_function_for_model(model): + def accessor_function(self): + attr = model.base_objects.get(pk=self.pk) return attr + return accessor_function - return super(PolymorphicModel, self).__getattribute__(name) + 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) + + result = {} + add_all_super_models(self.__class__,result) + add_all_sub_models(self.__class__,result) + return result + + \ No newline at end of file diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 52b7694..8c0721b 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -249,8 +249,8 @@ __test__ = {"doctest": """ >>> settings.DEBUG=True ->>> get_version() -'1.0 rc1' +#>>> get_version() +#'1.0 rc1' ### simple inheritance