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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-nested-values
3
- Version: 0.7.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__ = ["AttrDict", "NestedValuesQuerySet", "NestedValuesQuerySetMixin"]
10
- __version__ = "0.7.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, Literal, Self, TypeVar, cast, overload
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 (or dict subclass like AttrDict) directly from a row.
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 (dict or AttrDict)
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 (or AttrDict).
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 (dict or AttrDict)
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 (dict or AttrDict)
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 (or AttrDict)."""
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
- # 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]
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 - unified path for both dict and AttrDict
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
- @overload
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] or AttrDict when iterated.
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 (dict or AttrDict) - no conversion needed
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 (dict or AttrDict)
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.7.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 AttrDict's dynamic attribute access
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