Guest User

Untitled

a guest
Jun 13th, 2018
98
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.51 KB | None | 0 0
  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
Add Comment
Please, Sign In to add comment