daily pastebin goal
34%
SHARE
TWEET

Untitled

a guest Jun 13th, 2018 43 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. '''Manager-based polymorphic model inheritance.
  2.  
  3. This module provides a non-intrusive approach for accessing polymorphically
  4. instances of a model hierarchy. Non-intrusive means:
  5.   - It does not require extending a custom ``Model`` base class or metaclass.
  6.   - It does not require a ``ForeignKey`` to ``ContentType`` or the ``contenttypes``
  7.     app in general. Instead the real class of an instance is determined based on
  8.     the value (**polymorphic identity**) of a user-specified discriminating field
  9.     (**polymorphic on**).
  10.   - It does not override the default (or any other) model ``Manager`` (unless
  11.     explicitly shadowed). Standard (non-polymorphic) managers and querysets can
  12.     be still available.
  13.   - It does not have "magic" hidden side effects.
  14.  
  15. A single :func:`polymorphic_manager` function is exported. To use it:
  16.  
  17. 1. Create a polymorphic manager on the parent Model of the hierarchy::
  18.  
  19.     from polymorphic import polymorphic_manager
  20.  
  21.     class Player(models.Model):
  22.         hitpoints = models.PositiveIntegerField(default=100)
  23.  
  24.         # polymorphic_on field
  25.         race = models.SmallIntegerField(choices=enumerate(['Elf', 'Troll', 'Human']))
  26.  
  27.         # keep the default (non-polymorphic) manager
  28.         objects = models.Manager()
  29.  
  30.         # a new manager polymorphic on Player.race
  31.         objects_by_race = polymorphic_manager(on=race)
  32.  
  33.         def __unicode__(self):
  34.             return u'Player(%s)' % self.pk
  35.  
  36. 2. Create a polymorphic manager (usually default) on each child Model by
  37.    calling the :meth:`.polymorphic_identity` method of the parent polymorphic
  38.    manager and specifying the polymorphic identity for this model::
  39.  
  40.     class Elf(Player):
  41.         bows = models.PositiveIntegerField(default=0)
  42.  
  43.         # polymorphic manager for race=0
  44.         objects = Player.objects_by_race.polymorphic_identity(0)
  45.  
  46.         def __unicode__(self):
  47.             return u'Elf(%s)' % self.pk
  48.  
  49.     class Troll(Player):
  50.         axes = models.PositiveIntegerField(default=0)
  51.  
  52.         # polymorphic manager for race=1
  53.         objects = Player.objects_by_race.polymorphic_identity(1)
  54.  
  55.         def __unicode__(self):
  56.             return u'Troll(%s)' % self.pk
  57.  
  58.    Proxy models work too::
  59.  
  60.     class Human(Player):
  61.  
  62.         # polymorphic manager for race=2
  63.         objects = Player.objects_by_race.polymorphic_identity(2)
  64.  
  65.         class Meta:
  66.             proxy = True
  67.  
  68.         def __unicode__(self):
  69.             return u'Human(%s)' % self.pk
  70.  
  71. 3. And that's all, you can access instances polymorphically or non polymorphically::
  72.  
  73.     def test():
  74.         from random import choice
  75.  
  76.         # create a bunch of random type players
  77.         for i in xrange(10):
  78.             choice([Elf, Troll, Human]).objects.create()
  79.  
  80.         # retrieval through the polymorphic manager returns instances of the right class
  81.         print "Automatically downcast players:", Player.objects_by_race.all()
  82.  
  83.         # retrieval through default Player manager returns Player instances as usual
  84.         players = Player.objects.all()
  85.         print "Non-downcast players:", players
  86.  
  87.         # but they cast be explicitly downcast to the right class
  88.         print "Explicitly downcast players:", map(Player.objects_by_race.downcast, players)
  89.  
  90.         # retrieving the instances of a specific class works as expected
  91.         print "Elfs:", Elf.objects.all()
  92.         print "Trolls:", Troll.objects.all()
  93.         print "Humans:", Human.objects.all()
  94.  
  95.     >>> test()
  96.     Automatically downcast players: [<Troll: Troll(1)>, <Human: Human(2)>, <Human: Human(3)>, <Elf: Elf(4)>, <Human: Human(5)>, <Troll: Troll(6)>, <Troll: Troll(7)>, <Human: Human(8)>, <Troll: Troll(9)>, <Elf: Elf(10)>]
  97.     Non-downcast players: [<Player: Player(1)>, <Player: Player(2)>, <Player: Player(3)>, <Player: Player(4)>, <Player: Player(5)>, <Player: Player(6)>, <Player: Player(7)>, <Player: Player(8)>, <Player: Player(9)>, <Player: Player(10)>]
  98.     Explicitly downcast players: [<Troll: Troll(1)>, <Human: Human(2)>, <Human: Human(3)>, <Elf: Elf(4)>, <Human: Human(5)>, <Troll: Troll(6)>, <Troll: Troll(7)>, <Human: Human(8)>, <Troll: Troll(9)>, <Elf: Elf(10)>]
  99.     Elfs: [<Elf: Elf(4)>, <Elf: Elf(10)>]
  100.     Trolls: [<Troll: Troll(1)>, <Troll: Troll(6)>, <Troll: Troll(7)>, <Troll: Troll(9)>]
  101.     Humans: [<Human: Human(2)>, <Human: Human(3)>, <Human: Human(5)>, <Human: Human(8)>]
  102. '''
  103.  
  104. __all__ = ['polymorphic_manager']
  105.  
  106. from itertools import imap
  107. from django.db.models import Manager
  108. from django.db.models.signals import pre_init
  109. from django.core.exceptions import ImproperlyConfigured
  110.  
  111.  
  112. def polymorphic_manager(on):
  113.     '''Create a model Manager for accessing polymorphic Model instances.
  114.  
  115.     :param on: The field used to determine the real class of a model instance.
  116.     '''
  117.     # This creates a thin wrapper class around PolymorphicParentManager. There
  118.     # are two reasons for not using PolymorphicParentManager directly:
  119.     # 1. Preserve the __init__ signature. Regular Manager.__init__ doesn't take
  120.     #    arguments but PolymorphicParentManager has to take the polymorphic_on
  121.     #    field. This breaks code that attempts to subclass it and call the
  122.     #    super __init__.
  123.     # 2. Make all manager instances for this field share the same _id2model
  124.     #    mapping. This is necessary, for example, to support polymorphic "related
  125.     #    managers" at the other side of a ForeignKey or both sides of a
  126.     #    ManyToManyField.
  127.     parent = PolymorphicParentManager(on)
  128.     class PolymorphicManager(PolymorphicParentManager):
  129.         def __init__(self):
  130.             PolymorphicParentManager.__init__(self, on, parent._id2model)
  131.     return PolymorphicManager()
  132.  
  133.  
  134. class PolymorphicParentManager(Manager):
  135.     '''Polymorphic Manager for the parent Model of a hierarchy.
  136.  
  137.     All Manager methods that return model instances (``all``, ``iterator``,
  138.     ``get``, ``create``, ``get_or_create``, etc.) automatically downcast them to
  139.     the right class. Downcasting can be also done explicitly on any model
  140.     instance using the :meth:`downcast` method.
  141.     '''
  142.  
  143.     def __init__(self, on, id2model=None):
  144.         '''Instantiate a new PolymorphicParentManager.
  145.  
  146.         :param on: The field used to determine the real class of a model instance.
  147.         :param id2model: An optional mapping of each polymorphic identity to the
  148.             respective Model subclass.
  149.         '''
  150.         super(PolymorphicParentManager, self).__init__()
  151.         self._field = on
  152.         if id2model is None:
  153.             id2model = {}
  154.         self._id2model = id2model
  155.  
  156.     @property
  157.     def polymorphic_on(self):
  158.         '''The name of the field this manager is polymorphic on.'''
  159.         return self._field.name
  160.  
  161.     def polymorphic_identity(self, identity, autoinit=True):
  162.         '''Create a polymorphic Manager for the given ``identity``.
  163.  
  164.         :param identity: The value of the :attr:`polymorphic_on` field.
  165.         :param autoinit: If True (default), a ``pre_init`` signal handler is
  166.             connected to the Model of the newly created manager, that sets the
  167.             :attr:`polymorphic_on` field to ``identity`` (unless an explicit
  168.             identity is passed). Usually there is no reason to set this to False.
  169.         '''
  170.         return PolymorphicChildManager(self, identity, autoinit)
  171.  
  172.     def downcast(self, obj, _hit_db=True):
  173.         '''Return an instance having the real class of ``obj``.
  174.  
  175.         If ``obj`` is already an instance of the real class it is returned as
  176.         is, otherwise a new instance is returned.
  177.  
  178.         :param obj: A model instance.
  179.         :param _hit_db: Mainly for internal usage, if unsure leave it to True.
  180.             Long answer: If ``obj`` has a primary key and its real model class
  181.             is not a proxy, normally the database should be queried for it. In
  182.             case it is known in advance that ``obj`` is not in the database,
  183.             or if the full ``obj`` state is not important, pass ``_hit_db=False``
  184.             to save a database roundtrip.
  185.         '''
  186.         polymorphic_value = getattr(obj, self.polymorphic_on)
  187.         model = self._id2model.get(polymorphic_value, obj.__class__)
  188.         if model is obj.__class__: # or polymorphic value is unknown
  189.             return obj
  190.         if _hit_db and obj.pk is not None and not model._meta.proxy:
  191.             try:
  192.                 return model._default_manager.get(pk=obj.pk)
  193.             except model.DoesNotExist:
  194.                 pass
  195.         cast_obj = model(pk=obj.pk)
  196.         # XXX: dumping the whole obj.__dict__ as a way to copy the state is
  197.         # not foolproof but that's probably the best we can do
  198.         cast_obj.__dict__.update(obj.__dict__)
  199.         return cast_obj
  200.  
  201.     def get_query_set(self):
  202.         queryset = super(PolymorphicParentManager, self).get_query_set()
  203.         # blend the super queryset's class with the DowncastingQuerySetMixin
  204.         queryset_subclass = DowncastingQuerySetMixin._get_subclass_with(queryset.__class__)
  205.         # and return a clone of the queryset having the blended class
  206.         # also pass the downcast bound method required by DowncastingQuerySetMixin
  207.         return queryset._clone(klass=queryset_subclass, downcast=self.downcast)
  208.  
  209.  
  210. class PolymorphicChildManager(Manager):
  211.     '''Polymorphic manager for the children Models of a hierarchy.
  212.  
  213.     Querysets created by this manager are filtered to return only objects with
  214.     the polymorphic identity value of the manager.
  215.     '''
  216.  
  217.     def __init__(self, polymorphic_manager, identity, autoinit=True):
  218.         super(PolymorphicChildManager, self).__init__()
  219.         self._polymorphic_manager = polymorphic_manager
  220.         self._identity = identity
  221.         self._autoinit = autoinit
  222.  
  223.     def downcast(self, obj, _hit_db=True):
  224.         return self._polymorphic_manager.downcast(obj, _hit_db)
  225.     downcast.__doc__ = PolymorphicParentManager.downcast.__doc__
  226.  
  227.     def contribute_to_class(self, cls, name):
  228.         super(PolymorphicChildManager, self).contribute_to_class(cls, name)
  229.         polymorphic_on = self._polymorphic_manager.polymorphic_on
  230.         identity = self._identity
  231.         id2model = self._polymorphic_manager._id2model
  232.         if identity in id2model:
  233.              raise ImproperlyConfigured(
  234.                 'More than one subclasses with the same identity (%s.%s=%s)' %
  235.                 (self._polymorphic_manager.model.__name__, polymorphic_on, identity))
  236.         id2model[identity] = cls
  237.         if self._autoinit:
  238.             def preset_identity(sender, args, kwargs, **_):
  239.                 if polymorphic_on not in kwargs:
  240.                     kwargs[polymorphic_on] = identity
  241.             pre_init.connect(preset_identity, sender=cls, weak=False)
  242.  
  243.     def get_query_set(self):
  244.         cond = {self._polymorphic_manager.polymorphic_on: self._identity}
  245.         return super(PolymorphicChildManager, self).get_query_set().filter(**cond)
  246.  
  247.  
  248. class DowncastingQuerySetMixin(object):
  249.     '''Mixin class to be used along with a QuerySet class for automatic downcasting.
  250.  
  251.     Instances must have a ``downcast`` method with the signature of
  252.     :meth:`PolymorphicParentManager.downcast`.
  253.     '''
  254.  
  255.     def iterator(self):
  256.         return imap(self.downcast, super(DowncastingQuerySetMixin, self).iterator())
  257.  
  258.     def create(self, **kwargs):
  259.         # make a clone of this queryset but replace self.model with the real one
  260.         # we don't care about the full instance state, we just need the class
  261.         cast_obj = self.downcast(self.model(**kwargs), _hit_db=False)
  262.         clone = self._clone(model=cast_obj.__class__)
  263.         return super(DowncastingQuerySetMixin, clone).create(**kwargs)
  264.  
  265.     def get_or_create(self, **kwargs):
  266.         obj_created = super(DowncastingQuerySetMixin, self).get_or_create(**kwargs)
  267.         if obj_created[1]:
  268.             # the real-class object is not in the db, so don't hit it again
  269.             cast_obj = self.downcast(obj_created[0], _hit_db=False)
  270.             cast_obj.save(force_insert=True, using=self.db)
  271.             obj_created = cast_obj, obj_created[1]
  272.         # else get() has already downcast it; nothing else to do
  273.         return obj_created
  274.  
  275.     def _clone(self, **kwargs):
  276.         kwargs['downcast'] = self.downcast  # propagate the downcast callable
  277.         return super(DowncastingQuerySetMixin, self)._clone(**kwargs)
  278.  
  279.     # mapping of a Queryset (sub)class to a subclass of it with DowncastingQuerySetMixin
  280.     _cached_subclasses = {}
  281.  
  282.     @classmethod
  283.     def _get_subclass_with(cls, qset_cls):
  284.         if issubclass(qset_cls, cls):
  285.             return qset_cls     # already a DowncastingQuerySetMixin subclass
  286.         try:
  287.             return cls._cached_subclasses[qset_cls]
  288.         except KeyError:
  289.             sub_cls = type(cls.__name__ + qset_cls.__name__, (cls, qset_cls), {})
  290.             cls._cached_subclasses[qset_cls] = sub_cls
  291.             return sub_cls
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top