5

I'm trying to change the behaviour of a Django model to allow me to access a foreign key's properties directly from the parent, e.g.

cache.part_number  
vs  
cache.product.part_number

I've tried overriding the __getattr__ method as follows, but I get a recursion error when I try to access the foreign key's properties

class Product(models.Model):
    part_number = models.CharField(max_length=10)
    ...

class Cache(models.Model):
    product = models.ForeignKey(Product)
    ...

    def __getattr__(self, name):
        value = getattr(self.product, name, None)
        if value:
            return value
        else:
            raise AttributeError

What am I doing wrong?

drjeep
  • 1,019
  • 2
  • 9
  • 15
  • Why are you trying to override a python inner function instead of using a typical function or method name? – Mp0int Sep 21 '10 at 07:13

4 Answers4

10

Consider the code inside your __getattr__ method:

value = getattr(self.product, name, None)

Try guessing what happens when self.product is invoked. I'll give you a clue: it involves a call to __getattr__. The documentation has the details:

Called when an attribute lookup has not found the attribute in the usual places (i.e. it is not an instance attribute nor is it found in the class tree for self). name is the attribute name. This method should return the (computed) attribute value or raise an AttributeError exception.

Have you wondered how self.product resolves to the correct Product instance, even though you are not setting it anywhere?

Note that if the attribute is found through the normal mechanism, __getattr__() is not called.

Django does some magic that involves intercepting, you guessed it, __getattr__. Thereby self automatically ends up with an attribute product. Since you are overriding the __getattr__ method, Django's magic ceases to work and your version is used. Since self.product is not an instance attribute, __getattr__ is called again, and again and so on, leading to an infinite loop.

You'd be better off using a property to achieve this.

class Cache(models.Model):
    product = models.ForeignKey(Product)
    ...

    def _get_part_number(self):
        part_number = self.product.part_number
        if not part_number:
            raise AttributeError
        return part_number
    part_number = property(_get_part_number)
Manoj Govindan
  • 72,339
  • 21
  • 134
  • 141
  • now there's an answer I can +1. Though instead of a property, OP could use `super(Cache, self).__getattr__('product')`, or am I missing something? – aaronasterling Sep 21 '10 at 07:48
  • @Aaron: Aye, you can. Do you mean to add the snippet in a method, say `def part_number(self)`? – Manoj Govindan Sep 21 '10 at 07:51
  • I meant to add it to OP's `__getattr__` function. I'll add an answer to clarify. – aaronasterling Sep 21 '10 at 07:58
  • Thanks for the explanation, @Manoj. I considered @property, but my problem is Product has over 100 fields. Is there no other way to make all Product's fields available through Cache? – drjeep Sep 21 '10 at 08:21
1

I had a similar problem to the one posted here and only got the answer when I looked into the way Python accesses attributes of an object.

As i understand it when getattr() is called Python first calls getattribute(), if that attribute is not found by getattribute then python will use your getattr function.

I tried to stay away from using getattr within my function because it causes infinite recursions see: https://stackoverflow.com/a/3278104/2319915

so:

class Product(models.Model):
   part_number = models.CharField(max_length=10)


class Cache(models.Model):
   product = models.ForeignKey(Product)

   def __getattr__(self, name):
      try:
         return getattribute(self, name)
      except AttributeError:
         try:
            return Product.objects.get(part_no=self.product.part_no)
         except ObjectDoesNotExist:
            raise AttributeError
Community
  • 1
  • 1
colins44
  • 544
  • 6
  • 9
0

How about something like:

class Product(models.Model):
    part_number = models.CharField(max_length=10)
    ...

class Cache(models.Model):
    product = models.ForeignKey(Product)
    ...

    def __getattr__(self, name):
        prefix = 'product_'

        # Only deal with get_ calls
        if not name.startswith(prefix):
            raise AttributeError
        else:
            name = name.replace(prefix,'')
            value = getattr(self.product, name, None)
            if value:
                return value
            else:
                raise AttributeError

You can then call:

cache.product_part_number
so_
  • 354
  • 3
  • 10
-1

I had a similiar need and i have solved it for me so i thought i would share. Here is the solution converted to your example:

class Product(models.Model):
    part_number = models.CharField(max_length=10)
    test_value1 = models.CharField(max_length=20)
    test_value2 = models.CharField(max_length=20)

class Cache(models.Model):
    name = models.CharField(max_length=30)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    def __getattr__(self, name):

        if name in [f.name for f in Product._meta.get_fields()]:
            return getattr(self.product, name, None)

        else:
            raise AttributeError('Item not found')

If you were calling this many times per instance you could also potentially make it a little more efficient by building the list once and storing locally on first call.

class Product(models.Model):
    part_number = models.CharField(max_length=10)
    test_value1 = models.CharField(max_length=20)
    test_value2 = models.CharField(max_length=20)

class Cache(models.Model):
    name = models.CharField(max_length=30)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    def __getattr__(self, name):

        if name == 'prod_attrlist':
            self.__setattr__('prod_attrlist', set([f.name for f in Product._meta.get_fields()]))
            return self.prod_attrlist

        elif name in self.prod_attrlist:
            return getattr(self.product, name, None)

        else:
            raise AttributeError('Item not found')

Then i tested as follows:

newitem = Product(part_number='123', test_value1='456', test_value2='789')
newitem.save()
newitem2 = Cache(name='testcache', product=newitem)
newitem2.save() 

item = Cache.objects.get(name='testcache')

print(item.part_number)     #123
print(item.test_value1)     #456
print(item.doesntexist)     #AttributeError:  Item not found

The reason this works is that you are only calling getattr for items which are known to exist ensuring that you will not enter an infinite loop.

MPlumb
  • 67
  • 5