diff --git a/django/db/models/query.py b/django/db/models/query.py index be5892a371..9371aa14e0 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -516,21 +516,19 @@ class QuerySet(object): """ Returns the first object of a query, returns None if no match is found. """ - qs = self if self.ordered else self.order_by('pk') - try: - return qs[0] - except IndexError: - return None + objects = list((self if self.ordered else self.order_by('pk'))[:1]) + if objects: + return objects[0] + return None def last(self): """ Returns the last object of a query, returns None if no match is found. """ - qs = self.reverse() if self.ordered else self.order_by('-pk') - try: - return qs[0] - except IndexError: - return None + objects = list((self.reverse() if self.ordered else self.order_by('-pk'))[:1]) + if objects: + return objects[0] + return None def in_bulk(self, id_list): """ diff --git a/tests/get_earliest_or_latest/models.py b/tests/get_earliest_or_latest/models.py index cd62cf0104..55096f5a2c 100644 --- a/tests/get_earliest_or_latest/models.py +++ b/tests/get_earliest_or_latest/models.py @@ -14,3 +14,18 @@ class Person(models.Model): name = models.CharField(max_length=30) birthday = models.DateField() # Note that this model doesn't have "get_latest_by" set. + + +# Ticket #23555 - model with an intentionally broken QuerySet.__iter__ method. + +class IndexErrorQuerySet(models.QuerySet): + """ + Emulates the case when some internal code raises an unexpected + IndexError. + """ + def __iter__(self): + raise IndexError + + +class IndexErrorArticle(Article): + objects = IndexErrorQuerySet.as_manager() diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py index 742c70bc41..de3307b1a6 100644 --- a/tests/get_earliest_or_latest/tests.py +++ b/tests/get_earliest_or_latest/tests.py @@ -4,7 +4,7 @@ from datetime import datetime from django.test import TestCase -from .models import Article, Person +from .models import Article, Person, IndexErrorArticle class EarliestOrLatestTests(TestCase): @@ -122,6 +122,9 @@ class EarliestOrLatestTests(TestCase): self.assertRaises(AssertionError, Person.objects.latest) self.assertEqual(Person.objects.latest("birthday"), p2) + +class TestFirstLast(TestCase): + def test_first(self): p1 = Person.objects.create(name="Bob", birthday=datetime(1950, 1, 1)) p2 = Person.objects.create(name="Alice", birthday=datetime(1961, 2, 3)) @@ -152,3 +155,24 @@ class EarliestOrLatestTests(TestCase): self.assertIs( Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).last(), None) + + def test_index_error_not_suppressed(self): + """ + #23555 -- Unexpected IndexError exceptions in QuerySet iteration + shouldn't be suppressed. + """ + def check(): + # We know that we've broken the __iter__ method, so the queryset + # should always raise an exception. + self.assertRaises(IndexError, lambda: IndexErrorArticle.objects.all()[0]) + self.assertRaises(IndexError, IndexErrorArticle.objects.all().first) + self.assertRaises(IndexError, IndexErrorArticle.objects.all().last) + + check() + + # And it does not matter if there are any records in the DB. + IndexErrorArticle.objects.create( + headline="Article 1", pub_date=datetime(2005, 7, 26), + expire_date=datetime(2005, 9, 1) + ) + check()