hgcrpd
hgcrpd

Reputation: 1900

Python/Django - Dynamic property overloading for Django models

I have two related models:

class FirstModel(models.Model):
    base_value = models.FloatField()

class SecondModel(models.Model):
    parent = models.ForeignKey(FirstModel)

    @property
    def parent_value(self):
        return self.parent.base_value

    @property
    def calculate(self):
        return self.parent_value + 1

In general, SecondModel.calculate is mostly used in the context of its related FirstModel. However, I sometimes want to be able to call calculate with a temporary value as its parent_value. Something like this:

foo = SecondModel()
# would look in the database for the related FirstModel and add 1 to its base_value
foo.calculate

foo.parent_value = 10
foo.calculate      # should return 11

Obviously you can't do this because the parent_value is a read-only property. I also have many different models similar to SecondModel that needs to have this kind of capability.

I've thought about and tried several things, but none have quite seemed to work:

1) Writing a Django proxy model - possible, but the number of objects is rather high, so I'd be writing a lot of similar code. Also, there appears to be a bug related to overriding properties: https://code.djangoproject.com/ticket/16176. But it'd look like this:

class ModelProxy(SecondModel):
    class Meta:
         proxy = True
    def __init__(self, temp_value):
         self.parent_value = temp_value

2) Overloading the parent_value property on the instance - like this:

foo = SecondModel()
setattr(foo, 'parent_value', 10)

but you can't do this because properties are members of the class, not the instance. And I only want the temporary value to be set for the instance

3) Metaclass or class generator? - Seems overly complicated. Also, I am uncertain what would happen if I used a metaclass to dynamically generate classes that are children of models.Model. Would I run into problems with the db tables not being in sync?

4) Rewriting the properties with proper getters and setters? - maybe the solution is to rewrite SecondModel so that the property can be set?

Any suggestions?

Upvotes: 1

Views: 1718

Answers (3)

robjohncox
robjohncox

Reputation: 3665

I think you can do what you need to using the mixin PropertyOverrideMixin shown below which, if some property value isn't available, then it will look for the same property prefixed with temp_. This will allow you to provide temporary values that can be used when the real property values can't be looked up.

Below is the mixin, some example models and a unit test to show how this can work. Hopefully this can be adapted for your problem! Finally it is worth mentioning that the properties here can be interchanged with normal object attributes and it should still all work.

from unittest import TestCase


class PropertyOverrideMixin(object):

    def __getattribute__(self, name):
        """
        Override that, if an attribute isn't found on the object, then it instead
        looks for the same attribute prefixed with 'temp_' and tries to return
        that value.
        """

        try:
            return object.__getattribute__(self, name)
        except AttributeError:
            temp_name = 'temp_{0}'.format(name)
            return object.__getattribute__(self, temp_name)


class ParentModel(object):

    attribute_1 = 'parent value 1'


class Model(PropertyOverrideMixin):

    # Set our temporary property values
    @property
    def temp_attribute_1(self):
        return 'temporary value 1'

    @property
    def temp_attribute_2(self):
        return 'temporary value 2'

    # Attribute 1 looks up value on its parent
    @property
    def attribute_1(self):
        return self.parent.attribute_1

    # Attribute 2 looks up a value on this object
    @property
    def attribute_2(self):
        return self.some_other_attribute


class PropertyOverrideMixinTest(TestCase):

    def test_attributes(self):
        model = Model()

        # Looking up attributes 1 and 2 returns the temp versions at first
        self.assertEquals('temporary value 1', model.attribute_1)
        self.assertEquals('temporary value 2', model.attribute_2)

        # Now we set the parent, and lookup of attribute 1 works on the parent
        model.parent = ParentModel()
        self.assertEquals('parent value 1', model.attribute_1)

        # now we set attribute_2, so this gets returned and the temporary ignored
        model.some_other_attribute = 'value 2'
        self.assertEquals('value 2', model.attribute_2)

Upvotes: 0

robjohncox
robjohncox

Reputation: 3665

I believe a mixin would achieve what you want to do, and provide a simple and reusable way of supporting temporary values in your calculations. By mixing the below example into each model you want this behaviour on you can then:

  • Set a temporary parent value on each model
  • When calculate is called, it will check whether there is a property parent_value available, and if not it will use the temporary parent value in the calculation.

The code below should achieve what you are looking for - apologies I haven't been able to test it yet but it should be about right - please let me know if any problems that need editing.

class CalculateMixin(object):

    @property
    def temp_parent_value(self):
        return self._temp_parent_value

    @temp_parent_value.setter
    def temp_parent_value(self, value):
        self._temp_parent_value = value

    @property
    def calculate(self):
        parent_value = self.parent_value if self.parent_value else self.temp_parent_value
        return parent_value + 1


class SecondModel(models.Model, CalculateMixin):
    parent = models.ForeignKey(FirstModel)

    self.temp_parent_value = 'Whatever value you desire'

    @property
    def parent_value(self):
        return self.parent.base_value

Upvotes: 1

vvd
vvd

Reputation: 476

You can use the property setter:

class SecondModel(models.Model):
    _base_value = None

    parent = models.ForeignKey(FirstModel)

    @property
    def parent_value(self):
        if self._base_value is None:
            return self.parent.base_value
        else:
            return self._base_value

    @parent_value.setter
    def parent_value(self, value):
        self._base_value = value

    @property
    def calculate(self):
        return self.parent_value + 1

Upvotes: 0

Related Questions