Allows ORM constructs to be sealed to prevent them from executing queries on attribute accesses.


License
MIT
Install
pip install django-seal==1.6.1

Documentation

django-seal

Seal
Build Status Coverage status

Django application providing queryset sealing capability to force appropriate usage of only()/defer() and select_related()/prefetch_related().

Installation

pip install django-seal

Usage

# models.py
from django.db import models
from seal.models import SealableModel

class Location(SealableModel):
    latitude = models.FloatField()
    longitude = models.FloatField()

class SeaLion(SealableModel):
    height = models.PositiveIntegerField()
    weight = models.PositiveIntegerField()
    location = models.ForeignKey(Location, models.CASCADE, null=True)
    previous_locations = models.ManyToManyField(Location, related_name='previous_visitors')

By default UnsealedAttributeAccess warnings will be raised on sealed objects attributes accesses

>>> location = Location.objects.create(latitude=51.585474, longitude=156.634331)
>>> sealion = SeaLion.objects.create(height=1, weight=100, location=location)
>>> sealion.previous_locations.add(location)
>>> SeaLion.objects.only('height').seal().get().weight
UnsealedAttributeAccess:: Attempt to fetch deferred field "weight" on sealed <SeaLion instance>.
>>> SeaLion.objects.seal().get().location
UnsealedAttributeAccess: Attempt to fetch related field "location" on sealed <SeaLion instance>.
>>> SeaLion.objects.seal().get().previous_locations.all()
UnsealedAttributeAccess: Attempt to fetch many-to-many field "previous_locations" on sealed <SeaLion instance>.

You can elevate the warnings to exceptions by filtering them. This is useful to assert no unsealed attribute accesses are performed when running your test suite for example.

>>> import warnings
>>> from seal.exceptions import UnsealedAttributeAccess
>>> warnings.filterwarnings('error', category=UnsealedAttributeAccess)
>>> SeaLion.objects.only('height').seal().get().weight
Traceback (most recent call last)
...
UnsealedAttributeAccess:: Attempt to fetch deferred field "weight" on sealed <SeaLion instance>.
>>> SeaLion.objects.seal().get().location
Traceback (most recent call last)
...
UnsealedAttributeAccess: Attempt to fetch related field "location" on sealed <SeaLion instance>.
>>> SeaLion.objects.seal().get().previous_locations.all()
Traceback (most recent call last)
...
UnsealedAttributeAccess: Attempt to fetch many-to-many field "previous_locations" on sealed <SeaLion instance>.

Or you can configure logging to capture warnings to log unsealed attribute accesses to the py.warnings logger which is a nice way to identify and address unsealed attributes accesses from production logs without taking your application down if some instances happen to slip through your battery of tests.

>>> import logging
>>> logging.captureWarnings(True)

Sealable managers can also be automatically sealed at model definition time to avoid having to call seal() systematically by passing seal=True to SealableModel subclasses, SealableManager and SealableQuerySet.as_manager.

from django.db import models
from seal.models import SealableManager, SealableModel, SealableQuerySet

class Location(SealableModel, seal=True):
    latitude = models.FloatField()
    longitude = models.FloatField()

class SeaLion(SealableModel):
    height = models.PositiveIntegerField()
    weight = models.PositiveIntegerField()
    location = models.ForeignKey(Location, models.CASCADE, null=True)
    previous_locations = models.ManyToManyField(Location, related_name='previous_visitors')

    objects = SealableManager(seal=True)
    others = SealableQuerySet.as_manager(seal=True)

Development

Make your changes, and then run tests via tox:

tox