djhtmx 1.3.2__tar.gz → 1.3.3__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.
Files changed (40) hide show
  1. {djhtmx-1.3.2 → djhtmx-1.3.3}/CHANGELOG.md +13 -0
  2. {djhtmx-1.3.2 → djhtmx-1.3.3}/PKG-INFO +18 -1
  3. {djhtmx-1.3.2 → djhtmx-1.3.3}/README.md +17 -0
  4. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/__init__.py +1 -1
  5. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/introspection.py +50 -3
  6. {djhtmx-1.3.2 → djhtmx-1.3.3}/.gitignore +0 -0
  7. {djhtmx-1.3.2 → djhtmx-1.3.3}/LICENSE +0 -0
  8. {djhtmx-1.3.2 → djhtmx-1.3.3}/MANIFEST.in +0 -0
  9. {djhtmx-1.3.2 → djhtmx-1.3.3}/pyproject.toml +0 -0
  10. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/apps.py +0 -0
  11. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/command_queue.py +0 -0
  12. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/commands.py +0 -0
  13. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/component.py +0 -0
  14. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/consumer.py +0 -0
  15. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/context.py +0 -0
  16. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/exceptions.py +0 -0
  17. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/global_events.py +0 -0
  18. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/json.py +0 -0
  19. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/management/commands/htmx.py +0 -0
  20. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/middleware.py +0 -0
  21. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/query.py +0 -0
  22. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/repo.py +0 -0
  23. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/settings.py +0 -0
  24. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/ext/idiomorph-ext.min.js +0 -0
  25. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/ext/ws.js +0 -0
  26. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.amd.js +0 -0
  27. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.cjs.js +0 -0
  28. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.esm.d.ts +0 -0
  29. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.esm.js +0 -0
  30. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.js +0 -0
  31. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.min.js +0 -0
  32. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/django.js +0 -0
  33. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templates/htmx/headers.html +0 -0
  34. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templates/htmx/lazy.html +0 -0
  35. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templatetags/__init__.py +0 -0
  36. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templatetags/htmx.py +0 -0
  37. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/testing.py +0 -0
  38. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/tracing.py +0 -0
  39. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/urls.py +0 -0
  40. {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/utils.py +0 -0
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.3] - 2026-01-14
11
+
12
+ ### Fixed
13
+ - **Lazy Model Deleted Object Handling**: Fixed lazy models to gracefully handle deleted database objects:
14
+ - Required lazy models (`Annotated[Model, ModelConfig(lazy=True)]`): Raise clear `ObjectDoesNotExist` exception when checking truthiness if object was deleted
15
+ - Optional lazy models (`Annotated[Model | None, ModelConfig(lazy=True)]`): Become falsy and return `None` for field accesses when object was deleted
16
+ - Accessing `.pk` on lazy proxies always works without triggering database queries, even for deleted objects
17
+ - **ModelConfig Extraction**: Fixed `annotate_model()` to properly extract and apply `ModelConfig` from `Annotated` type metadata. Previously, `ModelConfig` in annotations like `Annotated[Item, ModelConfig(lazy=True)]` was ignored.
18
+
19
+ ### Added
20
+ - Comprehensive test coverage for lazy model deleted object handling using real `HtmxComponent` with user-facing API
21
+ - `__bool__()` method on `_LazyModelProxy` to detect deleted objects immediately when checking truthiness
22
+
10
23
  ## [1.3.2] - 2026-01-14
11
24
 
12
25
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djhtmx
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: Interactive UI Components for Django using HTMX
5
5
  Project-URL: Homepage, https://github.com/edelvalle/djhtmx
6
6
  Project-URL: Documentation, https://github.com/edelvalle/djhtmx#readme
@@ -311,6 +311,23 @@ class TodoComponent(HtmxComponent):
311
311
  ]
312
312
  ```
313
313
 
314
+ **Handling Deleted Objects:**
315
+
316
+ Lazy models handle deleted database objects gracefully:
317
+
318
+ ```python
319
+ class MyComponent(HtmxComponent):
320
+ # Required: raises ObjectDoesNotExist when checking if object was deleted
321
+ item: Annotated[Item, ModelConfig(lazy=True)]
322
+
323
+ # Optional: becomes falsy and returns None when object was deleted
324
+ archived_item: Annotated[Item | None, ModelConfig(lazy=True)]
325
+ ```
326
+
327
+ - **Required lazy models**: Checking truthiness (`if component.item:`) raises `ObjectDoesNotExist` with a clear message
328
+ - **Optional lazy models**: Checking truthiness returns `False`, field accesses return `None`
329
+ - **Both**: Accessing `.pk` always works without triggering database queries
330
+
314
331
  ## Component nesting
315
332
 
316
333
  Components can contain components inside to decompose the behavior in more granular and specialized parts, for this you don't have to do anything but to a component inside the template of other component....
@@ -264,6 +264,23 @@ class TodoComponent(HtmxComponent):
264
264
  ]
265
265
  ```
266
266
 
267
+ **Handling Deleted Objects:**
268
+
269
+ Lazy models handle deleted database objects gracefully:
270
+
271
+ ```python
272
+ class MyComponent(HtmxComponent):
273
+ # Required: raises ObjectDoesNotExist when checking if object was deleted
274
+ item: Annotated[Item, ModelConfig(lazy=True)]
275
+
276
+ # Optional: becomes falsy and returns None when object was deleted
277
+ archived_item: Annotated[Item | None, ModelConfig(lazy=True)]
278
+ ```
279
+
280
+ - **Required lazy models**: Checking truthiness (`if component.item:`) raises `ObjectDoesNotExist` with a clear message
281
+ - **Optional lazy models**: Checking truthiness returns `False`, field accesses return `None`
282
+ - **Both**: Accessing `.pk` always works without triggering database queries
283
+
267
284
  ## Component nesting
268
285
 
269
286
  Components can contain components inside to decompose the behavior in more granular and specialized parts, for this you don't have to do anything but to a component inside the template of other component....
@@ -1,4 +1,4 @@
1
1
  from .middleware import middleware
2
2
 
3
- __version__ = "1.3.2"
3
+ __version__ = "1.3.3"
4
4
  __all__ = ("middleware",)
@@ -78,12 +78,14 @@ class _LazyModelProxy(Generic[M]): # noqa
78
78
  __pk: Any | None
79
79
  __select_related: Sequence[str] | None
80
80
  __prefetch_related: Sequence[str | Prefetch] | None
81
+ __allow_none: bool
81
82
 
82
83
  def __init__(
83
84
  self,
84
85
  model: type[M],
85
86
  value: Any,
86
87
  model_annotation: ModelConfig | None = None,
88
+ allow_none: bool = False,
87
89
  ):
88
90
  self.__model = model
89
91
  if value is None or isinstance(value, model):
@@ -98,12 +100,44 @@ class _LazyModelProxy(Generic[M]): # noqa
98
100
  else:
99
101
  self.__select_related = None
100
102
  self.__prefetch_related = None
103
+ self.__allow_none = allow_none
104
+
105
+ def __bool__(self) -> bool:
106
+ """Check if the instance exists. Called when proxy is used in boolean context."""
107
+ if self.__instance is None:
108
+ self.__ensure_instance()
109
+ if self.__instance is None:
110
+ # Object doesn't exist
111
+ if not self.__allow_none:
112
+ # Required field - raise exception
113
+ from django.core.exceptions import ObjectDoesNotExist
114
+
115
+ raise ObjectDoesNotExist(
116
+ f"{self.__model.__name__} with pk={self.__pk} does not exist "
117
+ "(object may have been deleted)"
118
+ )
119
+ # Optional field - return False (proxy is falsy)
120
+ return False
121
+ return True
101
122
 
102
123
  def __getattr__(self, name: str) -> Any:
103
124
  if name == "pk":
104
125
  return self.__pk
105
126
  if self.__instance is None:
106
127
  self.__ensure_instance()
128
+ if self.__instance is None:
129
+ # Object doesn't exist (was deleted or never existed)
130
+ if self.__allow_none:
131
+ # Optional field (Model | None) - return None gracefully
132
+ return None
133
+ else:
134
+ # Required field (Model) - raise explicit exception
135
+ from django.core.exceptions import ObjectDoesNotExist
136
+
137
+ raise ObjectDoesNotExist(
138
+ f"{self.__model.__name__} with pk={self.__pk} does not exist "
139
+ "(object may have been deleted)"
140
+ )
107
141
  return getattr(self.__instance, name)
108
142
 
109
143
  def __ensure_instance(self):
@@ -143,9 +177,13 @@ class _ModelBeforeValidator(Generic[M]): # noqa
143
177
  return None
144
178
  elif isinstance(value, _LazyModelProxy):
145
179
  instance = value._LazyModelProxy__instance or value._LazyModelProxy__pk
146
- return _LazyModelProxy(self.model, instance, model_annotation=self.model_config)
180
+ return _LazyModelProxy(
181
+ self.model, instance, model_annotation=self.model_config, allow_none=self.allow_none
182
+ )
147
183
  else:
148
- return _LazyModelProxy(self.model, value, model_annotation=self.model_config)
184
+ return _LazyModelProxy(
185
+ self.model, value, model_annotation=self.model_config, allow_none=self.allow_none
186
+ )
149
187
 
150
188
  def _get_instance(self, value):
151
189
  if value is None or isinstance(value, self.model):
@@ -254,7 +292,16 @@ def annotate_model(annotation, *, model_config: ModelConfig | None = None):
254
292
  # Process the base type (first arg) and keep other metadata
255
293
  base_type = args[0]
256
294
  metadata = args[1:]
257
- processed_base = annotate_model(base_type, model_config=model_config)
295
+
296
+ # Extract ModelConfig from metadata if present
297
+ extracted_model_config = next(
298
+ (m for m in metadata if isinstance(m, ModelConfig)),
299
+ None,
300
+ )
301
+ # Use extracted config, falling back to passed parameter
302
+ config_to_use = extracted_model_config or model_config
303
+
304
+ processed_base = annotate_model(base_type, model_config=config_to_use)
258
305
 
259
306
  # If processed_base is also Annotated, merge the metadata
260
307
  if get_origin(processed_base) is Annotated:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes