djhtmx 1.3.2__tar.gz → 1.3.4__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.4}/CHANGELOG.md +17 -12
- {djhtmx-1.3.2 → djhtmx-1.3.4}/PKG-INFO +18 -1
- {djhtmx-1.3.2 → djhtmx-1.3.4}/README.md +17 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/__init__.py +1 -1
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/introspection.py +66 -94
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/repo.py +13 -2
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/utils.py +2 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/.gitignore +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/LICENSE +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/MANIFEST.in +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/pyproject.toml +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/apps.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/command_queue.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/commands.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/component.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/consumer.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/context.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/exceptions.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/global_events.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/json.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/management/commands/htmx.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/middleware.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/query.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/settings.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/ext/idiomorph-ext.min.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/ext/ws.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/htmx.amd.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/htmx.cjs.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/htmx.esm.d.ts +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/htmx.esm.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/htmx.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/2.0.4/htmx.min.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/static/htmx/django.js +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/templates/htmx/headers.html +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/templates/htmx/lazy.html +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/templatetags/__init__.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/templatetags/htmx.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/testing.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/tracing.py +0 -0
- {djhtmx-1.3.2 → djhtmx-1.3.4}/src/djhtmx/urls.py +0 -0
|
@@ -7,23 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [1.3.
|
|
10
|
+
## [1.3.4] - 2026-01-19
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- **Generic Type Handling**: Fixed `annotate_model()` to preserve non-Model generic types (like `defaultdict`, `list`, `dict`) when used with `Annotated` wrappers. Previously, these types would be incorrectly transformed to `None`, causing validation errors.
|
|
12
|
+
**Note**: This release supersedes versions 1.3.1, 1.3.2, and 1.3.3, which contained incomplete implementations of the `Model | None` handling feature. Users on 1.3.1-1.3.3 should upgrade to 1.3.4 immediately.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
### Added
|
|
15
|
+
- **Yield Logging**: Added debug logging for commands yielded from component methods during event handling. Logs format: `< YIELD: ComponentName.method_name -> Command(...)`. Helps developers track command flow and debug issues when components emit multiple commands.
|
|
16
|
+
- Comprehensive test coverage for `Model | None` and lazy model handling with 12 new tests
|
|
16
17
|
|
|
17
18
|
### Fixed
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
19
|
+
- **Model | None Handling**: Fixed components with `Model | None` fields to gracefully return `None` when objects don't exist or have been deleted, instead of raising `DoesNotExist` exceptions
|
|
20
|
+
- Changed database lookups from `manager.get()` to `manager.filter().first()` for graceful handling
|
|
21
|
+
- For optional fields (`Model | None`), returns `None` when object doesn't exist
|
|
22
|
+
- For required fields (`Model`), raises clear `ValueError` with descriptive message
|
|
23
|
+
- Works correctly with both eager and lazy loading (`ModelConfig(lazy=True)`)
|
|
24
|
+
- Lazy models create proxies that handle non-existent objects when attributes are accessed
|
|
25
|
+
- **Type Safety**: Added None check for `app_config.module` in `autodiscover_htmx_modules()` to prevent AttributeError
|
|
23
26
|
|
|
24
|
-
###
|
|
25
|
-
-
|
|
26
|
-
- Enhanced `
|
|
27
|
+
### Technical Details
|
|
28
|
+
- Added `allow_none` parameter to `_ModelBeforeValidator` and `_LazyModelProxy` classes
|
|
29
|
+
- Enhanced `annotate_model()` to detect `Model | None` unions and pass `allow_none=True`
|
|
30
|
+
- Updated lazy proxy `__ensure_instance()` to use `filter().first()` and handle missing objects gracefully
|
|
31
|
+
- QuerySet fields continue to work correctly by silently filtering out non-existent IDs
|
|
27
32
|
|
|
28
33
|
## [1.3.0] - 2026-01-07
|
|
29
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: djhtmx
|
|
3
|
-
Version: 1.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
|
|
@@ -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....
|
|
@@ -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
|
|
|
@@ -78,14 +77,17 @@ class _LazyModelProxy(Generic[M]): # noqa
|
|
|
78
77
|
__pk: Any | None
|
|
79
78
|
__select_related: Sequence[str] | None
|
|
80
79
|
__prefetch_related: Sequence[str | Prefetch] | None
|
|
80
|
+
__allow_none: bool
|
|
81
81
|
|
|
82
82
|
def __init__(
|
|
83
83
|
self,
|
|
84
84
|
model: type[M],
|
|
85
85
|
value: Any,
|
|
86
86
|
model_annotation: ModelConfig | None = None,
|
|
87
|
+
allow_none: bool = False,
|
|
87
88
|
):
|
|
88
89
|
self.__model = model
|
|
90
|
+
self.__allow_none = allow_none
|
|
89
91
|
if value is None or isinstance(value, model):
|
|
90
92
|
self.__instance = value
|
|
91
93
|
self.__pk = getattr(value, "pk", None)
|
|
@@ -107,19 +109,22 @@ class _LazyModelProxy(Generic[M]): # noqa
|
|
|
107
109
|
return getattr(self.__instance, name)
|
|
108
110
|
|
|
109
111
|
def __ensure_instance(self):
|
|
110
|
-
if self.__instance:
|
|
111
|
-
return self.__instance
|
|
112
|
-
elif self.__pk is None:
|
|
113
|
-
# If pk is None, don't try to load anything
|
|
114
|
-
return None
|
|
115
|
-
else:
|
|
112
|
+
if not self.__instance:
|
|
116
113
|
manager = self.__model.objects
|
|
117
114
|
if select_related := self.__select_related:
|
|
118
115
|
manager = manager.select_related(*select_related)
|
|
119
116
|
if prefetch_related := self.__prefetch_related:
|
|
120
117
|
manager = manager.prefetch_related(*prefetch_related)
|
|
118
|
+
# Use filter().first() instead of get() to avoid exceptions
|
|
121
119
|
self.__instance = manager.filter(pk=self.__pk).first()
|
|
122
|
-
|
|
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
|
|
123
128
|
|
|
124
129
|
def __repr__(self) -> str:
|
|
125
130
|
return f"<_LazyModelProxy model={self.__model}, pk={self.__pk}, instance={self.__instance}>"
|
|
@@ -138,14 +143,11 @@ class _ModelBeforeValidator(Generic[M]): # noqa
|
|
|
138
143
|
return self._get_instance(value)
|
|
139
144
|
|
|
140
145
|
def _get_lazy_proxy(self, value):
|
|
141
|
-
if value
|
|
142
|
-
# Don't create a proxy for explicit None
|
|
143
|
-
return None
|
|
144
|
-
elif isinstance(value, _LazyModelProxy):
|
|
146
|
+
if isinstance(value, _LazyModelProxy):
|
|
145
147
|
instance = value._LazyModelProxy__instance or value._LazyModelProxy__pk
|
|
146
|
-
return _LazyModelProxy(self.model, instance,
|
|
148
|
+
return _LazyModelProxy(self.model, instance, allow_none=self.allow_none)
|
|
147
149
|
else:
|
|
148
|
-
return _LazyModelProxy(self.model, value,
|
|
150
|
+
return _LazyModelProxy(self.model, value, allow_none=self.allow_none)
|
|
149
151
|
|
|
150
152
|
def _get_instance(self, value):
|
|
151
153
|
if value is None or isinstance(value, self.model):
|
|
@@ -168,11 +170,7 @@ class _ModelBeforeValidator(Generic[M]): # noqa
|
|
|
168
170
|
return None
|
|
169
171
|
else:
|
|
170
172
|
# For required Model fields, raise validation error
|
|
171
|
-
raise
|
|
172
|
-
"model_not_found",
|
|
173
|
-
f"{self.model.__name__} with pk={{pk}} does not exist",
|
|
174
|
-
{"pk": value},
|
|
175
|
-
)
|
|
173
|
+
raise ValueError(f"{self.model.__name__} with pk={value} does not exist")
|
|
176
174
|
return instance
|
|
177
175
|
|
|
178
176
|
@classmethod
|
|
@@ -186,11 +184,7 @@ class _ModelPlainSerializer(Generic[M]): # noqa
|
|
|
186
184
|
model: type[M]
|
|
187
185
|
|
|
188
186
|
def __call__(self, value):
|
|
189
|
-
|
|
190
|
-
if value is None:
|
|
191
|
-
return None
|
|
192
|
-
else:
|
|
193
|
-
return value.pk
|
|
187
|
+
return value.pk
|
|
194
188
|
|
|
195
189
|
@classmethod
|
|
196
190
|
@cache
|
|
@@ -199,16 +193,20 @@ class _ModelPlainSerializer(Generic[M]): # noqa
|
|
|
199
193
|
|
|
200
194
|
|
|
201
195
|
def _Model(
|
|
202
|
-
model: type[models.Model],
|
|
196
|
+
model: type[models.Model],
|
|
197
|
+
model_config: ModelConfig | None = None,
|
|
198
|
+
allow_none: bool = False,
|
|
203
199
|
):
|
|
204
200
|
assert issubclass_safe(model, models.Model)
|
|
205
201
|
model_config = model_config or _DEFAULT_MODEL_CONFIG
|
|
202
|
+
|
|
203
|
+
# Determine the base type
|
|
206
204
|
base_type = model if not model_config.lazy else _LazyModelProxy[model]
|
|
207
|
-
# If allow_none
|
|
208
|
-
if allow_none
|
|
209
|
-
|
|
205
|
+
# If allow_none, make it optional
|
|
206
|
+
annotated_type = base_type | None if allow_none else base_type
|
|
207
|
+
|
|
210
208
|
return Annotated[
|
|
211
|
-
|
|
209
|
+
annotated_type,
|
|
212
210
|
BeforeValidator(_ModelBeforeValidator.from_modelclass(model, model_config, allow_none)),
|
|
213
211
|
PlainSerializer(
|
|
214
212
|
func=_ModelPlainSerializer.from_modelclass(model),
|
|
@@ -247,54 +245,45 @@ def annotate_model(annotation, *, model_config: ModelConfig | None = None):
|
|
|
247
245
|
},
|
|
248
246
|
)
|
|
249
247
|
elif type_ := get_origin(annotation):
|
|
250
|
-
|
|
251
|
-
if type_ is Annotated:
|
|
252
|
-
args = get_args(annotation)
|
|
253
|
-
if args:
|
|
254
|
-
# Process the base type (first arg) and keep other metadata
|
|
255
|
-
base_type = args[0]
|
|
256
|
-
metadata = args[1:]
|
|
257
|
-
processed_base = annotate_model(base_type, model_config=model_config)
|
|
258
|
-
|
|
259
|
-
# If processed_base is also Annotated, merge the metadata
|
|
260
|
-
if get_origin(processed_base) is Annotated:
|
|
261
|
-
processed_args = get_args(processed_base)
|
|
262
|
-
inner_base = processed_args[0]
|
|
263
|
-
inner_metadata = processed_args[1:]
|
|
264
|
-
# Merge: inner metadata first, then original metadata
|
|
265
|
-
return Annotated[inner_base, *inner_metadata, *metadata] # type: ignore
|
|
266
|
-
else:
|
|
267
|
-
# Reconstruct the Annotated with processed base type
|
|
268
|
-
return Annotated[processed_base, *metadata] # type: ignore
|
|
269
|
-
return annotation
|
|
270
|
-
elif type_ is types.UnionType or type_ is Union:
|
|
248
|
+
if type_ is types.UnionType or type_ is Union:
|
|
271
249
|
type_ = Union
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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)),
|
|
259
|
+
None,
|
|
260
|
+
)
|
|
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
|
|
283
|
+
else:
|
|
284
|
+
return type_[
|
|
285
|
+
*(annotate_model(p, model_config=effective_model_config) for p in params)
|
|
286
|
+
] # type: ignore
|
|
298
287
|
else:
|
|
299
288
|
return annotation
|
|
300
289
|
|
|
@@ -472,27 +461,10 @@ def is_basic_type(ann):
|
|
|
472
461
|
- Literal types with simple values
|
|
473
462
|
|
|
474
463
|
"""
|
|
475
|
-
# Check if it's a Union (e.g., Item | None)
|
|
476
|
-
origin_type = get_origin(ann)
|
|
477
|
-
if origin_type in (types.UnionType, Union):
|
|
478
|
-
args = get_args(ann)
|
|
479
|
-
# If it's Model | None, consider it a basic type
|
|
480
|
-
model_types = [arg for arg in args if issubclass_safe(arg, models.Model)]
|
|
481
|
-
if model_types and types.NoneType in args:
|
|
482
|
-
return True
|
|
483
|
-
|
|
484
|
-
# Check for Annotated[Model, ...] or Annotated[Model | None, ...] pattern
|
|
485
|
-
origin = getattr(ann, "__origin__", None)
|
|
486
|
-
if origin is not None and get_origin(origin) in (types.UnionType, Union):
|
|
487
|
-
args = get_args(origin)
|
|
488
|
-
model_types = [arg for arg in args if issubclass_safe(arg, models.Model)]
|
|
489
|
-
if model_types and types.NoneType in args:
|
|
490
|
-
return True
|
|
491
|
-
|
|
492
464
|
return (
|
|
493
465
|
ann in _SIMPLE_TYPES
|
|
494
466
|
# __origin__ -> model in 'Annotated[model, BeforeValidator(...), PlainSerializer(...)]'
|
|
495
|
-
or issubclass_safe(
|
|
467
|
+
or issubclass_safe(getattr(ann, "__origin__", None), models.Model)
|
|
496
468
|
or issubclass_safe(ann, (enum.IntEnum, enum.StrEnum))
|
|
497
469
|
or is_collection_annotation(ann)
|
|
498
470
|
or is_literal_annotation(ann)
|
|
@@ -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,
|
|
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,
|
|
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
|
)
|
|
@@ -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:
|
|
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
|