From 981a072dd4dec586f8fc606712ed9a2ef116eeee Mon Sep 17 00:00:00 2001 From: Kwist Date: Mon, 31 Aug 2020 11:54:34 +0300 Subject: [PATCH] Fixed #31843 -- Fixed pickling named values from QuerySet.values_list(). --- AUTHORS | 1 + django/db/models/query.py | 13 ++----------- django/db/models/utils.py | 18 ++++++++++++++++++ tests/queries/tests.py | 5 +++++ 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7e316cd739..81858e7b29 100644 --- a/AUTHORS +++ b/AUTHORS @@ -934,6 +934,7 @@ answer newbie questions, and generally made Django that much better: Vincent Foley Vinny Do Vitaly Babiy + Vitaliy Yelnik Vladimir Kuzma Vlado Vsevolod Solovyov diff --git a/django/db/models/query.py b/django/db/models/query.py index 67ffe7f000..6c78fbc4b3 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -5,8 +5,6 @@ The main QuerySet implementation. This provides the public API for the ORM. import copy import operator import warnings -from collections import namedtuple -from functools import lru_cache from itertools import chain import django @@ -23,7 +21,7 @@ from django.db.models.expressions import Case, Expression, F, Value, When from django.db.models.functions import Cast, Trunc from django.db.models.query_utils import FilteredRelation, Q from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE -from django.db.models.utils import resolve_callables +from django.db.models.utils import create_namedtuple_class, resolve_callables from django.utils import timezone from django.utils.functional import cached_property, partition @@ -148,13 +146,6 @@ class NamedValuesListIterable(ValuesListIterable): 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: @@ -162,7 +153,7 @@ class NamedValuesListIterable(ValuesListIterable): else: query = queryset.query names = [*query.extra_select, *query.values_select, *query.annotation_select] - tuple_class = self.create_namedtuple_class(*names) + tuple_class = create_namedtuple_class(*names) new = tuple.__new__ for row in super().__iter__(): yield new(tuple_class, row) diff --git a/django/db/models/utils.py b/django/db/models/utils.py index 989667dc8c..764ca5888b 100644 --- a/django/db/models/utils.py +++ b/django/db/models/utils.py @@ -1,3 +1,7 @@ +import functools +from collections import namedtuple + + def make_model_tuple(model): """ Take a model or a string of the form "app_label.ModelName" and return a @@ -28,3 +32,17 @@ def resolve_callables(mapping): """ for k, v in mapping.items(): yield k, v() if callable(v) else v + + +def unpickle_named_row(names, values): + return create_namedtuple_class(*names)(*values) + + +@functools.lru_cache() +def create_namedtuple_class(*names): + # Cache type() with @lru_cache() since it's too slow to be called for every + # QuerySet evaluation. + def __reduce__(self): + return unpickle_named_row, (names, tuple(self)) + + return type('Row', (namedtuple('Row', names),), {'__reduce__': __reduce__}) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 428153402f..5b667186a3 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -2408,6 +2408,11 @@ class ValuesQuerysetTests(TestCase): values = qs.first() self.assertEqual(values._fields, ('combinedexpression2', 'combinedexpression1')) + def test_named_values_pickle(self): + value = Number.objects.values_list('num', 'other_num', named=True).get() + self.assertEqual(value, (72, None)) + self.assertEqual(pickle.loads(pickle.dumps(value)), value) + class QuerySetSupportsPythonIdioms(TestCase):