import warnings from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import Prefetch from django.db.models.query import ModelIterable from django.test import SimpleTestCase, TestCase from django.test.utils import isolate_apps from seal.descriptors import _SealedRelatedQuerySet from seal.exceptions import UnsealedAttributeAccess from seal.query import SealableQuerySet, SealedModelIterable from .models import ( Climate, GreatSeaLion, Island, Leak, Location, Nickname, SeaGull, SeaLion, ) class SealableQuerySetTests(TestCase): @classmethod def setUpTestData(cls): cls.location = Location.objects.create(latitude=51.585474, longitude=156.634331) cls.climate = Climate.objects.create(temperature=100) cls.location.climates.add(cls.climate) cls.leak = Leak.objects.create(description='Salt water') cls.great_sealion = GreatSeaLion.objects.create( height=1, weight=100, location=cls.location, leak=cls.leak, leak_o2o=cls.leak, ) cls.sealion = cls.great_sealion.sealion_ptr cls.sealion.previous_locations.add(cls.location) cls.gull = SeaGull.objects.create(sealion=cls.sealion) cls.nickname = Nickname.objects.create(name='Jonathan Livingston', content_object=cls.gull) tests_models = tuple(apps.get_app_config('tests').get_models()) ContentType.objects.get_for_models(*tests_models, for_concrete_models=True) def setUp(self): warnings.filterwarnings('error', category=UnsealedAttributeAccess) self.addCleanup(warnings.resetwarnings) def test_state_sealed_assigned(self): instance = SeaLion.objects.seal().get() self.assertTrue(instance._state.sealed) def test_sealed_deferred_field(self): instance = SeaLion.objects.seal().defer('weight').get() message = 'Attempt to fetch deferred field "weight" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.weight def test_not_sealed_deferred_field(self): instance = SeaLion.objects.defer('weight').get() self.assertEqual(instance.weight, 100) def test_sealed_foreign_key(self): instance = SeaLion.objects.seal().get() message = 'Attempt to fetch related field "location" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.location def test_not_sealed_foreign_key(self): instance = SeaLion.objects.get() self.assertEqual(instance.location, self.location) def test_sealed_select_related_foreign_key(self): instance = SeaLion.objects.select_related('location').seal().get() self.assertEqual(instance.location, self.location) instance = SeaGull.objects.select_related('sealion').seal().get() message = 'Attempt to fetch related field "location" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.sealion.location instance = SeaGull.objects.select_related('sealion__location').seal().get() self.assertEqual(instance.sealion.location, self.location) def test_sealed_select_related_none_foreign_key(self): SeaLion.objects.update(location=None) instance = SeaLion.objects.select_related('location').seal().get() self.assertIsNone(instance.location) SeaGull.objects.update(sealion=None) instance = SeaGull.objects.select_related('sealion__location').seal().get() self.assertIsNone(instance.sealion) def test_select_related_foreign_key_leak(self): instance = SeaLion.objects.get() self.assertEqual(instance.leak.description, self.leak.description) instance = SeaLion.objects.select_related('leak').get() self.assertEqual(instance.leak.description, self.leak.description) def test_select_related_foreign_key_leak_o2o(self): instance = SeaLion.objects.get() self.assertEqual(instance.leak_o2o.description, self.leak.description) instance = SeaLion.objects.select_related('leak_o2o').get() self.assertEqual(instance.leak_o2o.description, self.leak.description) def test_sealed_select_related_foreign_key_leak(self): instance = SeaLion.objects.select_related('leak').defer('leak__description').seal().get() with self.assertNumQueries(1): self.assertEqual(instance.leak.description, self.leak.description) def test_sealed_select_related_foreign_key_leak_o2o(self): instance = SeaLion.objects.select_related('leak_o2o').defer('leak_o2o__description').seal().get() with self.assertNumQueries(1): self.assertEqual(instance.leak_o2o.description, self.leak.description) def test_sealed_select_related_deferred_field(self): instance = SeaGull.objects.select_related( 'sealion__location', ).only('sealion__location__latitude').seal().get() self.assertEqual(instance.sealion.location, self.location) self.assertEqual(instance.sealion.location.latitude, self.location.latitude) message = 'Attempt to fetch deferred field "longitude" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.sealion.location.longitude def test_sealed_one_to_one(self): instance = SeaGull.objects.seal().get() message = 'Attempt to fetch related field "sealion" on sealed <SeaGull instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.sealion def test_not_sealed_one_to_one(self): instance = SeaGull.objects.get() self.assertEqual(instance.sealion, self.sealion) def test_sealed_select_related_one_to_one(self): instance = SeaGull.objects.select_related('sealion').seal().get() self.assertEqual(instance.sealion, self.sealion) def test_sealed_select_related_reverse_one_to_one(self): instance = SeaLion.objects.select_related('gull').seal().get() self.assertEqual(instance.gull, self.gull) self.gull.sealion = None self.gull.save(update_fields={'sealion'}) instance = SeaLion.objects.select_related('gull').seal().get() with self.assertRaises(SeaLion.gull.RelatedObjectDoesNotExist): instance.gull def test_sealed_many_to_many(self): instance = SeaLion.objects.seal().get() message = 'Attempt to fetch many-to-many field "previous_locations" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_locations.all()) with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.previous_locations.all()[0] def test_sealed_many_to_many_queryset(self): instance = SeaLion.objects.seal().get() self.assertEqual(instance.previous_locations.get(pk=self.location.pk), self.location) self.assertFalse( isinstance(instance.previous_locations.filter(pk=self.location.pk), _SealedRelatedQuerySet) ) def test_not_sealed_many_to_many(self): instance = SeaLion.objects.get() self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) def test_sealed_string_prefetched_many_to_many(self): instance = SeaLion.objects.prefetch_related('previous_locations').seal().get() with self.assertNumQueries(0): self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) instance = instance.previous_locations.all()[0] message = 'Attempt to fetch many-to-many field "previous_visitors" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_visitors.all()) def test_sealed_prefetch_prefetched_many_to_many(self): instance = SeaLion.objects.prefetch_related( Prefetch('previous_locations'), ).seal().get() with self.assertNumQueries(0): self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) instance = instance.previous_locations.all()[0] message = 'Attempt to fetch many-to-many field "previous_visitors" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_visitors.all()) def test_sealed_prefetch_queryset_prefetched_many_to_many(self): instance = SeaLion.objects.prefetch_related( Prefetch('previous_locations', Location.objects.all()), ).seal().get() with self.assertNumQueries(0): self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) instance = instance.previous_locations.all()[0] message = 'Attempt to fetch many-to-many field "previous_visitors" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_visitors.all()) def test_sealed_string_prefetched_nested_many_to_many(self): instance = SeaLion.objects.prefetch_related('previous_locations__previous_visitors').seal().get() with self.assertNumQueries(0): self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) self.assertSequenceEqual( instance.previous_locations.all()[0].previous_visitors.all(), [self.sealion] ) instance = instance.previous_locations.all()[0].previous_visitors.all()[0] message = 'Attempt to fetch many-to-many field "previous_locations" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_locations.all()) def test_sealed_prefetch_prefetched_nested_many_to_many(self): instance = SeaLion.objects.prefetch_related( Prefetch('previous_locations__previous_visitors'), ).seal().get() with self.assertNumQueries(0): self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) self.assertSequenceEqual( instance.previous_locations.all()[0].previous_visitors.all(), [self.sealion] ) instance = instance.previous_locations.all()[0].previous_visitors.all()[0] message = 'Attempt to fetch many-to-many field "previous_locations" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_locations.all()) def test_prefetched_sealed_many_to_many(self): instance = SeaLion.objects.prefetch_related( Prefetch('previous_locations', Location.objects.seal()), ).get() with self.assertNumQueries(0): self.assertSequenceEqual(instance.previous_locations.all(), [self.location]) message = 'Attempt to fetch many-to-many field "previous_visitors" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_locations.all()[0].previous_visitors.all()) def test_sealed_deferred_parent_link(self): instance = GreatSeaLion.objects.only('pk').seal().get() message = 'Attempt to fetch related field "sealion_ptr" on sealed <GreatSeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.sealion_ptr def test_not_sealed_parent_link(self): instance = GreatSeaLion.objects.only('pk').get() self.assertEqual(instance.sealion_ptr, self.sealion) def test_sealed_parent_link(self): instance = GreatSeaLion.objects.seal().get() with self.assertNumQueries(0): self.assertEqual(instance.sealion_ptr, self.sealion) def test_sealed_generic_foreign_key(self): instance = Nickname.objects.seal().get() message = 'Attempt to fetch related field "content_object" on sealed <Nickname instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.content_object def test_not_sealed_generic_foreign_key(self): instance = Nickname.objects.get() self.assertEqual(instance.content_object, self.gull) def test_sealed_prefetch_related_generic_foreign_key(self): instance = Nickname.objects.prefetch_related('content_object').seal().get() with self.assertNumQueries(0): self.assertEqual(instance.content_object, self.gull) def test_sealed_reverse_foreign_key(self): instance = Location.objects.seal().get() message = 'Attempt to fetch many-to-many field "visitors" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.visitors.all()) def test_not_sealed_reverse_foreign_key(self): instance = Location.objects.get() self.assertSequenceEqual(instance.visitors.all(), [self.sealion]) def test_sealed_prefetched_reverse_foreign_key(self): instance = Location.objects.prefetch_related('visitors').seal().get() self.assertSequenceEqual(instance.visitors.all(), [self.sealion]) def test_sealed_reverse_parent_link(self): instance = SeaLion.objects.seal().get() message = 'Attempt to fetch related field "greatsealion" on sealed <SeaLion instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.greatsealion def test_not_sealed_reverse_parent_link(self): instance = SeaLion.objects.get() self.assertEqual(instance.greatsealion, self.great_sealion) def test_sealed_select_related_reverse_parent_link(self): instance = SeaLion.objects.select_related('greatsealion').seal().get() self.assertEqual(instance.greatsealion, self.great_sealion) def test_sealed_reverse_many_to_many(self): instance = Location.objects.seal().get() message = 'Attempt to fetch many-to-many field "previous_visitors" on sealed <Location instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.previous_visitors.all()) with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.previous_visitors.all()[0] def test_sealed_reverse_many_to_many_queryset(self): instance = Location.objects.seal().get() self.assertEqual(instance.previous_visitors.get(pk=self.sealion.pk), self.sealion) self.assertFalse( isinstance(instance.previous_visitors.filter(pk=self.sealion.pk), _SealedRelatedQuerySet) ) def test_not_reverse_sealed_many_to_many(self): instance = Location.objects.get() self.assertSequenceEqual(instance.previous_visitors.all(), [self.sealion]) def test_sealed_prefetched_reverse_many_to_many(self): instance = Location.objects.prefetch_related('previous_visitors').seal().get() self.assertSequenceEqual(instance.previous_visitors.all(), [self.sealion]) def test_sealed_generic_relation(self): instance = SeaGull.objects.seal().get() message = 'Attempt to fetch many-to-many field "nicknames" on sealed <SeaGull instance>' with self.assertRaisesMessage(UnsealedAttributeAccess, message): list(instance.nicknames.all()) with self.assertRaisesMessage(UnsealedAttributeAccess, message): instance.nicknames.all()[0] def test_not_sealed_generic_relation(self): instance = SeaGull.objects.get() self.assertSequenceEqual(instance.nicknames.all(), [self.nickname]) def test_sealed_prefetched_generic_relation(self): instance = SeaGull.objects.prefetch_related('nicknames').seal().get() self.assertSequenceEqual(instance.nicknames.all(), [self.nickname]) def test_sealed_prefetched_select_related_many_to_many(self): instance = SeaLion.objects.select_related( 'location', ).prefetch_related( 'location__climates', ).seal().get() self.assertSequenceEqual(instance.location.climates.all(), [self.climate]) def test_prefetch_without_related_name(self): island = Island.objects.create(location=self.location) location = Location.objects.prefetch_related('island_set').seal().get() self.assertSequenceEqual(location.island_set.all(), [island]) class SealableQuerySetInteractionTests(SimpleTestCase): def test_sealed_select_related_disallowed(self): with self.assertRaisesMessage(TypeError, 'Cannot call select_related() after .seal()'): SeaGull.objects.seal().select_related() def test_sealed_prefetch_related_disallowed(self): with self.assertRaisesMessage(TypeError, 'Cannot call prefetch_related() after .seal()'): SeaGull.objects.seal().prefetch_related() def test_values_seal_disallowed(self): with self.assertRaisesMessage(TypeError, 'Cannot call seal() after .values() or .values_list()'): SeaGull.objects.values('id').seal() def test_values_list_seal_disallowed(self): with self.assertRaisesMessage(TypeError, 'Cannot call seal() after .values() or .values_list()'): SeaGull.objects.values_list('id').seal() def test_seal_sealable_model_iterable_subclass(self): class SealableModelIterableSubclass(SealedModelIterable): pass queryset = SeaGull.objects.seal(iterable_class=SealableModelIterableSubclass) self.assertIs(queryset._iterable_class, SealableModelIterableSubclass) def test_seal_non_sealable_model_iterable_subclass(self): message = ( "iterable_class <class 'django.db.models.query.ModelIterable'> is not a subclass of SealedModelIterable" ) with self.assertRaisesMessage(TypeError, message): SeaGull.objects.seal(iterable_class=ModelIterable) class SealableQuerySetNonSealableModelTests(TestCase): """ A SealableQuerySet should be usuable on non SealableModel subclasses. """ @classmethod def setUpTestData(cls): cls.location = Location.objects.create(latitude=51.585474, longitude=156.634331) cls.climate = Climate.objects.create(temperature=100) cls.location.climates.add(cls.climate) cls.sealion = SeaLion.objects.create(height=1, weight=100, location=cls.location) @isolate_apps('tests') def test_sealed_non_sealable_model(self): class NonSealableLocation(models.Model): class Meta: db_table = Location._meta.db_table queryset = SealableQuerySet(model=NonSealableLocation) instance = queryset.seal().get() self.assertTrue(instance._state.sealed) @isolate_apps('tests') def test_sealed_select_related_non_sealable_model(self): class NonSealableLocation(models.Model): class Meta: db_table = Location._meta.db_table class NonSealableSeaLion(models.Model): location = models.ForeignKey(NonSealableLocation, models.CASCADE) class Meta: db_table = SeaLion._meta.db_table queryset = SealableQuerySet(model=NonSealableSeaLion) instance = queryset.select_related('location').seal().get() self.assertTrue(instance._state.sealed) self.assertTrue(instance.location._state.sealed) @isolate_apps('tests') def test_sealed_prefetch_related_non_sealable_model(self): class NonSealableClimate(models.Model): objects = SealableQuerySet.as_manager() class Meta: db_table = Climate._meta.db_table class NonSealableLocationClimatesThrough(models.Model): climate = models.ForeignKey(NonSealableClimate, models.CASCADE) location = models.ForeignKey('NonSealableLocation', models.CASCADE) class Meta: db_table = Location.climates.through._meta.db_table class NonSealableLocation(models.Model): climates = models.ManyToManyField(NonSealableClimate, through=NonSealableLocationClimatesThrough) class Meta: db_table = Location._meta.db_table queryset = SealableQuerySet(model=NonSealableLocation) instance = queryset.prefetch_related('climates').seal().get() self.assertTrue(instance._state.sealed) with self.assertNumQueries(0): self.assertTrue(instance.climates.all()[0]._state.sealed)