django-nested-values 0.7.0__tar.gz → 1.0.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.7.0 → django_nested_values-1.0.0}/PKG-INFO +1 -1
- {django_nested_values-0.7.0 → django_nested_values-1.0.0}/django_nested_values/__init__.py +2 -3
- {django_nested_values-0.7.0 → django_nested_values-1.0.0}/django_nested_values/queryset.py +17 -76
- {django_nested_values-0.7.0 → django_nested_values-1.0.0}/pyproject.toml +2 -2
- {django_nested_values-0.7.0 → django_nested_values-1.0.0}/.gitignore +0 -0
- {django_nested_values-0.7.0 → django_nested_values-1.0.0}/LICENSE +0 -0
- {django_nested_values-0.7.0 → django_nested_values-1.0.0}/README.md +0 -0
- {django_nested_values-0.7.0 → django_nested_values-1.0.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: 1.0.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/
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Django Nested Values - Enable .prefetch_related().values_nested() in Django ORM."""
|
|
2
2
|
|
|
3
3
|
from django_nested_values.queryset import (
|
|
4
|
-
AttrDict,
|
|
5
4
|
NestedValuesQuerySet,
|
|
6
5
|
NestedValuesQuerySetMixin,
|
|
7
6
|
)
|
|
8
7
|
|
|
9
|
-
__all__ = ["
|
|
10
|
-
__version__ = "0.
|
|
8
|
+
__all__ = ["NestedValuesQuerySet", "NestedValuesQuerySetMixin"]
|
|
9
|
+
__version__ = "1.0.0"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Generic,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar, cast
|
|
6
6
|
|
|
7
7
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
|
8
8
|
from django.contrib.contenttypes.models import ContentType
|
|
@@ -15,40 +15,10 @@ 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
|
|
18
|
+
# Type alias for dict-like container classes (kept for future custom container support)
|
|
19
19
|
_ContainerType = type[dict[str, Any]]
|
|
20
20
|
|
|
21
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
|
-
|
|
52
22
|
if TYPE_CHECKING:
|
|
53
23
|
from collections.abc import Iterator
|
|
54
24
|
|
|
@@ -67,7 +37,7 @@ def _build_from_klass_info(
|
|
|
67
37
|
select: list[tuple[Any, ...]],
|
|
68
38
|
container: _ContainerType = dict,
|
|
69
39
|
) -> dict[str, Any]:
|
|
70
|
-
"""Build a dict
|
|
40
|
+
"""Build a dict directly from a row using Django's compiler metadata.
|
|
71
41
|
|
|
72
42
|
This uses Django's internal compiler metadata to know exactly which
|
|
73
43
|
columns belong to which model, avoiding manual field path parsing.
|
|
@@ -76,7 +46,7 @@ def _build_from_klass_info(
|
|
|
76
46
|
row: A tuple of values from the database row
|
|
77
47
|
klass_info: The klass_info dict from compiler
|
|
78
48
|
select: The compiler.select list
|
|
79
|
-
container: The dict-like class to use
|
|
49
|
+
container: The dict-like class to use for results
|
|
80
50
|
|
|
81
51
|
Returns:
|
|
82
52
|
A container instance with field names as keys
|
|
@@ -92,6 +62,8 @@ def _build_from_klass_info(
|
|
|
92
62
|
for related_ki in klass_info.get("related_klass_infos", []):
|
|
93
63
|
pk_idx = related_ki["select_fields"][0]
|
|
94
64
|
if row[pk_idx] is None:
|
|
65
|
+
# Include NULL FK as None rather than omitting the key
|
|
66
|
+
result[related_ki["field"].name] = None
|
|
95
67
|
continue
|
|
96
68
|
result[related_ki["field"].name] = _build_from_klass_info(row, related_ki, select, container)
|
|
97
69
|
|
|
@@ -103,12 +75,12 @@ def _execute_queryset(
|
|
|
103
75
|
db: str,
|
|
104
76
|
container: _ContainerType = dict,
|
|
105
77
|
) -> list[dict[str, Any]]:
|
|
106
|
-
"""Execute a queryset and return results as nested dicts
|
|
78
|
+
"""Execute a queryset and return results as nested dicts.
|
|
107
79
|
|
|
108
80
|
Args:
|
|
109
81
|
queryset: The queryset to execute
|
|
110
82
|
db: Database alias to use
|
|
111
|
-
container: The dict-like class to use
|
|
83
|
+
container: The dict-like class to use for results
|
|
112
84
|
|
|
113
85
|
Returns:
|
|
114
86
|
List of nested containers, one per row
|
|
@@ -141,7 +113,7 @@ def _execute_prefetch(
|
|
|
141
113
|
Args:
|
|
142
114
|
queryset: The queryset to execute
|
|
143
115
|
db: Database alias to use
|
|
144
|
-
container: The dict-like class to use
|
|
116
|
+
container: The dict-like class to use for results
|
|
145
117
|
|
|
146
118
|
Returns:
|
|
147
119
|
Tuple of (list of model containers, list of extra column dicts for grouping)
|
|
@@ -190,7 +162,7 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
190
162
|
queryset: NestedValuesQuerySetMixin[Any]
|
|
191
163
|
|
|
192
164
|
def __iter__(self) -> Iterator[dict[str, Any]]:
|
|
193
|
-
"""Iterate over the queryset, yielding nested dictionaries
|
|
165
|
+
"""Iterate over the queryset, yielding nested dictionaries."""
|
|
194
166
|
queryset = self.queryset
|
|
195
167
|
db = queryset.db
|
|
196
168
|
prefetch_lookups = getattr(queryset, "_nested_prefetch_lookups", ())
|
|
@@ -211,12 +183,11 @@ class NestedValuesIterable(BaseIterable): # type: ignore[type-arg]
|
|
|
211
183
|
if klass_info is None:
|
|
212
184
|
return
|
|
213
185
|
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
container: _ContainerType = AttrDict if as_attr_dicts else dict # type: ignore[assignment]
|
|
186
|
+
# Container class for results (kept configurable for potential future use)
|
|
187
|
+
container: _ContainerType = dict
|
|
217
188
|
pk_name = queryset.model._meta.pk.name
|
|
218
189
|
|
|
219
|
-
# Build main results
|
|
190
|
+
# Build main results from database rows
|
|
220
191
|
main_results = [
|
|
221
192
|
_build_from_klass_info(row, klass_info, select, container) for row in compiler.results_iter(results)
|
|
222
193
|
]
|
|
@@ -320,48 +291,22 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
320
291
|
"""
|
|
321
292
|
|
|
322
293
|
_nested_prefetch_lookups: tuple[Any, ...] = ()
|
|
323
|
-
_as_attr_dicts: bool = False
|
|
324
294
|
|
|
325
295
|
def _clone(self) -> Self:
|
|
326
296
|
"""Clone the queryset, preserving our custom attributes."""
|
|
327
297
|
clone: Self = super()._clone() # type: ignore[misc]
|
|
328
298
|
clone._nested_prefetch_lookups = self._nested_prefetch_lookups
|
|
329
|
-
clone._as_attr_dicts = self._as_attr_dicts
|
|
330
299
|
return clone
|
|
331
300
|
|
|
332
|
-
|
|
333
|
-
def values_nested(
|
|
334
|
-
self,
|
|
335
|
-
*,
|
|
336
|
-
as_attr_dicts: Literal[False] = ...,
|
|
337
|
-
) -> QuerySet[_ModelT_co, dict[str, Any]]: ...
|
|
338
|
-
|
|
339
|
-
@overload
|
|
340
|
-
def values_nested(
|
|
341
|
-
self,
|
|
342
|
-
*,
|
|
343
|
-
as_attr_dicts: Literal[True],
|
|
344
|
-
) -> QuerySet[_ModelT_co, AttrDict]: ...
|
|
345
|
-
|
|
346
|
-
def values_nested(
|
|
347
|
-
self,
|
|
348
|
-
*,
|
|
349
|
-
as_attr_dicts: bool = False,
|
|
350
|
-
) -> QuerySet[_ModelT_co, dict[str, Any]] | QuerySet[_ModelT_co, AttrDict]:
|
|
301
|
+
def values_nested(self) -> QuerySet[_ModelT_co, dict[str, Any]]:
|
|
351
302
|
"""Return nested dictionaries with related objects included.
|
|
352
303
|
|
|
353
|
-
Args:
|
|
354
|
-
as_attr_dicts: If True, return AttrDict instances instead of plain dicts.
|
|
355
|
-
AttrDict supports attribute access (book.title) in addition to
|
|
356
|
-
dict access (book["title"]).
|
|
357
|
-
|
|
358
304
|
Returns:
|
|
359
|
-
A QuerySet that yields dict[str, Any]
|
|
305
|
+
A QuerySet that yields dict[str, Any] when iterated.
|
|
360
306
|
|
|
361
307
|
"""
|
|
362
308
|
clone: Self = self._clone() # type: ignore[assignment]
|
|
363
309
|
clone._iterable_class = NestedValuesIterable
|
|
364
|
-
clone._as_attr_dicts = as_attr_dicts
|
|
365
310
|
clone._nested_prefetch_lookups = clone._prefetch_related_lookups # type: ignore[attr-defined]
|
|
366
311
|
clone._prefetch_related_lookups = () # type: ignore[attr-defined]
|
|
367
312
|
return clone # type: ignore[return-value]
|
|
@@ -548,7 +493,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
548
493
|
}
|
|
549
494
|
related_qs = related_qs.extra(select=extra_select) # noqa: S610
|
|
550
495
|
|
|
551
|
-
# Build directly into container
|
|
496
|
+
# Build directly into container - no conversion needed
|
|
552
497
|
containers, extra_values = _execute_prefetch(related_qs, self.db, container)
|
|
553
498
|
if not containers:
|
|
554
499
|
return {pk: [] for pk in parent_pks}
|
|
@@ -612,7 +557,7 @@ class NestedValuesQuerySetMixin(_MixinBase[_ModelT_co]):
|
|
|
612
557
|
else:
|
|
613
558
|
related_qs = related_model._default_manager.filter(**{f"{fk_field_name}__in": parent_pks})
|
|
614
559
|
|
|
615
|
-
# Build directly into container
|
|
560
|
+
# Build directly into container
|
|
616
561
|
related_data = _execute_queryset(related_qs, self.db, container)
|
|
617
562
|
if not related_data:
|
|
618
563
|
return {pk: [] for pk in parent_pks}
|
|
@@ -996,8 +941,4 @@ class NestedValuesQuerySet(NestedValuesQuerySetMixin[_ModelT_co], QuerySet[_Mode
|
|
|
996
941
|
|
|
997
942
|
Book.objects.select_related("publisher").prefetch_related("authors").values_nested()
|
|
998
943
|
# Returns: [{'id': 1, 'title': '...', 'publisher': {...}, 'authors': [...]}, ...]
|
|
999
|
-
|
|
1000
|
-
With attribute access:
|
|
1001
|
-
Book.objects.values_nested(as_attr_dicts=True)
|
|
1002
|
-
# Returns AttrDict instances: book.title, book.publisher.name work
|
|
1003
944
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "django-nested-values"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Add .values_nested() to Django querysets for nested prefetch dictionaries"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -63,7 +63,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
|
63
63
|
explicit-preview-rules = true
|
|
64
64
|
fixable = ["ALL"]
|
|
65
65
|
ignore = [
|
|
66
|
-
"ANN401", # Any needed for
|
|
66
|
+
"ANN401", # Any needed for dynamic dict values and Django internals
|
|
67
67
|
"COM812", # Conflicts with ruff-format
|
|
68
68
|
"E5", # 120 char limit is sufficient
|
|
69
69
|
"SLF001", # Private member access needed for Django internals
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|