Browse files

Initial commit.

  • Loading branch information...
0 parents commit 636a78c08f81f7aeabbdfd1123eaaa3f8b4b12d1 @fish2000 committed Sep 8, 2011
Showing with 437 additions and 0 deletions.
  1. +26 −0 LICENSE.txt
  2. +2 −0 MANIFEST.in
  3. +76 −0 README.rst
  4. +296 −0 delegate/__init__.py
  5. +2 −0 delegate/models.py
  6. +35 −0 setup.py
26 LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2007-2008, Alexander Böhn
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of django-imagekit nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2 MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst
+include LICENSE.txt
76 README.rst
@@ -0,0 +1,76 @@
+
+===============
+django-delegate
+===============
+
+With django-delegate, you get AUTOMATICALLY CHAINABLE MANAGER/QUERYSET DELEGATE METHODS.
+
+Normally, by defining manager methods, Django lets you do this:
+
+::
+
+ >>> SomeModel.objects.custom_query()
+
+... but it WON'T let you do this:
+
+::
+
+ >>> SomeModel.objects.custom_query().another_custom_query()
+
+... unless you duplicate your methods and define a redundant queryset subclass... UNTIL NOW.
+
+With DelegateManager and @delegate, you can write maintainable custom-query logic
+with free chaining. instead of defining manager methods, you define queryset methods,
+decorate those you'd like to delegate, and a two-line DelegateManager subclass
+specifying the queryset. ET VIOLA. Like so:
+
+::
+
+ class CustomQuerySet(models.query.QuerySet):
+
+ @delegate
+ def qs_method(self, some_value):
+ return self.filter(some_param__icontains=some_value)
+
+ def dont_delegate_me(self):
+ return self.filter(some_other_param="something else")
+
+ class CustomManager(DelegateManager):
+ __queryset__ = CustomQuerySet
+
+ class SomeModel(models.Model):
+ objects = CustomManager()
+
+
+ # will work:
+ SomeModel.objects.qs_method('yo dogg')
+ # will also work:
+ SomeModel.objects.qs_method('yo dogg').qs_method('i heard you like queryset method delegation')
+
+To delegate all of the methods in a QuerySet automatically, you can create a subclass
+of DelegateQuerySet. These two QuerySet subclasses work identically:
+
+::
+
+ class ManualDelegator(models.query.QuerySet):
+ @delegate
+ def qs_method(self):
+ # ...
+
+ class AutomaticDelegator(DelegateQuerySet):
+ def qs_method(self):
+ # ...
+
+
+You can also apply the @delegate decorator directly to a class -- this permits you to
+delegate all the methods in a class without disrupting its inheritance chain. This example
+works identically to the previous two:
+
+::
+
+ @delegate
+ class CustomQuerySet(models.query.QuerySet):
+
+ def qs_method(self, some_value):
+ return self.filter(some_param__icontains=some_value)
+
296 delegate/__init__.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python
+# encoding: utf-8
+"""
+delegate/__init__.py
+
+Created by Alexander Bohn on 2011-02-08.
+Copyright (c) 2011 Objects in Space and Time. All rights reserved.
+
+"""
+__author__ = 'Alexander Bohn'
+__version__ = (0, 1, 0)
+
+import types
+from django.db import models
+
+comsumable_types = (
+ types.FunctionType,
+ types.MethodType,
+)
+
+delegateable_types = comsumable_types + (
+ types.ClassType,
+ types.TypeType,
+)
+
+def delegate(f_or_cls):
+ """
+ # Delegate QuerySet methods to a DelegateManager subclass by decorating them thusly:
+
+ class CustomQuerySet(models.query.QuerySet):
+
+ @delegate
+ def qs_method(self, some_value):
+ return self.filter(some_param__icontains=some_value)
+
+ # ...
+
+ # You can also decorate classes -- this will delegate all of the classes' methods:
+
+ @delegate
+ class CustomQuerySet(models.query.QuerySet):
+
+ def qs_method(self, some_value):
+ return self.filter(some_param__icontains=some_value)
+
+ # Both of these examples are equivalent.
+
+ """
+ if type(f_or_cls) in delegateable_types:
+
+ # It's a class decorator.
+ f_or_cls.__delegate__ = 1
+
+ if hasattr(f_or_cls, '__dict__'):
+ cls_funcs = filter(lambda attr: type(attr) in comsumable_types, f_or_cls.__dict__.values())
+ for cls_func in cls_funcs:
+ cls_func.__delegate__ = 1
+
+ else:
+ # It's a function decorator.
+ f_or_cls.__delegate__ = 1
+
+ return f_or_cls
+
+
+class DelegateSupervisor(type):
+ """
+ # The DelegateSupervisor metaclass handles delegation
+ # of the specified methods from a QuerySet to a Manager
+ # at compile-time. You don't need to invoke it to
+ # perform your delegations.
+
+ """
+ def __new__(cls, name, bases, attrs):
+
+ qs_delegates = dict()
+
+ if '__queryset__' in attrs:
+
+ qs = attrs.get('__queryset__', None)
+
+ if issubclass(qs, models.query.QuerySet):
+ qs_funcs = dict(filter(lambda attr: type(attr[1]) in (types.FunctionType, types.MethodType), qs.__dict__.items()))
+
+ deleg = 0
+
+ for f_name, f in qs_funcs.items():
+ if issubclass(qs, DelegateQuerySet):
+ deleg += 1
+ qs_delegates[f_name] = f
+ elif hasattr(f, '__delegate__'):
+ deleg += 1
+ qs_delegates[f_name] = f
+
+ #print "Delegating %s funcs to %s from %s" % (deleg, name, qs.__name__)
+
+ attrs.update(qs_delegates)
+ return super(DelegateSupervisor, cls).__new__(cls, name, bases, attrs)
+
+
+class DelegateManager(models.Manager):
+ """
+ # Subclass DelegateManager and specify the queryset from which to delegate:
+
+ class CustomManager(DelegateManager):
+ __queryset__ = CustomQuerySet
+
+ # No other methods are necessary.
+
+ """
+ __metaclass__ = DelegateSupervisor
+
+ use_for_related_fields = True
+
+ def __init__(self, fields=None, *args, **kwargs):
+ super(DelegateManager, self).__init__(*args, **kwargs)
+ self.__managerfields__ = fields
+
+ def get_query_set(self):
+ qs = getattr(self, '__queryset__', None)
+ if callable(qs):
+ return qs(self.model, self.__managerfields__)
+ return None
+
+
+class DelegateQuerySet(models.query.QuerySet):
+ """
+ # ALL methods in a DelegateQuerySet subclass will
+ # be delegated -- no decoration needed.
+ # These two QuerySet subclasses work identically:
+
+ class ManualDelegator(models.query.QuerySet):
+ @delegate
+ def qs_method(self):
+ # ...
+
+ class AutomaticDelegator(DelegateQuerySet):
+ def qs_method(self):
+ # ...
+
+ """
+ pass
+
+
+
+
+"""
+*************************** WARNING -- HIGHLY EXPERIMENTAL -- FOR THE TRULY LAZY ***************************
+
+@micromanage -- get it? 'micromanage'? -- will cut out even more boilerplate from your manager definitions.
+The following class decorator @micromanage will set everything up for you, as per the above delegation
+apparatus, in one fell swoop. You do this:
+
+
+@micromanage(model=MyModel)
+class MyQuerySet(models.query.QuerySet):
+ def yodogg(self):
+ return self.filter(yo__icontains="dogg")
+
+
+... and everything will behave as though you had done ALL THIS:
+
+
+class MyQuerySet(models.query.QuerySet):
+ def yodogg(self):
+ return self.filter(yo__icontains="dogg")
+
+class MyManager(models.Manager):
+ use_for_related_fields = True
+
+ def __init__(self, fields=None, *args, **kwargs):
+ super(MyManager, self).__init__(*args, **kwargs)
+ self._fields = fields
+
+ def get_query_set(self):
+ return MyQuerySet(self.model, self._fields)
+
+ def yodogg(self):
+ return self.filter(yo__icontains="dogg")
+
+class MyModel(models.Model):
+ objects = MyManager()
+ # ...
+
+
+This minimizes boilerplate -- you're defining the manager AND queryset AND plugging the manager
+into the model in one fell swoop. @micromanage creates a manager automatically, based on your
+queryset. If you already have a manager defined on your target model (with "objects = SomeManager()"),
+@micromanage will subclass the manager you've chosen, to keep your refactoring to a minimum.
+It can also subclass an arbitrary manager class you specify in the decorator kwargs -- see
+the micromanage.__init__() method and the undergo_management_training() function below.
+
+HOWEVER: the implementation is meta enough that it may very well break your existing shit.
+
+So DO NOT USE IT ANYWHERE NEAR PRODUCTION. Do not use it with existing codebases **IN GENERAL** and
+expect everything to come up roses, unless you have the most anal-retentive test suite around
+and/or you enjoy debugging tracebacks with "Error when calling the metaclass bases" in them (google
+it if you're curious). YOU HAVE BEEN WARNED. PROCEED PAST THIS POINT AT YOUR OWN RISK.
+
+At the moment, one limitation is: you have to define @micromanaged QuerySet subclasses AFTER you
+define the model, because you need to pass the model class itself into the decorator kwargs;
+in the future I may make it work by naming the class with a string instead, but maybe not,
+as the thing is already pushing it w/r/t complexity, I think.
+
+"""
+
+
+def undergo_management_training(queryset=None, progenitor=None):
+ """
+ I believe this function is an example of a 'factory', as per the employ of many Java afficionatos.
+
+ """
+ if not progenitor:
+ progenitor = models.Manager
+
+ if queryset and hasattr(progenitor, '__class__'):
+
+ # define the micromanager
+ class MicroManager(progenitor.__class__):
+ use_for_related_fields = True
+
+ __queryset__ = queryset
+
+ def __init__(smelf, fields=None, *args, **kwargs):
+ super(MicroManager, smelf).__init__(*args, **kwargs)
+ smelf.__managerfields__ = fields
+
+ def get_query_set(smelf):
+ queset = getattr(smelf, '__queryset__', None)
+ if callable(queset):
+ return queset(smelf.model, smelf.__managerfields__)
+ return None
+
+ # return it
+ return MicroManager
+
+class micromanage(object):
+
+ cls_name_suffixes = ('QuerySet', 'Queryset', 'queryset')
+
+ def __init__(self, *args, **kwargs):
+ self.clsname = kwargs.pop('name', None)
+ self.target_model = kwargs.pop('model', None)
+ self.subclass = kwargs.pop('subclass', models.Manager)
+ super(micromanage, self).__init__(*args, **kwargs)
+
+ if not self.clsname:
+ if hasattr(self.target_model, "__name__"):
+ self.clsname = "%sMgr" % self.target_model.__name__
+
+ def __call__(self, qs):
+ if issubclass(self.target_model, models.Model):
+ theoldboss = getattr(self.target_model, 'objects', None)
+ if theoldboss:
+ # subclass existant manager if one wasn't specified
+ if self.subclass == models.Manager and issubclass(theoldboss.__class__, models.Manager):
+ self.subclass = theoldboss # literally, same as the new one
+
+ # crank out the new manager
+ newmgr = undergo_management_training(qs, self.subclass)
+
+ # if there isn't an explicit name for the generated manager class,
+ # try to make one out of this queryset's name:
+ if not self.clsname:
+ for suffix in self.cls_name_suffixes:
+ if qs.__name__.endswith(suffix):
+ self.clsname = "%sMgr" % qs.__name__.rstrip(suffix)
+ break
+
+ if not self.clsname: # if *still* not self.clsname
+ self.clsname = self.clsname = "%sMgr" % qs.__name__
+
+ newmgr.__name__ = self.clsname
+
+ # delegate all methods. (currently we delegate everything, ignoring status
+ # as it is set per the @delegate decorator... that may change)
+ if issubclass(qs, models.query.QuerySet):
+ #qs_delegates = dict()
+ qs_funcs = dict(filter(lambda attr: type(attr[1]) in comsumable_types, qs.__dict__.items()))
+ for f_name, f in qs_funcs.items():
+ setattr(newmgr, f_name, f)
+
+ # if a target_model was specified,
+ # add an instance of the new queryset class
+ # TODO: do something useful with the class in the absence of a target_model setting
+ if issubclass(self.target_model, models.Model):
+ qs.model = self.target_model
+ self.target_model.add_to_class('objects', newmgr())
+ self.target_model._default_manager = self.target_model.objects
+
+ # we're done here.
+ return qs
+
+
+if __name__ == '__main__':
+ pass
2 delegate/models.py
@@ -0,0 +1,2 @@
+
+# this models.py file intentionally left blank
35 setup.py
@@ -0,0 +1,35 @@
+#/usr/bin/env python
+from distutils.core import setup
+
+setup(
+ name='django-delegate',
+ version='0.1.0',
+ description='Automatic delegate methods for Django managers and querysets without runtime dispatch penalties.',
+ author='Alexander Bohn',
+ author_email='fish2000@gmail.com',
+ maintainer='Alexander Bohn',
+ maintainer_email='fish2000@gmail.com',
+ license='BSD',
+ url='http://github.com/fish2000/django-delegate/',
+ keywords=[
+ 'django',
+ 'delegate',
+ 'queryset',
+ 'manager',
+ ],
+ packages=[
+ 'delegate',
+ ],
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Topic :: Utilities'
+ ]
+)
+

0 comments on commit 636a78c

Please sign in to comment.