Why can't I use __getattr__ with Django models?
Asked Answered
L

2

9

I've seen examples online of people using __getattr__ with Django models, but whenever I try I get errors. (Django 1.2.3)

I don't have any problems when I am using __getattr__ on normal objects. For example:

class Post(object):
     def __getattr__(self, name):
         return 42

Works just fine...

 >>> from blog.models import Post
 >>> p = Post()
 >>> p.random
 42

Now when I try it with a Django model:

from django.db import models
class Post(models.Model):
     def __getattr__(self, name):
         return 42

And test it on on the interpreter:

 >>> from blog.models import Post
 >>> p = Post()
 ERROR: An unexpected error occurred while tokenizing input The

following traceback may be corrupted or invalid The error message is: ('EOF in multi-line statement', (6, 0))

--------------------------------------------------------------------------- TypeError
Traceback (most recent call last)

/Users/josh/project/ in ()

/Users/josh/project/lib/python2.6/site-packages/django/db/models/base.pyc in init(self, *args, **kwargs) 338 if kwargs: 339 raise TypeError("'%s' is an invalid keyword argument for this function" % kwargs.keys()[0]) --> 340 signals.post_init.send(sender=self.class, instance=self) 341 342 def repr(self):

/Users/josh/project/lib/python2.6/site-packages/django/dispatch/dispatcher.pyc in send(self, sender, **named) 160 161 for receiver in self._live_receivers(_make_id(sender)): --> 162 response = receiver(signal=self, sender=sender, **named) 163 responses.append((receiver, response)) 164 return responses

/Users/josh/project/python2.6/site-packages/photologue/models.pyc in add_methods(sender, instance, signal, *args, **kwargs) 728 """ 729 if hasattr(instance, 'add_accessor_methods'): --> 730 instance.add_accessor_methods() 731 732 # connect the add_accessor_methods function to the post_init signal

TypeError: 'int' object is not callable

Can someone explain what is going on?


EDIT: I may have been too abstract in the examples, here is some code that is closer to what I actually would use on the website:

class Post(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField()
    date_published = models.DateTimeField()
    content = RichTextField('Content', blank=True, null=True)
    # Etc...

Class CuratedPost(models.Model):
    post = models.ForeignKey('Post')
    position = models.PositiveSmallIntegerField()

    def __getattr__(self, name):
        ''' If the user tries to access a property of the CuratedPost, return the property of the Post instead...  '''
        return self.post.name

    # Etc...

While I could create a property for each attribute of the Post class, that would lead to a lot of code duplication. Further more, that would mean anytime I add or edit a attribute of the Post class I would have to remember to make the same change to the CuratedPost class, which seems like a recipe for code rot.

Labial answered 7/1, 2011 at 21:12 Comment(3)
is that really "return self.post.name"? It should be "return getattr(self.post, name)"Latrishalatry
Indeed, that is what the code should beLabial
I've run into infinite recursion problems using a similar pattern in the case that the name input is itself "post".Hydrolytic
L
7

One must be careful using __getattr__ . Only intercept what you know, and let the base class handle what you do not.

The first step is, can you use a property instead? If you want a "random" attribute which return 42 then this is much safer:

class Post(...):
  @property
  def random(self):
    return 42

If you want "random_*" (like "random_1", "random_34", etc) to do something then you'll have to use __getattr__ like this:

class Post(...):
  def __getattr__(self, name):
    if name.startswith("random_"):
      return name[7:]
    return super(Post, self).__getattr__(name)
Latrishalatry answered 8/1, 2011 at 14:18 Comment(3)
The example I used in my post was supposed to show that even very simple applications of the getattr property break when I extend the Django model object. I have updated my original post to include something closer to what I would actually use.Labial
I don't believe this will work with Django, because models.Model has no __getattr__, so your super(Post, self).__getattr__(...) should throw an exception.Lecompte
Have you tried it? Python provides a default getattr, so it 'should' not be a problem.Latrishalatry
G
0

Django sends certain signals when models are first initialized (ie, by loading up the shell) - by making it so that calls to __getattr always return an integer, you've modified the code in a way that Django signals weren't expecting (and therefore, they're breaking).

If you want to do this, maybe try it this way:

def __getattr__(self, attr):
  if hasattr(self, attr):
    return super(MyModel, self).__getattr__(attr)
  return 42
Grapery answered 7/1, 2011 at 21:16 Comment(5)
This doesn't work since hasattr(self, attr) ends up calling __getattr__ recursively. You want "attr in self.__dict__" instead.Latrishalatry
And actually, that won't work either since __getattr__ is only called if attr is not in __dict__Latrishalatry
@Andrew: Sounds like you could do that with __getattribute__, thoughRemillard
@Cameron: __getattribute__ is also tricky, just in different ways. I suspect @Labial should use a property instead.Latrishalatry
The "Return 42" is just an example, __getattr__ breaks no matter what I have it return. I've seen code samples from older Django projects that used __getattr__ without problem and I can't find any documentation that says it has been deprecated.Labial

© 2022 - 2024 — McMap. All rights reserved.