djhtmx 1.3.1__py3-none-any.whl → 1.3.3__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from .middleware import middleware
2
2
 
3
- __version__ = "1.3.1"
3
+ __version__ = "1.3.3"
4
4
  __all__ = ("middleware",)
djhtmx/introspection.py CHANGED
@@ -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:
@@ -293,6 +340,8 @@ def annotate_model(annotation, *, model_config: ModelConfig | None = None):
293
340
  return type_[
294
341
  *(annotate_model(p, model_config=model_annotation) for p in params)
295
342
  ] # type: ignore
343
+ # Other generic types (list, dict, defaultdict, etc.) - return as-is
344
+ return annotation
296
345
  else:
297
346
  return annotation
298
347
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djhtmx
3
- Version: 1.3.1
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....
@@ -1,4 +1,4 @@
1
- djhtmx/__init__.py,sha256=sFHexB1yrCT8QBiug5AGMLj4zmcax_WDdxeUL6DYYoQ,84
1
+ djhtmx/__init__.py,sha256=VL_7rY7fcdRsK0N7SZK5nteSMhURTdTtuExuNLQaZBs,84
2
2
  djhtmx/apps.py,sha256=hAyjzmInEstxLY9k8Qn58LvNlezgQLx5_NqyVL1WwYs,323
3
3
  djhtmx/command_queue.py,sha256=LSUkb2YMRt1lDyOg6WP7PoHsObynec0B55JyFtcshT0,5090
4
4
  djhtmx/commands.py,sha256=UxXbARd4Teetjh_zjvAWgI2KNbvdETH-WrGf4qD9Xr8,1206
@@ -7,7 +7,7 @@ djhtmx/consumer.py,sha256=0Yh8urgMH3khA6_pWeY049w3jqHWZL_K9dErOhNctQA,2898
7
7
  djhtmx/context.py,sha256=cWvz8Z0MC6x_G8sn5mvoH8Hu38qReY21_eNdThuba1A,214
8
8
  djhtmx/exceptions.py,sha256=UtyE1N-52OmzwgRM9xFxjUuhHTMDvD7Oy3rNpgthLcs,47
9
9
  djhtmx/global_events.py,sha256=bYb8WmQn_WsZ_Dadr0pGiGOPia01K-VanPpM97Lt324,342
10
- djhtmx/introspection.py,sha256=HGtKTFNG2FhJkLftQC7bUiJPhyyBZtix0BCOdlraTqE,17807
10
+ djhtmx/introspection.py,sha256=s1hJ5EO-DcLOZwBiZSUAcnBsCi1ewwPlER1cFPO1QNo,19907
11
11
  djhtmx/json.py,sha256=7cjwWIJj7e0dk54INKYZJe6zKkIW7wlsNSlD05cbXfY,1374
12
12
  djhtmx/middleware.py,sha256=JuMtv9ZnpungTvQ1qD2Lg6LiFPB3knQlA1ERgH4iGl0,1274
13
13
  djhtmx/query.py,sha256=GKubKKZ1Z6krzLjG4GZpcxAyPy-pCMCtyfSVfXrpy90,6855
@@ -31,7 +31,7 @@ djhtmx/templates/htmx/headers.html,sha256=z7r9klwBDXDyjbHrzatZeHDvXB2DaZhgu55CFb
31
31
  djhtmx/templates/htmx/lazy.html,sha256=LfAThtKmFj-lCUZ7JWF_sC1Y6XsIpEz8A3IgWASn-J8,52
32
32
  djhtmx/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  djhtmx/templatetags/htmx.py,sha256=-qFqz4T9mCJocG9XIIey81cCYwk07XUd_DMpxNdmbsM,8397
34
- djhtmx-1.3.1.dist-info/METADATA,sha256=8ZRSOBEA730-0kym22awNPktiV6c715fDTvON-DI5o0,33050
35
- djhtmx-1.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
- djhtmx-1.3.1.dist-info/licenses/LICENSE,sha256=kCi_iSBUGsRZInQn96w7LXYzjiRjZ8FXl6vP--mFRPk,1085
37
- djhtmx-1.3.1.dist-info/RECORD,,
34
+ djhtmx-1.3.3.dist-info/METADATA,sha256=5UCGB6iJel1PhQxBnImgvcGp-n6jgfhWUEmNtz68Bck,33745
35
+ djhtmx-1.3.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ djhtmx-1.3.3.dist-info/licenses/LICENSE,sha256=kCi_iSBUGsRZInQn96w7LXYzjiRjZ8FXl6vP--mFRPk,1085
37
+ djhtmx-1.3.3.dist-info/RECORD,,
File without changes