Fixed #15648 -- Allowed QuerySet.values_list() to return a namedtuple.

This commit is contained in:
Sergey Fedoseev 2017-08-04 15:28:39 +05:00 committed by Tim Graham
parent a027447f56
commit f3c9562143
4 changed files with 101 additions and 5 deletions

View File

@ -6,8 +6,9 @@ import copy
import operator
import sys
import warnings
from collections import OrderedDict
from collections import OrderedDict, namedtuple
from contextlib import suppress
from functools import lru_cache
from itertools import chain
from django.conf import settings
@ -137,6 +138,34 @@ class ValuesListIterable(BaseIterable):
return compiler.results_iter(tuple_expected=True)
class NamedValuesListIterable(ValuesListIterable):
"""
Iterable returned by QuerySet.values_list(named=True) that yields a
namedtuple for each row.
"""
@staticmethod
@lru_cache()
def create_namedtuple_class(*names):
# Cache namedtuple() with @lru_cache() since it's too slow to be
# called for every QuerySet evaluation.
return namedtuple('Row', names)
def __iter__(self):
queryset = self.queryset
if queryset._fields:
names = queryset._fields
else:
query = queryset.query
names = list(query.extra_select)
names.extend(query.values_select)
names.extend(query.annotation_select)
tuple_class = self.create_namedtuple_class(*names)
new = tuple.__new__
for row in super().__iter__():
yield new(tuple_class, row)
class FlatValuesListIterable(BaseIterable):
"""
Iterable returned by QuerySet.values_list(flat=True) that yields single
@ -712,22 +741,35 @@ class QuerySet:
clone._iterable_class = ValuesIterable
return clone
def values_list(self, *fields, flat=False):
def values_list(self, *fields, flat=False, named=False):
if flat and named:
raise TypeError("'flat' and 'named' can't be used together.")
if flat and len(fields) > 1:
raise TypeError("'flat' is not valid when values_list is called with more than one field.")
field_names = {f for f in fields if not hasattr(f, 'resolve_expression')}
_fields = []
expressions = {}
counter = 1
for field in fields:
if hasattr(field, 'resolve_expression'):
field_id = str(id(field))
field_id_prefix = getattr(field, 'default_alias', field.__class__.__name__.lower())
while True:
field_id = field_id_prefix + str(counter)
counter += 1
if field_id not in field_names:
break
expressions[field_id] = field
_fields.append(field_id)
else:
_fields.append(field)
clone = self._values(*_fields, **expressions)
clone._iterable_class = FlatValuesListIterable if flat else ValuesListIterable
clone._iterable_class = (
NamedValuesListIterable if named
else FlatValuesListIterable if flat
else ValuesListIterable
)
return clone
def dates(self, field_name, kind, order='ASC'):

View File

@ -626,7 +626,7 @@ You can also refer to fields on related models with reverse relations through
``values_list()``
~~~~~~~~~~~~~~~~~
.. method:: values_list(*fields, flat=False)
.. method:: values_list(*fields, flat=False, named=False)
This is similar to ``values()`` except that instead of returning dictionaries,
it returns tuples when iterated over. Each tuple contains the value from the
@ -651,6 +651,15 @@ rather than one-tuples. An example should make the difference clearer::
It is an error to pass in ``flat`` when there is more than one field.
You can pass ``named=True`` to get results as a
:func:`~python:collections.namedtuple`::
>>> Entry.objects.values_list('id', 'headline', named=True)
<QuerySet [Row(id=1, headline='First entry'), ...]>
Using a named tuple may make use of the results more readable, at the expense
of a small performance penalty for transforming the results into a named tuple.
If you don't pass any values to ``values_list()``, it will return all the
fields in the model, in the order they were declared.
@ -688,6 +697,10 @@ not having any author::
Support for expressions in ``*fields`` was added.
.. versionchanged:: 2.0
The ``named`` parameter was added.
``dates()``
~~~~~~~~~~~

View File

@ -291,6 +291,9 @@ Models
* Added support for expressions in :attr:`Meta.ordering
<django.db.models.Options.ordering>`.
* The new ``named`` parameter of :meth:`.QuerySet.values_list` allows fetching
results as named tuples.
Pagination
~~~~~~~~~~

View File

@ -2248,6 +2248,44 @@ class ValuesQuerysetTests(TestCase):
with self.assertRaisesMessage(FieldError, msg):
Tag.objects.values_list('name__foo')
def test_named_values_list_flat(self):
msg = "'flat' and 'named' can't be used together."
with self.assertRaisesMessage(TypeError, msg):
Number.objects.values_list('num', flat=True, named=True)
def test_named_values_list_bad_field_name(self):
msg = "Type names and field names must be valid identifiers: '1'"
with self.assertRaisesMessage(ValueError, msg):
Number.objects.extra(select={'1': 'num+1'}).values_list('1', named=True).first()
def test_named_values_list_with_fields(self):
qs = Number.objects.extra(select={'num2': 'num+1'}).annotate(Count('id'))
values = qs.values_list('num', 'num2', named=True).first()
self.assertEqual(type(values).__name__, 'Row')
self.assertEqual(values._fields, ('num', 'num2'))
self.assertEqual(values.num, 72)
self.assertEqual(values.num2, 73)
def test_named_values_list_without_fields(self):
qs = Number.objects.extra(select={'num2': 'num+1'}).annotate(Count('id'))
values = qs.values_list(named=True).first()
self.assertEqual(type(values).__name__, 'Row')
self.assertEqual(values._fields, ('num2', 'id', 'num', 'id__count'))
self.assertEqual(values.num, 72)
self.assertEqual(values.num2, 73)
self.assertEqual(values.id__count, 1)
def test_named_values_list_expression_with_default_alias(self):
expr = Count('id')
values = Number.objects.annotate(id__count1=expr).values_list(expr, 'id__count1', named=True).first()
self.assertEqual(values._fields, ('id__count2', 'id__count1'))
def test_named_values_list_expression(self):
expr = F('num') + 1
qs = Number.objects.annotate(combinedexpression1=expr).values_list(expr, 'combinedexpression1', named=True)
values = qs.first()
self.assertEqual(values._fields, ('combinedexpression2', 'combinedexpression1'))
class QuerySetSupportsPythonIdioms(TestCase):