-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simply and re-organize the process of making field descriptors sealable.
This avoids the usage of metaclasses and remove a ton of boilerplate.
- Loading branch information
Showing
5 changed files
with
111 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,73 @@ | ||
from django.contrib.contenttypes.fields import GenericForeignKey | ||
from __future__ import unicode_literals | ||
|
||
from django.db import models | ||
from django.utils.six import with_metaclass | ||
from django.db.models.fields.related import lazy_related_operation | ||
from django.dispatch import receiver | ||
|
||
from .exceptions import SealedObject | ||
from .descriptors import sealable_descriptor_classes | ||
from .managers import SealableQuerySet | ||
from .related import ( | ||
SealableGenericForeignKeyProxy, create_sealable_m2m_contribute_to_class, | ||
create_sealable_m2m_contribute_to_related_class, sealable_accessor_classes, | ||
) | ||
|
||
|
||
class SealaleModelBase(models.base.ModelBase): | ||
def __new__(cls, name, bases, attrs): | ||
meta = attrs.get('Meta') | ||
abstract = meta and getattr(meta, 'abstract', False) | ||
proxy = meta and getattr(meta, 'proxy', False) | ||
if not (abstract or proxy): | ||
# Turn implicit one-to-one parent link into explicit ones in order | ||
# to replace their related accessors below. | ||
concrete_bases = ( | ||
base for base in bases | ||
if isinstance(base, models.base.ModelBase) and not base._meta.abstract | ||
) | ||
parent_links = { | ||
models.utils.make_model_tuple(value.remote_field.model) for value in attrs.values() | ||
if isinstance(value, models.OneToOneField) and value.remote_field.parent_link | ||
} | ||
for base in concrete_bases: | ||
if models.utils.make_model_tuple(base) in parent_links: | ||
continue | ||
parent_link_name = '%s_ptr' % base._meta.model_name | ||
attrs[parent_link_name] = models.OneToOneField( | ||
base, | ||
on_delete=models.CASCADE, | ||
name=parent_link_name, | ||
auto_created=True, | ||
parent_link=True, | ||
) | ||
for attr, value in attrs.items(): | ||
if isinstance(value, models.ForeignObject): | ||
sealable_accessor_class = sealable_accessor_classes.get(value.related_accessor_class) | ||
if sealable_accessor_class: | ||
value.related_accessor_class = sealable_accessor_class | ||
sealable_forward_accessor_class = sealable_accessor_classes.get(value.forward_related_accessor_class) | ||
if sealable_forward_accessor_class: | ||
value.forward_related_accessor_class = sealable_forward_accessor_class | ||
elif isinstance(value, models.ManyToManyField): | ||
# ManyToManyField doesn't declare a class level attribute for | ||
# its forward and reverse accessor class. We must provide | ||
# override contribute_to_class/contribute_to_related_class to | ||
# work around it. | ||
value.contribute_to_class = create_sealable_m2m_contribute_to_class(value) | ||
value.contribute_to_related_class = create_sealable_m2m_contribute_to_related_class(value) | ||
elif isinstance(value, GenericForeignKey): | ||
attrs[attr] = SealableGenericForeignKeyProxy(value) | ||
return super(SealaleModelBase, cls).__new__(cls, name, bases, attrs) | ||
|
||
class SealableModel(models.Model): | ||
""" | ||
Abstract model class that turns deferred and related fields accesses that | ||
would incur a database query into exceptions once sealed. | ||
""" | ||
|
||
class SealableModel(with_metaclass(SealaleModelBase, models.Model)): | ||
objects = SealableQuerySet.as_manager() | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
def seal(self): | ||
""" | ||
Seal the instance to turn deferred and related fields access that would | ||
required fetching from the database into exceptions. | ||
""" | ||
self._state.sealed = True | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
def refresh_from_db(self, using=None, fields=None): | ||
sealed = getattr(self._state, 'sealed', False) | ||
if sealed and fields is not None: | ||
fields = set(fields) | ||
deferred_fields = self.get_deferred_fields() | ||
if fields.intersection(deferred_fields): | ||
raise SealedObject('Cannot fetch deferred fields %s on a sealed object.' % ','.join(sorted(fields))) | ||
super(SealableModel, self).refresh_from_db(using=using, fields=fields) | ||
def make_descriptor_sealable(model, attname): | ||
""" | ||
Make a descriptor sealable if a sealable class is defined. | ||
""" | ||
try: | ||
descriptor = getattr(model, attname) | ||
except AttributeError: | ||
# Handle hidden reverse accessor case. e.g. related_name='+' | ||
return | ||
sealable_descriptor_class = sealable_descriptor_classes.get(descriptor.__class__) | ||
if sealable_descriptor_class: | ||
descriptor.__class__ = sealable_descriptor_class | ||
|
||
|
||
def make_remote_field_descriptor_sealable(model, related_model, remote_field): | ||
""" | ||
Make a remote field descriptor sealable if a sealable class is defined. | ||
""" | ||
if not issubclass(related_model, SealableModel): | ||
return | ||
accessor_name = remote_field.get_accessor_name() | ||
make_descriptor_sealable(related_model, accessor_name) | ||
|
||
|
||
@receiver(models.signals.class_prepared) | ||
def make_field_descriptors_sealable(sender, **kwargs): | ||
""" | ||
Replace SealableModel subclasses forward and reverse fields descriptors | ||
by sealable ones. | ||
""" | ||
if not issubclass(sender, SealableModel): | ||
return | ||
opts = sender._meta | ||
if opts.abstract or opts.proxy: | ||
return | ||
for field in (opts.local_fields + opts.local_many_to_many + opts.private_fields): | ||
make_descriptor_sealable(sender, field.name) | ||
remote_field = field.remote_field | ||
if remote_field: | ||
# Use lazy_related_operation because lazy relationships might not | ||
# be resolved yet. | ||
lazy_related_operation( | ||
make_remote_field_descriptor_sealable, sender, remote_field.model, remote_field=remote_field | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters