Django's manager class has one hook for providing the QuerySet, so we'll start with this:
from django.db import models
class MagicManager(models.Manager):
def get_query_set(self):
qs = super(MagicManager, self).get_query_set()
return qs
Here we have a very simple get_query_set method, it doesn't do anything but return it's parent's queryset. Now we need to actually get the methods defined on our class onto the queryset:
class MagicManager(models.Manager):
def get_query_set(self):
qs = super(MagicManager, self).get_query_set()
class _QuerySet(qs.__class__):
pass
for method in [attr for attr in dir(self) if not attr.startswith('__') and callable(getattr(self, attr)) and not hasattr(_QuerySet, attr)]:
setattr(_QuerySet, method, getattr(self, method))
qs.__class__ = _QuerySet
return qs
The trick here is we dynamically create a subclass of whatever class the call to our parent's get_query_set method returns, then we take each attribute on ourself, and if the queryset doesn't have an attribute by that name, and if that attribute is a method then we assign it to our QuerySet subclass. Finally we set the __class__ attribute of the queryset to be our QuerySet subclass. The reason this works is when Django chains queryset methods it makes the copy of the queryset have the same class as the current one, so anything we add to our manager will not only be available on the immediately following queryset, but on any that follow due to chaining.
Now that we have this we can simply subclass it to add methods, and then add it to our models like a regular manager. Whether this is a good idea is a debatable issue, on the one hand having to write methods twice is a gross violation of Don't Repeat Yourself, however this is exceptionally implicit, which is a major violation of The Zen of Python.
your final loop looks kinda strange to me... why not:
ReplyDeletefor method_name, method in self.__dict__.items():
if not method_name.startswith('__') and callable(method):
setattr(_QuerySet, method_name, method)
looks cleaner ;)
Monster:
ReplyDeleteThat looks like it would work as well, it's totally in the eyes of the beholder as to which you prefer, personally I love list comprehensions, but you're obviously works as well(it also doesn't violate the 80 char rule).
it's not the comprehension and the 80 char thing that's bugging me, it's the getattr abuse :P
ReplyDeleteHey, you seem to post a lot of code around here. Why not highlight it so us readers can better read your code? Take a look: http://it-ride.blogspot.com/2009/03/syntax-highlighting-on-blogger-with.html :-)
ReplyDeleteCool article!
ReplyDeleteAnd in battle of DRY versus Explicity, DRY wins in this case IMO. Because Django's ORM is one big violation of The Zen of Python =) You've mentioned hack with QuerySet class substitutions in chains, there is hack with default "objects" manager, hack with related_names and many more hacks. Why not to add just another one?! :) It doesn't violates Django ORM's ideology I think.
Thanks for post.