djhtmx 1.3.3__py3-none-any.whl → 1.3.4__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.3"
3
+ __version__ = "1.3.4"
4
4
  __all__ = ("middleware",)
djhtmx/introspection.py CHANGED
@@ -29,7 +29,6 @@ from django.db import models
29
29
  from django.db.models import Prefetch
30
30
  from django.utils.datastructures import MultiValueDict
31
31
  from pydantic import BeforeValidator, PlainSerializer, TypeAdapter
32
- from pydantic_core import PydanticCustomError
33
32
 
34
33
  M = TypeVar("M", bound=models.Model)
35
34
 
@@ -88,6 +87,7 @@ class _LazyModelProxy(Generic[M]): # noqa
88
87
  allow_none: bool = False,
89
88
  ):
90
89
  self.__model = model
90
+ self.__allow_none = allow_none
91
91
  if value is None or isinstance(value, model):
92
92
  self.__instance = value
93
93
  self.__pk = getattr(value, "pk", None)
@@ -100,60 +100,31 @@ class _LazyModelProxy(Generic[M]): # noqa
100
100
  else:
101
101
  self.__select_related = None
102
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
122
103
 
123
104
  def __getattr__(self, name: str) -> Any:
124
105
  if name == "pk":
125
106
  return self.__pk
126
107
  if self.__instance is None:
127
108
  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
- )
141
109
  return getattr(self.__instance, name)
142
110
 
143
111
  def __ensure_instance(self):
144
- if self.__instance:
145
- return self.__instance
146
- elif self.__pk is None:
147
- # If pk is None, don't try to load anything
148
- return None
149
- else:
112
+ if not self.__instance:
150
113
  manager = self.__model.objects
151
114
  if select_related := self.__select_related:
152
115
  manager = manager.select_related(*select_related)
153
116
  if prefetch_related := self.__prefetch_related:
154
117
  manager = manager.prefetch_related(*prefetch_related)
118
+ # Use filter().first() instead of get() to avoid exceptions
155
119
  self.__instance = manager.filter(pk=self.__pk).first()
156
- return self.__instance
120
+ if self.__instance is None:
121
+ if self.__allow_none:
122
+ # For Model | None, object doesn't exist - proxy becomes None-like
123
+ pass
124
+ else:
125
+ # For required Model fields, raise error
126
+ raise ValueError(f"{self.__model.__name__} with pk={self.__pk} does not exist")
127
+ return self.__instance
157
128
 
158
129
  def __repr__(self) -> str:
159
130
  return f"<_LazyModelProxy model={self.__model}, pk={self.__pk}, instance={self.__instance}>"
@@ -172,18 +143,11 @@ class _ModelBeforeValidator(Generic[M]): # noqa
172
143
  return self._get_instance(value)
173
144
 
174
145
  def _get_lazy_proxy(self, value):
175
- if value is None:
176
- # Don't create a proxy for explicit None
177
- return None
178
- elif isinstance(value, _LazyModelProxy):
146
+ if isinstance(value, _LazyModelProxy):
179
147
  instance = value._LazyModelProxy__instance or value._LazyModelProxy__pk
180
- return _LazyModelProxy(
181
- self.model, instance, model_annotation=self.model_config, allow_none=self.allow_none
182
- )
148
+ return _LazyModelProxy(self.model, instance, allow_none=self.allow_none)
183
149
  else:
184
- return _LazyModelProxy(
185
- self.model, value, model_annotation=self.model_config, allow_none=self.allow_none
186
- )
150
+ return _LazyModelProxy(self.model, value, allow_none=self.allow_none)
187
151
 
188
152
  def _get_instance(self, value):
189
153
  if value is None or isinstance(value, self.model):
@@ -206,11 +170,7 @@ class _ModelBeforeValidator(Generic[M]): # noqa
206
170
  return None
207
171
  else:
208
172
  # For required Model fields, raise validation error
209
- raise PydanticCustomError(
210
- "model_not_found",
211
- f"{self.model.__name__} with pk={{pk}} does not exist",
212
- {"pk": value},
213
- )
173
+ raise ValueError(f"{self.model.__name__} with pk={value} does not exist")
214
174
  return instance
215
175
 
216
176
  @classmethod
@@ -224,11 +184,7 @@ class _ModelPlainSerializer(Generic[M]): # noqa
224
184
  model: type[M]
225
185
 
226
186
  def __call__(self, value):
227
- # Handle None for Model | None fields
228
- if value is None:
229
- return None
230
- else:
231
- return value.pk
187
+ return value.pk
232
188
 
233
189
  @classmethod
234
190
  @cache
@@ -237,16 +193,20 @@ class _ModelPlainSerializer(Generic[M]): # noqa
237
193
 
238
194
 
239
195
  def _Model(
240
- model: type[models.Model], model_config: ModelConfig | None = None, allow_none: bool = False
196
+ model: type[models.Model],
197
+ model_config: ModelConfig | None = None,
198
+ allow_none: bool = False,
241
199
  ):
242
200
  assert issubclass_safe(model, models.Model)
243
201
  model_config = model_config or _DEFAULT_MODEL_CONFIG
202
+
203
+ # Determine the base type
244
204
  base_type = model if not model_config.lazy else _LazyModelProxy[model]
245
- # If allow_none is True, the base type can be None (for Model | None unions)
246
- if allow_none:
247
- base_type = base_type | None # type: ignore
205
+ # If allow_none, make it optional
206
+ annotated_type = base_type | None if allow_none else base_type
207
+
248
208
  return Annotated[
249
- base_type,
209
+ annotated_type,
250
210
  BeforeValidator(_ModelBeforeValidator.from_modelclass(model, model_config, allow_none)),
251
211
  PlainSerializer(
252
212
  func=_ModelPlainSerializer.from_modelclass(model),
@@ -285,63 +245,45 @@ def annotate_model(annotation, *, model_config: ModelConfig | None = None):
285
245
  },
286
246
  )
287
247
  elif type_ := get_origin(annotation):
288
- # Handle Annotated types like Annotated[Item | None, Query("editing")]
289
- if type_ is Annotated:
290
- args = get_args(annotation)
291
- if args:
292
- # Process the base type (first arg) and keep other metadata
293
- base_type = args[0]
294
- metadata = args[1:]
295
-
296
- # Extract ModelConfig from metadata if present
297
- extracted_model_config = next(
298
- (m for m in metadata if isinstance(m, ModelConfig)),
248
+ if type_ is types.UnionType or type_ is Union:
249
+ type_ = Union
250
+ match get_args(annotation):
251
+ case ():
252
+ return type_
253
+ case (param,):
254
+ return type_[annotate_model(param)] # type: ignore
255
+ case params:
256
+ # Check for ModelConfig in params (for Annotated types)
257
+ param_model_config = next(
258
+ (p for p in params if isinstance(p, ModelConfig)),
299
259
  None,
300
260
  )
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)
305
-
306
- # If processed_base is also Annotated, merge the metadata
307
- if get_origin(processed_base) is Annotated:
308
- processed_args = get_args(processed_base)
309
- inner_base = processed_args[0]
310
- inner_metadata = processed_args[1:]
311
- # Merge: inner metadata first, then original metadata
312
- return Annotated[inner_base, *inner_metadata, *metadata] # type: ignore
261
+ # Use param ModelConfig if found, otherwise use the passed model_config
262
+ effective_model_config = param_model_config or model_config
263
+
264
+ # Check if this is a Model | None union
265
+ has_none = types.NoneType in params
266
+ model_types = [p for p in params if issubclass_safe(p, models.Model)]
267
+
268
+ # If we have Model | None, annotate the model with allow_none=True
269
+ if has_none and len(model_types) == 1:
270
+ annotated_params = []
271
+ for p in params:
272
+ if issubclass_safe(p, models.Model):
273
+ annotated_params.append(
274
+ _Model(p, effective_model_config, allow_none=True)
275
+ )
276
+ elif p is not types.NoneType:
277
+ annotated_params.append(
278
+ annotate_model(p, model_config=effective_model_config)
279
+ )
280
+ else:
281
+ annotated_params.append(p)
282
+ return type_[*annotated_params] # type: ignore
313
283
  else:
314
- # Reconstruct the Annotated with processed base type
315
- return Annotated[processed_base, *metadata] # type: ignore
316
- return annotation
317
- elif type_ is types.UnionType or type_ is Union:
318
- type_ = Union
319
- match get_args(annotation):
320
- case ():
321
- return type_
322
- case (param,):
323
- return type_[annotate_model(param)] # type: ignore
324
- case params:
325
- model_annotation = next(
326
- (p for p in params if isinstance(p, ModelConfig)),
327
- None,
328
- )
329
- # Check if this is a Model | None union
330
- has_none = types.NoneType in params
331
- model_params = [p for p in params if issubclass_safe(p, models.Model)]
332
-
333
- if has_none and len(model_params) == 1:
334
- # This is a Model | None union - use allow_none=True
335
- # Use the model_config parameter passed to annotate_model, not model_annotation from Union params
336
- model = model_params[0]
337
- return _Model(model, model_config or model_annotation, allow_none=True)
338
- else:
339
- # Regular union - process each param independently
340
- return type_[
341
- *(annotate_model(p, model_config=model_annotation) for p in params)
342
- ] # type: ignore
343
- # Other generic types (list, dict, defaultdict, etc.) - return as-is
344
- return annotation
284
+ return type_[
285
+ *(annotate_model(p, model_config=effective_model_config) for p in params)
286
+ ] # type: ignore
345
287
  else:
346
288
  return annotation
347
289
 
@@ -519,27 +461,10 @@ def is_basic_type(ann):
519
461
  - Literal types with simple values
520
462
 
521
463
  """
522
- # Check if it's a Union (e.g., Item | None)
523
- origin_type = get_origin(ann)
524
- if origin_type in (types.UnionType, Union):
525
- args = get_args(ann)
526
- # If it's Model | None, consider it a basic type
527
- model_types = [arg for arg in args if issubclass_safe(arg, models.Model)]
528
- if model_types and types.NoneType in args:
529
- return True
530
-
531
- # Check for Annotated[Model, ...] or Annotated[Model | None, ...] pattern
532
- origin = getattr(ann, "__origin__", None)
533
- if origin is not None and get_origin(origin) in (types.UnionType, Union):
534
- args = get_args(origin)
535
- model_types = [arg for arg in args if issubclass_safe(arg, models.Model)]
536
- if model_types and types.NoneType in args:
537
- return True
538
-
539
464
  return (
540
465
  ann in _SIMPLE_TYPES
541
466
  # __origin__ -> model in 'Annotated[model, BeforeValidator(...), PlainSerializer(...)]'
542
- or issubclass_safe(origin, models.Model)
467
+ or issubclass_safe(getattr(ann, "__origin__", None), models.Model)
543
468
  or issubclass_safe(ann, (enum.IntEnum, enum.StrEnum))
544
469
  or is_collection_annotation(ann)
545
470
  or is_literal_annotation(ann)
djhtmx/repo.py CHANGED
@@ -240,7 +240,11 @@ class Repository:
240
240
  Emit(HtmxUnhandledError(error, handler_annotations=annotations))
241
241
  ]
242
242
  yield from self._process_emited_commands(
243
- component, emited_commands, commands, during_execute=True
243
+ component,
244
+ emited_commands,
245
+ commands,
246
+ during_execute=True,
247
+ method_name=event_handler,
244
248
  )
245
249
 
246
250
  case SkipRender(component):
@@ -286,7 +290,11 @@ class Repository:
286
290
  else:
287
291
  raise
288
292
  yield from self._process_emited_commands(
289
- component, emited_commands, commands, during_execute=False
293
+ component,
294
+ emited_commands,
295
+ commands,
296
+ during_execute=False,
297
+ method_name="_handle_event",
290
298
  )
291
299
 
292
300
  case Signal(signals):
@@ -320,10 +328,13 @@ class Repository:
320
328
  emmited_commands: Iterable[Command] | None,
321
329
  commands: CommandQueue,
322
330
  during_execute: bool,
331
+ method_name: str | None = None,
323
332
  ) -> Iterable[ProcessedCommand]:
324
333
  component_was_rendered = False
325
334
  commands_to_add: list[Command] = []
326
335
  for command in emmited_commands or []:
336
+ if method_name:
337
+ logger.debug("< YIELD: %s.%s -> %s", component.hx_name, method_name, command)
327
338
  component_was_rendered = component_was_rendered or (
328
339
  isinstance(command, SkipRender | Render) and command.component.id == component.id
329
340
  )
djhtmx/utils.py CHANGED
@@ -130,6 +130,8 @@ def autodiscover_htmx_modules():
130
130
  - All Python files under htmx/ directories in apps (recursively)
131
131
  """
132
132
  for app_config in apps.get_app_configs():
133
+ if app_config.module is None:
134
+ continue
133
135
  module_name = f"{app_config.module.__name__}.htmx"
134
136
  spec = importlib.util.find_spec(module_name)
135
137
  if spec is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djhtmx
3
- Version: 1.3.3
3
+ Version: 1.3.4
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
@@ -1,4 +1,4 @@
1
- djhtmx/__init__.py,sha256=VL_7rY7fcdRsK0N7SZK5nteSMhURTdTtuExuNLQaZBs,84
1
+ djhtmx/__init__.py,sha256=mQgvMxtgLk8jKYzYLn1bhXKZ6f3YOzex0el1-J_A4w8,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,16 +7,16 @@ 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=s1hJ5EO-DcLOZwBiZSUAcnBsCi1ewwPlER1cFPO1QNo,19907
10
+ djhtmx/introspection.py,sha256=QZOaX7tWftMZCjRVXYGHxaCxSksoT2wljd3lIMaz9ic,16463
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
14
- djhtmx/repo.py,sha256=Qw8t9i5_upfvYJyUr4Cb9hZoI9WUHhNOpszxjhtx9r4,22478
14
+ djhtmx/repo.py,sha256=BZrxLCxcqJudCsHvlL_bmkjhvbsFMAP7sZLCTxTRiCI,22906
15
15
  djhtmx/settings.py,sha256=Iti4LkcKBTy-dNyCZxFH_cUp56aTcXjB5ftbssWyDnU,1318
16
16
  djhtmx/testing.py,sha256=QmZHrH6Up8uUxkVOHM6CyfocU774GL-lopJvM2X9Mkw,8369
17
17
  djhtmx/tracing.py,sha256=xkCXb7t_3yCj1PGzmQfHPu9sYQftDKwtALaEbFVnQ1E,1260
18
18
  djhtmx/urls.py,sha256=2LTzmBCd3lBlQcM6WrdQwkITxuL_4ArUHtiYbLk3T1M,4273
19
- djhtmx/utils.py,sha256=PhFKUpDA6rS6kGkbkP3V0Md8-RQDEiARMoE6399KBy0,4649
19
+ djhtmx/utils.py,sha256=XyRF3RD7_wOLIu6dVwxnUm828oGwKcPAbgq8oMd27m8,4708
20
20
  djhtmx/management/commands/htmx.py,sha256=tEtiJn_Z6byOFzBNIzTbdluA4T5q21zFwGvJ7yt90bw,3642
21
21
  djhtmx/static/htmx/django.js,sha256=QDgkUBiX9PBa_bqfK2k4NwjBAgZlxYEK-JklOgn_IR4,5453
22
22
  djhtmx/static/htmx/2.0.4/htmx.amd.js,sha256=Hgmm_X5zw7ek0pjBaxhzH7OHx6Xfce5UYVa9ICWlWR0,165593
@@ -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.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,,
34
+ djhtmx-1.3.4.dist-info/METADATA,sha256=2slsTxKtB7VvVAvU4PhlNV82_mk3L5vAZVodSWg07uw,33745
35
+ djhtmx-1.3.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ djhtmx-1.3.4.dist-info/licenses/LICENSE,sha256=kCi_iSBUGsRZInQn96w7LXYzjiRjZ8FXl6vP--mFRPk,1085
37
+ djhtmx-1.3.4.dist-info/RECORD,,
File without changes