django-nested-values 0.6.14__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/PKG-INFO +1 -1
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/django_nested_values/__init__.py +2 -3
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/django_nested_values/queryset.py +179 -295
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/pyproject.toml +1 -1
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/.gitignore +0 -0
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/LICENSE +0 -0
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/README.md +0 -0
- {django_nested_values-0.6.14 → django_nested_values-0.7.0}/django_nested_values/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-nested-values
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Add .values_nested() to Django querysets for nested prefetch dictionaries
|
|
5
5
|
Project-URL: Homepage, https://github.com/oliverhaas/django-nested-values
|
|
6
6
|
Project-URL: Documentation, https://oliverhaas.github.io/django-nested-values/
|
|
@@ -4,8 +4,7 @@ from django_nested_values.queryset import (
|
|
|
4
4
|
AttrDict,
|
|
5
5
|
NestedValuesQuerySet,
|
|
6
6
|
NestedValuesQuerySetMixin,
|
|
7
|
-
RelatedList,
|
|
8
7
|
)
|
|
9
8
|
|
|
10
|
-
__all__ = ["AttrDict", "NestedValuesQuerySet", "NestedValuesQuerySetMixin"
|
|
11
|
-
__version__ = "0.
|
|
9
|
+
__all__ = ["AttrDict", "NestedValuesQuerySet", "NestedValuesQuerySetMixin"]
|
|
10
|
+
__version__ = "0.7.0"
|
|
@@ -15,6 +15,40 @@ from django.db.models.query import BaseIterable
|
|
|
15
15
|
# TypeVar for the model type, used for generic typing with django-stubs
|
|
16
16
|
_ModelT_co = TypeVar("_ModelT_co", bound=Model, covariant=True)
|
|
17
17
|
|
|
18
|
+
# Type alias for dict-like container classes
|
|
19
|
+
_ContainerType = type[dict[str, Any]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AttrDict(dict[str, Any]):
|
|
23
|
+
"""Dict subclass with attribute access - minimal overhead.
|
|
24
|
+
|
|
25
|
+
Inherits from dict so isinstance(x, dict) is True and all dict
|
|
26
|
+
operations work unchanged. Just adds __getattr__ for dot access.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__slots__ = ()
|
|
30
|
+
|
|
31
|
+
def __getattr__(self, name: str) -> Any:
|
|
32
|
+
"""Get item as attribute."""
|
|
33
|
+
try:
|
|
34
|
+
return self[name]
|
|
35
|
+
except KeyError:
|
|
36
|
+
msg = f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
37
|
+
raise AttributeError(msg) from None
|
|
38
|
+
|
|
39
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
40
|
+
"""Set item as attribute."""
|
|
41
|
+
self[name] = value
|
|
42
|
+
|
|
43
|
+
def __delattr__(self, name: str) -> None:
|
|
44
|
+
"""Delete item as attribute."""
|
|
45
|
+
try:
|
|
46
|
+
del self[name]
|
|
47
|
+
except KeyError:
|
|
48
|
+
msg = f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
49
|
+
raise AttributeError(msg) from None
|
|
50
|
+
|
|
51
|
+
|
|
18
52
|
if TYPE_CHECKING:
|
|
19
53
|
from collections.abc import Iterator
|
|
20
54
|
|
|
@@ -27,71 +61,57 @@ else:
|
|
|
27
61
|
_MixinBase = Generic
|
|
28
62
|
|
|
29
63
|
|
|
30
|
-
def
|
|
64
|
+
def _build_from_klass_info(
|
|
31
65
|
row: tuple[Any, ...],
|
|
32
66
|
klass_info: dict[str, Any],
|
|
33
67
|
select: list[tuple[Any, ...]],
|
|
68
|
+
container: _ContainerType = dict,
|
|
34
69
|
) -> dict[str, Any]:
|
|
35
|
-
"""Build a dict
|
|
70
|
+
"""Build a dict (or dict subclass like AttrDict) directly from a row.
|
|
36
71
|
|
|
37
72
|
This uses Django's internal compiler metadata to know exactly which
|
|
38
73
|
columns belong to which model, avoiding manual field path parsing.
|
|
39
74
|
|
|
40
75
|
Args:
|
|
41
76
|
row: A tuple of values from the database row
|
|
42
|
-
klass_info: The klass_info dict from compiler
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- 'related_klass_infos': list of klass_info for select_related models
|
|
46
|
-
select: The compiler.select list, mapping indices to column expressions
|
|
77
|
+
klass_info: The klass_info dict from compiler
|
|
78
|
+
select: The compiler.select list
|
|
79
|
+
container: The dict-like class to use (dict or AttrDict)
|
|
47
80
|
|
|
48
81
|
Returns:
|
|
49
|
-
A
|
|
82
|
+
A container instance with field names as keys
|
|
50
83
|
|
|
51
84
|
"""
|
|
52
|
-
|
|
85
|
+
# Build directly into the container - no intermediate dict
|
|
86
|
+
result = container()
|
|
53
87
|
|
|
54
88
|
for idx in klass_info["select_fields"]:
|
|
55
89
|
col_expr = select[idx][0]
|
|
56
|
-
|
|
57
|
-
result[field_name] = row[idx]
|
|
90
|
+
result[col_expr.target.attname] = row[idx]
|
|
58
91
|
|
|
59
92
|
for related_ki in klass_info.get("related_klass_infos", []):
|
|
60
|
-
relation_name = related_ki["field"].name
|
|
61
|
-
|
|
62
|
-
# Check if related object is NULL (first select_field is typically the PK)
|
|
63
93
|
pk_idx = related_ki["select_fields"][0]
|
|
64
94
|
if row[pk_idx] is None:
|
|
65
95
|
continue
|
|
66
|
-
|
|
67
|
-
nested_dict = _build_dict_from_klass_info(row, related_ki, select)
|
|
68
|
-
result[relation_name] = nested_dict
|
|
96
|
+
result[related_ki["field"].name] = _build_from_klass_info(row, related_ki, select, container)
|
|
69
97
|
|
|
70
98
|
return result
|
|
71
99
|
|
|
72
100
|
|
|
73
|
-
def
|
|
101
|
+
def _execute_queryset(
|
|
74
102
|
queryset: QuerySet[Any, Any],
|
|
75
103
|
db: str,
|
|
104
|
+
container: _ContainerType = dict,
|
|
76
105
|
) -> list[dict[str, Any]]:
|
|
77
|
-
"""Execute a queryset
|
|
78
|
-
|
|
79
|
-
This uses Django's internal compiler to execute the query and builds
|
|
80
|
-
nested dictionaries directly from the raw row tuples - NO model instantiation.
|
|
81
|
-
|
|
82
|
-
The pipeline is: raw SQL rows → dicts (via klass_info metadata)
|
|
83
|
-
|
|
84
|
-
This handles:
|
|
85
|
-
- select_related: Automatically builds nested dicts for related objects
|
|
86
|
-
- only()/defer(): Compiler respects deferred field loading
|
|
87
|
-
- Custom querysets: Works with any queryset (base, filtered, etc.)
|
|
106
|
+
"""Execute a queryset and return results as nested dicts (or AttrDict).
|
|
88
107
|
|
|
89
108
|
Args:
|
|
90
|
-
queryset: The queryset to execute
|
|
109
|
+
queryset: The queryset to execute
|
|
91
110
|
db: Database alias to use
|
|
111
|
+
container: The dict-like class to use (dict or AttrDict)
|
|
92
112
|
|
|
93
113
|
Returns:
|
|
94
|
-
List of nested
|
|
114
|
+
List of nested containers, one per row
|
|
95
115
|
|
|
96
116
|
"""
|
|
97
117
|
compiler = queryset.query.get_compiler(using=db)
|
|
@@ -106,20 +126,25 @@ def _execute_queryset_as_dicts(
|
|
|
106
126
|
if klass_info is None:
|
|
107
127
|
return []
|
|
108
128
|
|
|
109
|
-
return [
|
|
129
|
+
return [_build_from_klass_info(row, klass_info, select, container) for row in compiler.results_iter(results)]
|
|
110
130
|
|
|
111
131
|
|
|
112
|
-
def
|
|
132
|
+
def _execute_prefetch(
|
|
113
133
|
queryset: QuerySet[Any, Any],
|
|
114
134
|
db: str,
|
|
135
|
+
container: _ContainerType = dict,
|
|
115
136
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
116
|
-
"""Execute a prefetch queryset and return (
|
|
137
|
+
"""Execute a prefetch queryset and return (containers, extra_values).
|
|
138
|
+
|
|
139
|
+
This extracts the extra columns Django adds for prefetch grouping.
|
|
117
140
|
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
Args:
|
|
142
|
+
queryset: The queryset to execute
|
|
143
|
+
db: Database alias to use
|
|
144
|
+
container: The dict-like class to use (dict or AttrDict)
|
|
120
145
|
|
|
121
146
|
Returns:
|
|
122
|
-
Tuple of (list of model
|
|
147
|
+
Tuple of (list of model containers, list of extra column dicts for grouping)
|
|
123
148
|
|
|
124
149
|
"""
|
|
125
150
|
compiler = queryset.query.get_compiler(using=db)
|
|
@@ -135,152 +160,22 @@ def _execute_prefetch_as_dicts(
|
|
|
135
160
|
return [], []
|
|
136
161
|
|
|
137
162
|
# Find extra column indices (columns with _prefetch_related_val_* alias)
|
|
138
|
-
# Extra columns from .extra() have format: (RawSQL, (sql, params), alias)
|
|
139
163
|
extra_indices = [
|
|
140
164
|
(i, s[2])
|
|
141
165
|
for i, s in enumerate(select)
|
|
142
166
|
if len(s) >= 3 and s[2] and s[2].startswith("_prefetch_related_val_") # noqa: PLR2004
|
|
143
167
|
]
|
|
144
168
|
|
|
145
|
-
|
|
169
|
+
containers = []
|
|
146
170
|
extra_values = []
|
|
147
171
|
|
|
148
172
|
for row in compiler.results_iter(results):
|
|
149
|
-
|
|
173
|
+
row_container = _build_from_klass_info(row, klass_info, select, container)
|
|
150
174
|
extra_vals = {alias: row[idx] for idx, alias in extra_indices}
|
|
151
|
-
|
|
152
|
-
dicts.append(row_dict)
|
|
175
|
+
containers.append(row_container)
|
|
153
176
|
extra_values.append(extra_vals)
|
|
154
177
|
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
class RelatedList(list[Any]):
|
|
159
|
-
"""List wrapper that provides .all() method to mimic Django's related manager.
|
|
160
|
-
|
|
161
|
-
This allows code like `for book in author.books.all()` to work with nested values,
|
|
162
|
-
making the results more drop-in compatible with Django ORM patterns.
|
|
163
|
-
"""
|
|
164
|
-
|
|
165
|
-
__slots__ = ()
|
|
166
|
-
|
|
167
|
-
def all(self) -> Self:
|
|
168
|
-
"""Return self to mimic Django's related manager .all() method."""
|
|
169
|
-
return self
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
class AttrDict:
|
|
173
|
-
"""Dict wrapper that provides attribute access to mimic Django model instances.
|
|
174
|
-
|
|
175
|
-
Supports both attribute access (book.title) and dict access (book["title"]).
|
|
176
|
-
Nested dicts are wrapped as AttrDict, lists are wrapped as RelatedList.
|
|
177
|
-
|
|
178
|
-
Example:
|
|
179
|
-
>>> obj = AttrDict({"title": "Django", "publisher": {"name": "Pub"}})
|
|
180
|
-
>>> obj.title
|
|
181
|
-
'Django'
|
|
182
|
-
>>> obj["title"]
|
|
183
|
-
'Django'
|
|
184
|
-
>>> obj.publisher.name
|
|
185
|
-
'Pub'
|
|
186
|
-
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
__slots__ = ("_data",)
|
|
190
|
-
|
|
191
|
-
def __init__(self, data: dict[str, Any]) -> None:
|
|
192
|
-
"""Initialize with a dict. Nested values are wrapped eagerly."""
|
|
193
|
-
# Wrap nested structures eagerly
|
|
194
|
-
wrapped: dict[str, Any] = {}
|
|
195
|
-
for key, value in data.items():
|
|
196
|
-
wrapped[key] = self._wrap_value(value)
|
|
197
|
-
object.__setattr__(self, "_data", wrapped)
|
|
198
|
-
|
|
199
|
-
@staticmethod
|
|
200
|
-
def _wrap_value(value: Any) -> Any:
|
|
201
|
-
"""Wrap a value: dicts become AttrDict, lists become RelatedList."""
|
|
202
|
-
if isinstance(value, dict):
|
|
203
|
-
return AttrDict(value)
|
|
204
|
-
if isinstance(value, list):
|
|
205
|
-
return RelatedList(AttrDict._wrap_value(item) for item in value)
|
|
206
|
-
return value
|
|
207
|
-
|
|
208
|
-
def __getattr__(self, name: str) -> Any:
|
|
209
|
-
"""Get attribute from underlying dict."""
|
|
210
|
-
try:
|
|
211
|
-
return self._data[name]
|
|
212
|
-
except KeyError:
|
|
213
|
-
msg = f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
214
|
-
raise AttributeError(msg) from None
|
|
215
|
-
|
|
216
|
-
def __setattr__(self, name: str, value: Any) -> None:
|
|
217
|
-
"""Set attribute in underlying dict."""
|
|
218
|
-
if name == "_data":
|
|
219
|
-
object.__setattr__(self, name, value)
|
|
220
|
-
else:
|
|
221
|
-
self._data[name] = self._wrap_value(value)
|
|
222
|
-
|
|
223
|
-
def __getitem__(self, key: str) -> Any:
|
|
224
|
-
"""Get item from underlying dict."""
|
|
225
|
-
return self._data[key]
|
|
226
|
-
|
|
227
|
-
def __setitem__(self, key: str, value: Any) -> None:
|
|
228
|
-
"""Set item in underlying dict."""
|
|
229
|
-
self._data[key] = self._wrap_value(value)
|
|
230
|
-
|
|
231
|
-
def __contains__(self, key: object) -> bool:
|
|
232
|
-
"""Check if key exists in underlying dict."""
|
|
233
|
-
return key in self._data
|
|
234
|
-
|
|
235
|
-
def __iter__(self) -> Iterator[str]:
|
|
236
|
-
"""Iterate over keys in underlying dict."""
|
|
237
|
-
return iter(self._data)
|
|
238
|
-
|
|
239
|
-
def __len__(self) -> int:
|
|
240
|
-
"""Return number of keys in underlying dict."""
|
|
241
|
-
return len(self._data)
|
|
242
|
-
|
|
243
|
-
def __repr__(self) -> str:
|
|
244
|
-
"""Return repr of underlying dict."""
|
|
245
|
-
return f"AttrDict({self._data!r})"
|
|
246
|
-
|
|
247
|
-
def __eq__(self, other: object) -> bool:
|
|
248
|
-
"""Compare with another AttrDict or dict."""
|
|
249
|
-
if isinstance(other, AttrDict):
|
|
250
|
-
return bool(self._data == other._data)
|
|
251
|
-
if isinstance(other, dict):
|
|
252
|
-
return bool(self._data == other)
|
|
253
|
-
return NotImplemented
|
|
254
|
-
|
|
255
|
-
__hash__: None = None # type: ignore[assignment] # Mutable container, so explicitly unhashable
|
|
256
|
-
|
|
257
|
-
def keys(self) -> Any:
|
|
258
|
-
"""Return keys of underlying dict."""
|
|
259
|
-
return self._data.keys()
|
|
260
|
-
|
|
261
|
-
def values(self) -> Any:
|
|
262
|
-
"""Return values of underlying dict."""
|
|
263
|
-
return self._data.values()
|
|
264
|
-
|
|
265
|
-
def items(self) -> Any:
|
|
266
|
-
"""Return items of underlying dict."""
|
|
267
|
-
return self._data.items()
|
|
268
|
-
|
|
269
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
270
|
-
"""Get item with default from underlying dict."""
|
|
271
|
-
return self._data.get(key, default)
|
|
272
|
-
|
|
273
|
-
def to_dict(self) -> dict[str, Any]:
|
|
274
|
-
"""Convert back to a plain dict (recursively unwraps nested objects)."""
|
|
275
|
-
result: dict[str, Any] = {}
|
|
276
|
-
for key, value in self._data.items():
|
|
277
|
-
if isinstance(value, AttrDict):
|
|
278
|
-
result[key] = value.to_dict()
|
|
279
|
-
elif isinstance(value, RelatedList):
|
|
280
|
-
result[key] = [item.to_dict() if isinstance(item, AttrDict) else item for item in value]
|
|
281
|
-
else:
|
|
282
|
-
result[key] = value
|
|
283
|
-
return result
|
|
178
|
+
return containers, extra_values
|
|
284
179
|
|
|
285
180
|
|
|
286
181
|
class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
@@ -294,12 +189,10 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
294
189
|
# The queryset is expected to be a NestedValuesQuerySetMixin
|
|
295
190
|
queryset: NestedValuesQuerySetMixin[Any]
|
|
296
191
|
|
|
297
|
-
def __iter__(self) -> Iterator[dict[str, Any]
|
|
298
|
-
"""Iterate over the queryset, yielding nested dictionaries or
|
|
192
|
+
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
193
|
+
"""Iterate over the queryset, yielding nested dictionaries (or AttrDict)."""
|
|
299
194
|
queryset = self.queryset
|
|
300
195
|
db = queryset.db
|
|
301
|
-
# Use _nested_prefetch_lookups which is set by values_nested() - we clear
|
|
302
|
-
# _prefetch_related_lookups to prevent Django from double-prefetching
|
|
303
196
|
prefetch_lookups = getattr(queryset, "_nested_prefetch_lookups", ())
|
|
304
197
|
|
|
305
198
|
main_qs = queryset._build_main_queryset()
|
|
@@ -318,34 +211,40 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
318
211
|
if klass_info is None:
|
|
319
212
|
return
|
|
320
213
|
|
|
321
|
-
|
|
214
|
+
# Choose container based on as_attr_dicts flag
|
|
215
|
+
as_attr_dicts = getattr(queryset, "_as_attr_dicts", False)
|
|
216
|
+
container: _ContainerType = AttrDict if as_attr_dicts else dict # type: ignore[assignment]
|
|
217
|
+
pk_name = queryset.model._meta.pk.name
|
|
218
|
+
|
|
219
|
+
# Build main results - unified path for both dict and AttrDict
|
|
220
|
+
main_results = [
|
|
221
|
+
_build_from_klass_info(row, klass_info, select, container) for row in compiler.results_iter(results)
|
|
222
|
+
]
|
|
322
223
|
if not main_results:
|
|
323
224
|
return
|
|
324
225
|
|
|
325
|
-
# Check if we should wrap results as AttrDict
|
|
326
|
-
as_attr_dicts = getattr(queryset, "_as_attr_dicts", False)
|
|
327
|
-
|
|
328
226
|
if not prefetch_lookups:
|
|
329
|
-
|
|
330
|
-
yield AttrDict(row) if as_attr_dicts else row
|
|
227
|
+
yield from main_results
|
|
331
228
|
return
|
|
332
229
|
|
|
333
|
-
|
|
230
|
+
# Fetch prefetched data and attach to main results
|
|
334
231
|
pk_values = [r[pk_name] for r in main_results]
|
|
335
|
-
prefetched_data = queryset._fetch_all_prefetched(
|
|
232
|
+
prefetched_data = queryset._fetch_all_prefetched(
|
|
233
|
+
prefetch_lookups,
|
|
234
|
+
pk_values,
|
|
235
|
+
main_results,
|
|
236
|
+
container=container,
|
|
237
|
+
)
|
|
336
238
|
|
|
337
239
|
for row in main_results:
|
|
338
240
|
pk = row[pk_name]
|
|
339
241
|
for attr_name, data_by_pk in prefetched_data.items():
|
|
340
242
|
prefetch_value = data_by_pk.get(pk, [])
|
|
341
243
|
self._set_nested_value(row, attr_name, prefetch_value)
|
|
342
|
-
yield
|
|
244
|
+
yield row
|
|
343
245
|
|
|
344
246
|
def _set_nested_value(self, row: dict[str, Any], attr_path: str, value: Any) -> None:
|
|
345
|
-
"""Set a value in a nested dict using a path like 'publisher__books'.
|
|
346
|
-
|
|
347
|
-
Navigates to the correct nested location and sets or merges the value.
|
|
348
|
-
"""
|
|
247
|
+
"""Set a value in a nested dict using a path like 'publisher__books'."""
|
|
349
248
|
parts = attr_path.split("__")
|
|
350
249
|
target = row
|
|
351
250
|
|
|
@@ -361,17 +260,11 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
361
260
|
existing = target[final_key]
|
|
362
261
|
if isinstance(existing, dict) and isinstance(value, dict):
|
|
363
262
|
self._merge_dicts(existing, value)
|
|
364
|
-
# Otherwise keep existing (select_related takes precedence)
|
|
365
263
|
else:
|
|
366
264
|
target[final_key] = value
|
|
367
265
|
|
|
368
266
|
def _merge_dicts(self, target: dict[str, Any], source: dict[str, Any]) -> None:
|
|
369
|
-
"""Recursively merge source dict into target dict.
|
|
370
|
-
|
|
371
|
-
New keys from source are added. For existing keys where both values
|
|
372
|
-
are dicts, recursively merge. Otherwise, target value takes precedence
|
|
373
|
-
(select_related data wins over prefetch data for same field).
|
|
374
|
-
"""
|
|
267
|
+
"""Recursively merge source dict into target dict."""
|
|
375
268
|
for key, value in source.items():
|
|
376
269
|
if key not in target:
|
|
377
270
|
target[key] = value
|
|
@@ -379,12 +272,7 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
379
272
|
self._merge_dicts(target[key], value)
|
|
380
273
|
|
|
381
274
|
def _ensure_fk_fields_not_deferred(self, qs: QuerySet[Any, Any]) -> None:
|
|
382
|
-
"""Ensure FK fields for select_related are not deferred.
|
|
383
|
-
|
|
384
|
-
When using .only() without the FK field and then select_related(),
|
|
385
|
-
Django would raise an error. This method clears deferral for FK fields
|
|
386
|
-
that are traversed via select_related.
|
|
387
|
-
"""
|
|
275
|
+
"""Ensure FK fields for select_related are not deferred."""
|
|
388
276
|
select_related = qs.query.select_related
|
|
389
277
|
if not select_related:
|
|
390
278
|
return
|
|
@@ -393,13 +281,9 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
393
281
|
if not deferred_fields:
|
|
394
282
|
return
|
|
395
283
|
|
|
396
|
-
# Get FK field names for select_related relations
|
|
397
284
|
if select_related is True:
|
|
398
|
-
# select_related() with no args - need all FK fields
|
|
399
285
|
fk_fields = {f.attname for f in qs.model._meta.concrete_fields if isinstance(f, ForeignKey)}
|
|
400
286
|
else:
|
|
401
|
-
# select_related is a dict like {'publisher': {}, 'author': {'publisher': {}}}
|
|
402
|
-
# We need the FK attnames for top-level relations
|
|
403
287
|
fk_fields = set()
|
|
404
288
|
for relation_name in select_related:
|
|
405
289
|
try:
|
|
@@ -433,19 +317,14 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
433
317
|
objects = MyQuerySet.as_manager()
|
|
434
318
|
|
|
435
319
|
Or use the pre-built NestedValuesQuerySet if you don't need a custom QuerySet.
|
|
436
|
-
|
|
437
|
-
Type hints: After calling values_nested(), the queryset yields dict[str, Any]
|
|
438
|
-
when iterated, similar to Django's values() method.
|
|
439
320
|
"""
|
|
440
321
|
|
|
441
|
-
# Custom attributes that should be preserved when cloning
|
|
442
322
|
_nested_prefetch_lookups: tuple[Any, ...] = ()
|
|
443
323
|
_as_attr_dicts: bool = False
|
|
444
324
|
|
|
445
325
|
def _clone(self) -> Self:
|
|
446
326
|
"""Clone the queryset, preserving our custom attributes."""
|
|
447
327
|
clone: Self = super()._clone() # type: ignore[misc]
|
|
448
|
-
# Copy our custom attributes to the clone
|
|
449
328
|
clone._nested_prefetch_lookups = self._nested_prefetch_lookups
|
|
450
329
|
clone._as_attr_dicts = self._as_attr_dicts
|
|
451
330
|
return clone
|
|
@@ -474,24 +353,15 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
474
353
|
Args:
|
|
475
354
|
as_attr_dicts: If True, return AttrDict instances instead of plain dicts.
|
|
476
355
|
AttrDict supports attribute access (book.title) in addition to
|
|
477
|
-
dict access (book["title"])
|
|
478
|
-
mimic Django's related manager interface.
|
|
479
|
-
|
|
480
|
-
Use standard Django methods to control output:
|
|
481
|
-
- .only() to select which fields to include
|
|
482
|
-
- .select_related() for ForeignKey relations (single dict/object)
|
|
483
|
-
- .prefetch_related() for ManyToMany/reverse FK relations (list of dicts/objects)
|
|
356
|
+
dict access (book["title"]).
|
|
484
357
|
|
|
485
358
|
Returns:
|
|
486
|
-
A QuerySet that yields dict[str, Any] or AttrDict when iterated
|
|
487
|
-
with nested dictionaries/objects for related objects.
|
|
359
|
+
A QuerySet that yields dict[str, Any] or AttrDict when iterated.
|
|
488
360
|
|
|
489
361
|
"""
|
|
490
362
|
clone: Self = self._clone() # type: ignore[assignment]
|
|
491
363
|
clone._iterable_class = NestedValuesIterable
|
|
492
364
|
clone._as_attr_dicts = as_attr_dicts
|
|
493
|
-
# Store prefetch lookups for our own use, then clear them so Django
|
|
494
|
-
# doesn't try to prefetch on our dict/AttrDict results
|
|
495
365
|
clone._nested_prefetch_lookups = clone._prefetch_related_lookups # type: ignore[attr-defined]
|
|
496
366
|
clone._prefetch_related_lookups = () # type: ignore[attr-defined]
|
|
497
367
|
return clone # type: ignore[return-value]
|
|
@@ -508,9 +378,11 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
508
378
|
prefetch_lookups: tuple[Any, ...],
|
|
509
379
|
parent_pks: list[Any],
|
|
510
380
|
main_results: list[dict[str, Any]],
|
|
511
|
-
|
|
381
|
+
*,
|
|
382
|
+
container: _ContainerType = dict,
|
|
383
|
+
) -> dict[str, dict[Any, Any]]:
|
|
512
384
|
"""Fetch all prefetched relations."""
|
|
513
|
-
result: dict[str, dict[Any,
|
|
385
|
+
result: dict[str, dict[Any, Any]] = {}
|
|
514
386
|
lookup_map = self._group_prefetch_lookups(prefetch_lookups)
|
|
515
387
|
|
|
516
388
|
for attr_name, lookup_info in lookup_map.items():
|
|
@@ -518,10 +390,11 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
518
390
|
nested = lookup_info["nested"]
|
|
519
391
|
|
|
520
392
|
if lookup_info.get("is_generic_fk"):
|
|
521
|
-
result[attr_name] = self._fetch_generic_fk_values(
|
|
393
|
+
result[attr_name] = self._fetch_generic_fk_values(
|
|
522
394
|
lookup,
|
|
523
395
|
parent_pks,
|
|
524
396
|
main_results,
|
|
397
|
+
container=container,
|
|
525
398
|
)
|
|
526
399
|
else:
|
|
527
400
|
result[attr_name] = self._fetch_relation_values(
|
|
@@ -529,6 +402,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
529
402
|
nested,
|
|
530
403
|
parent_pks,
|
|
531
404
|
main_results,
|
|
405
|
+
container=container,
|
|
532
406
|
)
|
|
533
407
|
|
|
534
408
|
return result
|
|
@@ -551,7 +425,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
551
425
|
|
|
552
426
|
if "__" in relation_path:
|
|
553
427
|
parts = relation_path.split("__", 1)
|
|
554
|
-
if parts[0] == attr_name
|
|
428
|
+
if parts[0] == attr_name:
|
|
555
429
|
result[attr_name]["nested"].append(parts[1])
|
|
556
430
|
else:
|
|
557
431
|
attr_name = lookup.split("__")[0]
|
|
@@ -573,6 +447,8 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
573
447
|
nested_relations: list[str],
|
|
574
448
|
parent_pks: list[Any],
|
|
575
449
|
main_results: list[dict[str, Any]],
|
|
450
|
+
*,
|
|
451
|
+
container: _ContainerType = dict,
|
|
576
452
|
) -> dict[Any, list[dict[str, Any]] | dict[str, Any] | None]:
|
|
577
453
|
"""Fetch a related model's data and group by parent PK."""
|
|
578
454
|
relation_name = (
|
|
@@ -598,14 +474,11 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
598
474
|
parent_path="",
|
|
599
475
|
parent_data={r[pk_name]: r for r in main_results},
|
|
600
476
|
custom_qs=custom_qs,
|
|
477
|
+
container=container,
|
|
601
478
|
)
|
|
602
479
|
|
|
603
480
|
def _get_select_related_from_queryset(self, qs: QuerySet[Any, Any] | None) -> dict[str, Any]:
|
|
604
|
-
"""Get select_related structure from a queryset.
|
|
605
|
-
|
|
606
|
-
Returns a dict like {'publisher': {}, 'publisher__country': {}} for
|
|
607
|
-
select_related('publisher', 'publisher__country').
|
|
608
|
-
"""
|
|
481
|
+
"""Get select_related structure from a queryset."""
|
|
609
482
|
if qs is None:
|
|
610
483
|
return {}
|
|
611
484
|
|
|
@@ -614,15 +487,12 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
614
487
|
return {}
|
|
615
488
|
|
|
616
489
|
if select_related is True:
|
|
617
|
-
# select_related() with no args - get all FK fields
|
|
618
490
|
result: dict[str, Any] = {}
|
|
619
491
|
for field in qs.model._meta.concrete_fields:
|
|
620
492
|
if isinstance(field, ForeignKey):
|
|
621
493
|
result[field.name] = {}
|
|
622
494
|
return result
|
|
623
495
|
|
|
624
|
-
# select_related is a nested dict like {'publisher': {'country': {}}}
|
|
625
|
-
# Flatten it to paths
|
|
626
496
|
result = {}
|
|
627
497
|
self._flatten_select_related_to_paths(select_related, "", result)
|
|
628
498
|
return result
|
|
@@ -651,12 +521,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
651
521
|
main_results: list[dict[str, Any]] | None,
|
|
652
522
|
parent_path: str,
|
|
653
523
|
m2m_field: ManyToManyField[Any, Any] | ManyToManyRel,
|
|
524
|
+
*,
|
|
525
|
+
container: _ContainerType = dict,
|
|
654
526
|
) -> dict[Any, list[dict[str, Any]]]:
|
|
655
|
-
"""Fetch M2M data for forward/reverse M2M lookups
|
|
656
|
-
|
|
657
|
-
Use Django's compiler with .extra() annotation to match Django's native
|
|
658
|
-
prefetch_related behavior. Raw rows → dicts, no model instantiation.
|
|
659
|
-
"""
|
|
527
|
+
"""Fetch M2M data for forward/reverse M2M lookups."""
|
|
660
528
|
related_pk_name = related_model._meta.pk.name
|
|
661
529
|
|
|
662
530
|
if custom_qs is not None:
|
|
@@ -666,7 +534,6 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
666
534
|
|
|
667
535
|
actual_field = m2m_field.field if isinstance(m2m_field, ManyToManyRel) else m2m_field
|
|
668
536
|
|
|
669
|
-
# Add extra select for parent FK (matching Django's prefetch approach)
|
|
670
537
|
through_model = actual_field.remote_field.through
|
|
671
538
|
assert through_model is not None # noqa: S101
|
|
672
539
|
source_field_name = (
|
|
@@ -679,23 +546,24 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
679
546
|
extra_select = {
|
|
680
547
|
f"_prefetch_related_val_{f.attname}": f"{qn(join_table)}.{qn(f.column)}" for f in fk.local_related_fields
|
|
681
548
|
}
|
|
682
|
-
related_qs = related_qs.extra(select=extra_select) # noqa: S610
|
|
549
|
+
related_qs = related_qs.extra(select=extra_select) # noqa: S610
|
|
683
550
|
|
|
684
|
-
|
|
685
|
-
|
|
551
|
+
# Build directly into container (dict or AttrDict) - no conversion needed
|
|
552
|
+
containers, extra_values = _execute_prefetch(related_qs, self.db, container)
|
|
553
|
+
if not containers:
|
|
686
554
|
return {pk: [] for pk in parent_pks}
|
|
687
555
|
|
|
688
556
|
result: dict[Any, list[dict[str, Any]]] = {pk: [] for pk in parent_pks}
|
|
689
557
|
related_data: dict[Any, dict[str, Any]] = {}
|
|
690
558
|
|
|
691
|
-
for
|
|
559
|
+
for row_container, extra_vals in zip(containers, extra_values, strict=True):
|
|
692
560
|
source_pk = next(iter(extra_vals.values())) if extra_vals else None
|
|
693
561
|
if source_pk in result:
|
|
694
|
-
result[source_pk].append(
|
|
562
|
+
result[source_pk].append(row_container)
|
|
695
563
|
|
|
696
|
-
related_pk =
|
|
564
|
+
related_pk = row_container.get(related_pk_name)
|
|
697
565
|
if nested_relations and related_pk not in related_data:
|
|
698
|
-
related_data[related_pk] =
|
|
566
|
+
related_data[related_pk] = row_container
|
|
699
567
|
|
|
700
568
|
if nested_relations and related_data:
|
|
701
569
|
select_related_paths = self._get_select_related_from_queryset(custom_qs)
|
|
@@ -709,6 +577,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
709
577
|
list(related_data.keys()),
|
|
710
578
|
main_results,
|
|
711
579
|
f"{full_path}__",
|
|
580
|
+
container=container,
|
|
712
581
|
)
|
|
713
582
|
for items in result.values():
|
|
714
583
|
for item in items:
|
|
@@ -730,18 +599,21 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
730
599
|
custom_qs: QuerySet[Any, Any] | None,
|
|
731
600
|
main_results: list[dict[str, Any]] | None,
|
|
732
601
|
parent_path: str,
|
|
602
|
+
*,
|
|
603
|
+
container: _ContainerType = dict,
|
|
733
604
|
) -> dict[Any, list[dict[str, Any]]]:
|
|
734
|
-
"""Fetch reverse FK data.
|
|
605
|
+
"""Fetch reverse FK data."""
|
|
735
606
|
related_pk_name = related_model._meta.pk.name
|
|
736
607
|
fk_field = related_model._meta.get_field(fk_field_name)
|
|
737
|
-
fk_attname = fk_field.attname # type: ignore[union-attr]
|
|
608
|
+
fk_attname = fk_field.attname # type: ignore[union-attr]
|
|
738
609
|
|
|
739
610
|
if custom_qs is not None:
|
|
740
611
|
related_qs = custom_qs.filter(**{f"{fk_field_name}__in": parent_pks})
|
|
741
612
|
else:
|
|
742
613
|
related_qs = related_model._default_manager.filter(**{f"{fk_field_name}__in": parent_pks})
|
|
743
614
|
|
|
744
|
-
|
|
615
|
+
# Build directly into container (dict or AttrDict)
|
|
616
|
+
related_data = _execute_queryset(related_qs, self.db, container)
|
|
745
617
|
if not related_data:
|
|
746
618
|
return {pk: [] for pk in parent_pks}
|
|
747
619
|
|
|
@@ -752,14 +624,14 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
752
624
|
related_pks = [r[related_pk_name] for r in related_data]
|
|
753
625
|
nested_dict = {r[related_pk_name]: r for r in related_data}
|
|
754
626
|
full_path = f"{parent_path}{relation_name}" if parent_path else relation_name
|
|
755
|
-
new_parent_path = f"{full_path}__"
|
|
756
627
|
self._add_nested_relations(
|
|
757
628
|
related_model,
|
|
758
629
|
nested_dict,
|
|
759
630
|
remaining_nested,
|
|
760
631
|
related_pks,
|
|
761
632
|
main_results,
|
|
762
|
-
|
|
633
|
+
f"{full_path}__",
|
|
634
|
+
container=container,
|
|
763
635
|
)
|
|
764
636
|
related_data = list(nested_dict.values())
|
|
765
637
|
|
|
@@ -767,9 +639,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
767
639
|
for row in related_data:
|
|
768
640
|
parent_pk = row.get(fk_attname)
|
|
769
641
|
if parent_pk in result:
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
642
|
+
# Remove FK attname from output
|
|
643
|
+
if fk_attname in row:
|
|
644
|
+
del row[fk_attname]
|
|
645
|
+
result[parent_pk].append(row)
|
|
773
646
|
|
|
774
647
|
return result
|
|
775
648
|
|
|
@@ -783,8 +656,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
783
656
|
main_results: list[dict[str, Any]] | None,
|
|
784
657
|
parent_path: str,
|
|
785
658
|
parent_model: type[Model] | None = None,
|
|
659
|
+
*,
|
|
660
|
+
container: _ContainerType = dict,
|
|
786
661
|
) -> dict[Any, dict[str, Any] | None]:
|
|
787
|
-
"""Fetch FK data.
|
|
662
|
+
"""Fetch FK data."""
|
|
788
663
|
related_model = cast("type[Model]", field.related_model)
|
|
789
664
|
fk_attname = field.attname
|
|
790
665
|
relation_name = field.name
|
|
@@ -819,14 +694,14 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
819
694
|
for row in parent_data.values(): # type: ignore[union-attr]
|
|
820
695
|
nested = row.get(relation_name)
|
|
821
696
|
if isinstance(nested, dict) and nested.get(related_pk_name) is not None:
|
|
822
|
-
related_data[nested[related_pk_name]] =
|
|
697
|
+
related_data[nested[related_pk_name]] = container(nested)
|
|
823
698
|
else:
|
|
824
699
|
if custom_qs is not None:
|
|
825
700
|
related_qs = custom_qs.filter(pk__in=fk_values)
|
|
826
701
|
else:
|
|
827
702
|
related_qs = related_model._default_manager.filter(pk__in=fk_values)
|
|
828
703
|
|
|
829
|
-
results =
|
|
704
|
+
results = _execute_queryset(related_qs, self.db, container)
|
|
830
705
|
related_data = {r[related_pk_name]: r for r in results}
|
|
831
706
|
|
|
832
707
|
if not related_data:
|
|
@@ -841,12 +716,13 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
841
716
|
fk_values,
|
|
842
717
|
main_results,
|
|
843
718
|
full_path,
|
|
719
|
+
container=container,
|
|
844
720
|
)
|
|
845
721
|
|
|
846
722
|
result: dict[Any, dict[str, Any] | None] = {}
|
|
847
723
|
for parent_pk in parent_pks:
|
|
848
724
|
fk_value = fk_data.get(parent_pk)
|
|
849
|
-
result[parent_pk] =
|
|
725
|
+
result[parent_pk] = container(related_data[fk_value]) if fk_value in related_data else None
|
|
850
726
|
|
|
851
727
|
return result
|
|
852
728
|
|
|
@@ -859,8 +735,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
859
735
|
custom_qs: QuerySet[Any, Any] | None,
|
|
860
736
|
main_results: list[dict[str, Any]] | None,
|
|
861
737
|
parent_path: str,
|
|
738
|
+
*,
|
|
739
|
+
container: _ContainerType = dict,
|
|
862
740
|
) -> dict[Any, list[dict[str, Any]]]:
|
|
863
|
-
"""Fetch GenericRelation data
|
|
741
|
+
"""Fetch GenericRelation data."""
|
|
864
742
|
related_model = cast("type[Model]", field.related_model)
|
|
865
743
|
ct_field_name = field.content_type_field_name
|
|
866
744
|
obj_id_field_name = field.object_id_field_name
|
|
@@ -874,7 +752,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
874
752
|
else:
|
|
875
753
|
related_qs = related_model._default_manager.filter(**filter_kwargs)
|
|
876
754
|
|
|
877
|
-
related_data =
|
|
755
|
+
related_data = _execute_queryset(related_qs, self.db, container)
|
|
878
756
|
|
|
879
757
|
if nested_relations and related_data:
|
|
880
758
|
related_pks = [r[related_pk_name] for r in related_data]
|
|
@@ -887,16 +765,19 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
887
765
|
related_pks,
|
|
888
766
|
main_results,
|
|
889
767
|
f"{full_path}__",
|
|
768
|
+
container=container,
|
|
890
769
|
)
|
|
891
770
|
related_data = list(nested_dict.values())
|
|
892
771
|
|
|
893
772
|
result: dict[Any, list[dict[str, Any]]] = {pk: [] for pk in parent_pks}
|
|
894
773
|
for row in related_data:
|
|
895
774
|
parent_pk = row[obj_id_field_name]
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
775
|
+
if obj_id_field_name in row:
|
|
776
|
+
del row[obj_id_field_name]
|
|
777
|
+
ct_key = f"{ct_field_name}_id"
|
|
778
|
+
if ct_key in row:
|
|
779
|
+
del row[ct_key]
|
|
780
|
+
result[parent_pk].append(row)
|
|
900
781
|
|
|
901
782
|
return result
|
|
902
783
|
|
|
@@ -905,12 +786,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
905
786
|
lookup: GenericPrefetch[Any],
|
|
906
787
|
parent_pks: list[Any],
|
|
907
788
|
main_results: list[dict[str, Any]],
|
|
789
|
+
*,
|
|
790
|
+
container: _ContainerType = dict,
|
|
908
791
|
) -> dict[Any, dict[str, Any] | None]:
|
|
909
|
-
"""Fetch GenericForeignKey data using GenericPrefetch.
|
|
910
|
-
|
|
911
|
-
GenericForeignKey can point to different model types. GenericPrefetch
|
|
912
|
-
provides a list of querysets, one for each possible content type.
|
|
913
|
-
"""
|
|
792
|
+
"""Fetch GenericForeignKey data using GenericPrefetch."""
|
|
914
793
|
gfk_attr = lookup.prefetch_to
|
|
915
794
|
gfk_descriptor = getattr(self.model, gfk_attr, None)
|
|
916
795
|
if not isinstance(gfk_descriptor, GenericForeignKey):
|
|
@@ -950,7 +829,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
950
829
|
object_ids = [obj_id for _, obj_id in parent_obj_pairs]
|
|
951
830
|
|
|
952
831
|
related_qs = qs.filter(pk__in=object_ids)
|
|
953
|
-
results =
|
|
832
|
+
results = _execute_queryset(related_qs, self.db, container)
|
|
954
833
|
related_data = {r[related_pk_name]: r for r in results}
|
|
955
834
|
|
|
956
835
|
if qs._prefetch_related_lookups: # type: ignore[attr-defined]
|
|
@@ -961,6 +840,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
961
840
|
qs._prefetch_related_lookups, # type: ignore[attr-defined]
|
|
962
841
|
nested_pks,
|
|
963
842
|
list(related_data.values()),
|
|
843
|
+
container=container,
|
|
964
844
|
)
|
|
965
845
|
for pk_val, row_data in related_data.items():
|
|
966
846
|
for attr, data_by_pk in nested_prefetched.items():
|
|
@@ -968,7 +848,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
968
848
|
|
|
969
849
|
for parent_pk, obj_id in parent_obj_pairs:
|
|
970
850
|
if obj_id in related_data:
|
|
971
|
-
result[parent_pk] =
|
|
851
|
+
result[parent_pk] = container(related_data[obj_id])
|
|
972
852
|
|
|
973
853
|
return result
|
|
974
854
|
|
|
@@ -978,14 +858,16 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
978
858
|
prefetch_lookups: tuple[Any, ...],
|
|
979
859
|
parent_pks: list[Any],
|
|
980
860
|
main_results: list[dict[str, Any]],
|
|
861
|
+
*,
|
|
862
|
+
container: _ContainerType = dict,
|
|
981
863
|
) -> dict[str, dict[Any, list[dict[str, Any]] | dict[str, Any] | None]]:
|
|
982
|
-
"""Fetch prefetched relations for a related model
|
|
864
|
+
"""Fetch prefetched relations for a related model."""
|
|
983
865
|
temp_qs = NestedValuesQuerySetMixin.__new__(NestedValuesQuerySetMixin)
|
|
984
866
|
temp_qs.model = model
|
|
985
867
|
temp_qs.db = self.db # type: ignore[misc]
|
|
986
868
|
temp_qs.query = model._default_manager.all().query
|
|
987
869
|
|
|
988
|
-
return temp_qs._fetch_all_prefetched(prefetch_lookups, parent_pks, main_results)
|
|
870
|
+
return temp_qs._fetch_all_prefetched(prefetch_lookups, parent_pks, main_results, container=container)
|
|
989
871
|
|
|
990
872
|
def _add_nested_relations( # noqa: PLR0913
|
|
991
873
|
self,
|
|
@@ -995,6 +877,8 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
995
877
|
parent_pks: list[Any],
|
|
996
878
|
main_results: list[dict[str, Any]] | None = None,
|
|
997
879
|
parent_path: str = "",
|
|
880
|
+
*,
|
|
881
|
+
container: _ContainerType = dict,
|
|
998
882
|
) -> None:
|
|
999
883
|
"""Fetch and add nested relation data to already-fetched parent data."""
|
|
1000
884
|
for nested_rel in nested_relations:
|
|
@@ -1015,6 +899,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1015
899
|
main_results=main_results,
|
|
1016
900
|
parent_path=parent_path,
|
|
1017
901
|
parent_data=data,
|
|
902
|
+
container=container,
|
|
1018
903
|
)
|
|
1019
904
|
|
|
1020
905
|
for pk, row in data.items():
|
|
@@ -1030,10 +915,12 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1030
915
|
parent_path: str = "",
|
|
1031
916
|
parent_data: dict[Any, dict[str, Any]] | None = None,
|
|
1032
917
|
custom_qs: QuerySet[Any, Any] | None = None,
|
|
918
|
+
*,
|
|
919
|
+
container: _ContainerType = dict,
|
|
1033
920
|
) -> dict[Any, list[dict[str, Any]] | dict[str, Any] | None]:
|
|
1034
921
|
"""Dispatch to the appropriate fetch method based on field type."""
|
|
1035
922
|
if isinstance(field, ManyToManyField):
|
|
1036
|
-
return self._fetch_m2m_internal(
|
|
923
|
+
return self._fetch_m2m_internal( # type: ignore[return-value]
|
|
1037
924
|
related_model=cast("type[Model]", field.related_model),
|
|
1038
925
|
accessor=field.related_query_name(),
|
|
1039
926
|
relation_name=field.name,
|
|
@@ -1043,9 +930,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1043
930
|
main_results=main_results,
|
|
1044
931
|
parent_path=parent_path,
|
|
1045
932
|
m2m_field=field,
|
|
1046
|
-
|
|
933
|
+
container=container,
|
|
934
|
+
)
|
|
1047
935
|
if isinstance(field, ManyToOneRel):
|
|
1048
|
-
return self._fetch_reverse_fk_internal(
|
|
936
|
+
return self._fetch_reverse_fk_internal( # type: ignore[return-value]
|
|
1049
937
|
related_model=cast("type[Model]", field.related_model),
|
|
1050
938
|
fk_field_name=field.field.name,
|
|
1051
939
|
relation_name=field.get_accessor_name() or field.name,
|
|
@@ -1054,9 +942,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1054
942
|
custom_qs=custom_qs,
|
|
1055
943
|
main_results=main_results,
|
|
1056
944
|
parent_path=parent_path,
|
|
1057
|
-
|
|
945
|
+
container=container,
|
|
946
|
+
)
|
|
1058
947
|
if isinstance(field, ForeignKey):
|
|
1059
|
-
return self._fetch_fk_internal(
|
|
948
|
+
return self._fetch_fk_internal( # type: ignore[return-value]
|
|
1060
949
|
field=field,
|
|
1061
950
|
nested_relations=nested_relations,
|
|
1062
951
|
parent_pks=parent_pks,
|
|
@@ -1065,9 +954,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1065
954
|
main_results=main_results,
|
|
1066
955
|
parent_path=parent_path,
|
|
1067
956
|
parent_model=parent_model,
|
|
1068
|
-
|
|
957
|
+
container=container,
|
|
958
|
+
)
|
|
1069
959
|
if isinstance(field, ManyToManyRel):
|
|
1070
|
-
return self._fetch_m2m_internal(
|
|
960
|
+
return self._fetch_m2m_internal( # type: ignore[return-value]
|
|
1071
961
|
related_model=cast("type[Model]", field.related_model),
|
|
1072
962
|
accessor=field.field.name,
|
|
1073
963
|
relation_name=field.get_accessor_name() or field.name,
|
|
@@ -1077,9 +967,10 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1077
967
|
main_results=main_results,
|
|
1078
968
|
parent_path=parent_path,
|
|
1079
969
|
m2m_field=field,
|
|
1080
|
-
|
|
970
|
+
container=container,
|
|
971
|
+
)
|
|
1081
972
|
if isinstance(field, GenericRelation):
|
|
1082
|
-
return self._fetch_generic_relation_internal(
|
|
973
|
+
return self._fetch_generic_relation_internal( # type: ignore[return-value]
|
|
1083
974
|
field,
|
|
1084
975
|
nested_relations,
|
|
1085
976
|
parent_pks,
|
|
@@ -1087,7 +978,8 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1087
978
|
custom_qs,
|
|
1088
979
|
main_results,
|
|
1089
980
|
parent_path,
|
|
1090
|
-
|
|
981
|
+
container=container,
|
|
982
|
+
)
|
|
1091
983
|
return {}
|
|
1092
984
|
|
|
1093
985
|
def _is_many_relation(self, field: Any) -> bool:
|
|
@@ -1098,22 +990,14 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
1098
990
|
class NestedValuesQuerySet(NestedValuesQuerySetMixin[_ModelT_co], QuerySet[_ModelT_co, _ModelT_co]):
|
|
1099
991
|
"""QuerySet that adds .values_nested() for nested dictionaries.
|
|
1100
992
|
|
|
1101
|
-
This is a ready-to-use QuerySet combining NestedValuesQuerySetMixin with Django's QuerySet.
|
|
1102
|
-
|
|
1103
993
|
Usage:
|
|
1104
994
|
class Book(models.Model):
|
|
1105
995
|
objects = NestedValuesQuerySet.as_manager()
|
|
1106
996
|
|
|
1107
|
-
|
|
1108
|
-
Book.objects.only("title").select_related("publisher").prefetch_related("authors").values_nested()
|
|
997
|
+
Book.objects.select_related("publisher").prefetch_related("authors").values_nested()
|
|
1109
998
|
# Returns: [{'id': 1, 'title': '...', 'publisher': {...}, 'authors': [...]}, ...]
|
|
1110
999
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
def my_custom_method(self):
|
|
1115
|
-
...
|
|
1116
|
-
|
|
1117
|
-
Type hints: The queryset is generic over the model type. After calling values_nested(),
|
|
1118
|
-
it yields dict[str, Any] when iterated, similar to Django's values() method.
|
|
1000
|
+
With attribute access:
|
|
1001
|
+
Book.objects.values_nested(as_attr_dicts=True)
|
|
1002
|
+
# Returns AttrDict instances: book.title, book.publisher.name work
|
|
1119
1003
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|