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.
- {djhtmx-1.3.2 → djhtmx-1.3.3}/CHANGELOG.md +13 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/PKG-INFO +18 -1
- {djhtmx-1.3.2 → djhtmx-1.3.3}/README.md +17 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/__init__.py +1 -1
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/introspection.py +50 -3
- {djhtmx-1.3.2 → djhtmx-1.3.3}/.gitignore +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/LICENSE +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/MANIFEST.in +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/pyproject.toml +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/apps.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/command_queue.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/commands.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/component.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/consumer.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/context.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/exceptions.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/global_events.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/json.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/management/commands/htmx.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/middleware.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/query.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/repo.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/settings.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/ext/idiomorph-ext.min.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/ext/ws.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.amd.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.cjs.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.esm.d.ts +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.esm.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/2.0.4/htmx.min.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/static/htmx/django.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templates/htmx/headers.html +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templates/htmx/lazy.html +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templatetags/__init__.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/templatetags/htmx.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/testing.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/tracing.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.3}/src/djhtmx/urls.py +0 -0
- {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.
|
|
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....
|
|
@@ -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(
|
|
180
|
+
return _LazyModelProxy(
|
|
181
|
+
self.model, instance, model_annotation=self.model_config, allow_none=self.allow_none
|
|
182
|
+
)
|
|
147
183
|
else:
|
|
148
|
-
return _LazyModelProxy(
|
|
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
|
-
|
|
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
|
|
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
|