mirror of https://github.com/django/django.git
Fixed #15648 -- Allowed QuerySet.values_list() to return a namedtuple.
This commit is contained in:
parent
a027447f56
commit
f3c9562143
|
@ -6,8 +6,9 @@ import copy
|
||||||
import operator
|
import operator
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict, namedtuple
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from functools import lru_cache
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -137,6 +138,34 @@ class ValuesListIterable(BaseIterable):
|
||||||
return compiler.results_iter(tuple_expected=True)
|
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):
|
class FlatValuesListIterable(BaseIterable):
|
||||||
"""
|
"""
|
||||||
Iterable returned by QuerySet.values_list(flat=True) that yields single
|
Iterable returned by QuerySet.values_list(flat=True) that yields single
|
||||||
|
@ -712,22 +741,35 @@ class QuerySet:
|
||||||
clone._iterable_class = ValuesIterable
|
clone._iterable_class = ValuesIterable
|
||||||
return clone
|
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:
|
if flat and len(fields) > 1:
|
||||||
raise TypeError("'flat' is not valid when values_list is called with more than one field.")
|
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 = []
|
_fields = []
|
||||||
expressions = {}
|
expressions = {}
|
||||||
|
counter = 1
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if hasattr(field, 'resolve_expression'):
|
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
|
expressions[field_id] = field
|
||||||
_fields.append(field_id)
|
_fields.append(field_id)
|
||||||
else:
|
else:
|
||||||
_fields.append(field)
|
_fields.append(field)
|
||||||
|
|
||||||
clone = self._values(*_fields, **expressions)
|
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
|
return clone
|
||||||
|
|
||||||
def dates(self, field_name, kind, order='ASC'):
|
def dates(self, field_name, kind, order='ASC'):
|
||||||
|
|
|
@ -626,7 +626,7 @@ You can also refer to fields on related models with reverse relations through
|
||||||
``values_list()``
|
``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,
|
This is similar to ``values()`` except that instead of returning dictionaries,
|
||||||
it returns tuples when iterated over. Each tuple contains the value from the
|
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.
|
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
|
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.
|
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.
|
Support for expressions in ``*fields`` was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
|
||||||
|
The ``named`` parameter was added.
|
||||||
|
|
||||||
``dates()``
|
``dates()``
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -291,6 +291,9 @@ Models
|
||||||
* Added support for expressions in :attr:`Meta.ordering
|
* Added support for expressions in :attr:`Meta.ordering
|
||||||
<django.db.models.Options.ordering>`.
|
<django.db.models.Options.ordering>`.
|
||||||
|
|
||||||
|
* The new ``named`` parameter of :meth:`.QuerySet.values_list` allows fetching
|
||||||
|
results as named tuples.
|
||||||
|
|
||||||
Pagination
|
Pagination
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -2248,6 +2248,44 @@ class ValuesQuerysetTests(TestCase):
|
||||||
with self.assertRaisesMessage(FieldError, msg):
|
with self.assertRaisesMessage(FieldError, msg):
|
||||||
Tag.objects.values_list('name__foo')
|
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):
|
class QuerySetSupportsPythonIdioms(TestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue