djhtmx 1.1.2__py3-none-any.whl → 1.2.1__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.1.2"
3
+ __version__ = "1.2.1"
4
4
  __all__ = ("middleware",)
djhtmx/apps.py CHANGED
@@ -1,11 +1,13 @@
1
1
  from django.apps import AppConfig
2
2
  from django.utils.module_loading import autodiscover_modules
3
3
 
4
+ from .utils import autodiscover_htmx_modules
5
+
4
6
 
5
7
  class App(AppConfig):
6
8
  name = "djhtmx"
7
9
  verbose_name = "Django HTMX"
8
10
 
9
11
  def ready(self):
10
- autodiscover_modules("live")
11
- autodiscover_modules("htmx")
12
+ autodiscover_modules("live") # legacy
13
+ autodiscover_htmx_modules()
djhtmx/component.py CHANGED
@@ -20,6 +20,7 @@ from typing import (
20
20
  get_type_hints,
21
21
  )
22
22
 
23
+ from django.core.exceptions import ImproperlyConfigured
23
24
  from django.db import models
24
25
  from django.shortcuts import resolve_url
25
26
  from django.template import Context, loader
@@ -306,9 +307,8 @@ class HtmxComponent(BaseModel):
306
307
  basename(cls._template_name.default)
307
308
  not in (f"{klass.__name__}.html" for klass in cls.__mro__)
308
309
  ):
309
- logger.warning(
310
- "HTMX Component <%s> template name does not match the component name",
311
- FQN[cls],
310
+ raise ImproperlyConfigured(
311
+ f"HTMX Component <{FQN[cls]}> template name does not match the component name"
312
312
  )
313
313
 
314
314
  # We use 'get_type_hints' to resolve the forward refs if needed, but
djhtmx/introspection.py CHANGED
@@ -13,6 +13,7 @@ from typing import (
13
13
  Annotated,
14
14
  Any,
15
15
  Generic,
16
+ Literal,
16
17
  TypedDict,
17
18
  TypeVar,
18
19
  Union,
@@ -381,6 +382,11 @@ infallible_bool_adapter = TypeAdapter(
381
382
  )
382
383
 
383
384
 
385
+ def is_literal_annotation(ann):
386
+ """Returns True if the annotation is a Literal type with simple values."""
387
+ return get_origin(ann) is Literal and all(type(arg) in _SIMPLE_TYPES for arg in get_args(ann))
388
+
389
+
384
390
  def is_basic_type(ann):
385
391
  """Returns True if the annotation is a simple type.
386
392
 
@@ -395,6 +401,8 @@ def is_basic_type(ann):
395
401
 
396
402
  - Instances of dict, tuple, list or set
397
403
 
404
+ - Literal types with simple values
405
+
398
406
  """
399
407
  return (
400
408
  ann in _SIMPLE_TYPES
@@ -402,6 +410,7 @@ def is_basic_type(ann):
402
410
  or issubclass_safe(getattr(ann, "__origin__", None), models.Model)
403
411
  or issubclass_safe(ann, (enum.IntEnum, enum.StrEnum))
404
412
  or is_collection_annotation(ann)
413
+ or is_literal_annotation(ann)
405
414
  )
406
415
 
407
416
 
@@ -46,6 +46,58 @@ def check_missing(fname):
46
46
  sys.exit(1)
47
47
 
48
48
 
49
+ @htmx.command("check-unused") # type: ignore
50
+ @click.argument("fname", type=click.File())
51
+ def check_unused(fname):
52
+ r"""Check if there are any unused HTMX components.
53
+
54
+ Expected usage:
55
+
56
+ find -type f -name '*.html' | while read f; do grep -P '{% htmx .(\\w+)' -o $f \
57
+ | awk '{print $3}' | cut -b2-; done | sort -u \
58
+ | python manage.py htmx check-unused -
59
+
60
+ """
61
+ names = {n.strip() for n in fname.readlines()}
62
+ known = set(REGISTRY)
63
+ unused = list(known - names)
64
+ if unused:
65
+ unused.sort()
66
+ for n in unused:
67
+ click.echo(
68
+ f"Unused component detected {bold(yellow(n))}",
69
+ file=sys.stderr,
70
+ )
71
+ sys.exit(1)
72
+
73
+
74
+ @htmx.command("check-unused-non-public") # type: ignore
75
+ def check_unused_non_public():
76
+ """Check if there are any unused non-public HTMX components.
77
+
78
+ Non-public components that are final subclasses (have no subclasses themselves)
79
+ are considered unused since they can't be instantiated from templates and serve
80
+ no purpose as base classes.
81
+ """
82
+ final_subclasses = set(
83
+ get_final_subclasses(
84
+ HtmxComponent, # type: ignore
85
+ without_duplicates=True,
86
+ )
87
+ )
88
+ registered = set(REGISTRY.values())
89
+ unused_non_public = list(final_subclasses - registered)
90
+
91
+ if unused_non_public:
92
+ unused_non_public.sort(key=lambda cls: cls.__name__)
93
+ for cls in unused_non_public:
94
+ click.echo(
95
+ f"Unused non-public component detected {bold(yellow(cls.__name__))}",
96
+ file=sys.stderr,
97
+ )
98
+ sys.exit(1)
99
+
100
+
49
101
  @htmx.command("check-shadowing") # type: ignore
50
102
  def check_shadowing():
51
103
  "Checks if there are components that might shadow one another."
djhtmx/utils.py CHANGED
@@ -1,8 +1,12 @@
1
+ import contextlib
2
+ import importlib
3
+ import pkgutil
1
4
  import typing as t
2
5
  from urllib.parse import urlparse
3
6
 
4
7
  import mmh3
5
8
  from channels.db import database_sync_to_async as db # type: ignore
9
+ from django.apps import apps
6
10
  from django.db import models
7
11
  from django.http.request import HttpRequest, QueryDict
8
12
  from uuid6 import uuid7
@@ -115,3 +119,27 @@ def compact_hash(value: str) -> str:
115
119
  # The symbols are chosen to avoid extra encoding in the URL and HTML, and
116
120
  # allowed in plain CSS selectors.
117
121
  _BASE = "ZmBeUHhTgusXNW_Y1b05KPiFcQJD86joqnIRE7Lfkrdp3AOMCvltSwzVG9yxa42"
122
+
123
+
124
+ def autodiscover_htmx_modules():
125
+ """
126
+ Auto-discover HTMX modules in Django apps.
127
+
128
+ This discovers both:
129
+ - htmx.py files (like standard autodiscover_modules("htmx"))
130
+ - All Python files under htmx/ directories in apps (recursively)
131
+ """
132
+
133
+ def _import_modules_recursively(module_name):
134
+ """Recursively import a module and all its submodules."""
135
+ with contextlib.suppress(ImportError):
136
+ module = importlib.import_module(module_name)
137
+
138
+ # If this is a package, recursively import all modules in it
139
+ if hasattr(module, "__path__"):
140
+ for _finder, submodule_name, _is_pkg in pkgutil.iter_modules(module.__path__):
141
+ _import_modules_recursively(f"{module_name}.{submodule_name}")
142
+
143
+ for app_config in apps.get_app_configs():
144
+ # Import htmx module and all its submodules recursively
145
+ _import_modules_recursively(f"{app_config.name}.htmx")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djhtmx
3
- Version: 1.1.2
3
+ Version: 1.2.1
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
@@ -184,6 +184,33 @@ This library is opinionated about how to use HTMX with Django, but it is not opi
184
184
 
185
185
  This app will look for `htmx.py` files in your app and registers all components found there, but if you load any module where you have components manually when Django boots up, that also works.
186
186
 
187
+ ### Component Organization
188
+
189
+ As of version 1.2.0, djhtmx supports both single file and directory-based component organization:
190
+
191
+ **Single file (traditional):**
192
+ ```
193
+ myapp/
194
+ ├── htmx.py # All components in one file
195
+ └── ...
196
+ ```
197
+
198
+ **Directory structure (new in v1.2.0):**
199
+ ```
200
+ myapp/
201
+ ├── htmx/
202
+ │ ├── __init__.py
203
+ │ ├── components.py # Basic components
204
+ │ ├── forms.py # Form components
205
+ │ └── widgets/
206
+ │ ├── __init__.py
207
+ │ ├── calendar.py # Calendar widgets
208
+ │ └── charts.py # Chart widgets
209
+ └── ...
210
+ ```
211
+
212
+ The autodiscovery system will recursively find and import all Python modules under `htmx/` directories, allowing you to organize your components in a structured way that scales with your project size.
213
+
187
214
  ```python
188
215
  from djhtmx.component import HtmxComponent
189
216
 
@@ -1,13 +1,13 @@
1
- djhtmx/__init__.py,sha256=FJ6V7xlcgRBLI2mcRzdK5-zP1d1bUGHrkeqdTtUUOE0,84
2
- djhtmx/apps.py,sha256=_Ic52zQLpbYmyuCAlgZ0lF3NDgi77sxptb31snBAN4o,268
1
+ djhtmx/__init__.py,sha256=ADstIQLho1ikMNCKiMEwgQdR5tM1cP5NiOy3Lpn1cq8,84
2
+ djhtmx/apps.py,sha256=hAyjzmInEstxLY9k8Qn58LvNlezgQLx5_NqyVL1WwYs,323
3
3
  djhtmx/command_queue.py,sha256=kiYbQFPyjnhMSR7KgO1Nu-lWiapnH511P2Pyg-Zrdq4,4862
4
4
  djhtmx/commands.py,sha256=UxXbARd4Teetjh_zjvAWgI2KNbvdETH-WrGf4qD9Xr8,1206
5
- djhtmx/component.py,sha256=viqrizs85e6zlGh3Zlf2n2HxfBg1cOaSp5deVGPRGXY,15739
5
+ djhtmx/component.py,sha256=0Z5gAbhOg7lKXzbwDGmVJVv_IA6WYtzrWxZo_ww9CpY,15789
6
6
  djhtmx/consumer.py,sha256=kHNoXokcWaFjs5zbZAhM7Y0x7GVwwawXbxBCkP8HNA8,2839
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=OVNaQ8r6G13HMftydfW2UVCfs2dzy34HET2Tb7bGnN4,13431
10
+ djhtmx/introspection.py,sha256=flVolO6xZiXsxMm876ZCEcWRmVfFsJWpAVmIdfcJNf8,13734
11
11
  djhtmx/json.py,sha256=7cjwWIJj7e0dk54INKYZJe6zKkIW7wlsNSlD05cbXfY,1374
12
12
  djhtmx/middleware.py,sha256=JuMtv9ZnpungTvQ1qD2Lg6LiFPB3knQlA1ERgH4iGl0,1274
13
13
  djhtmx/query.py,sha256=UyjN1jokh4wTwQJxcRwA9f-Zn-A7A4GLToeGrCnPhKA,6674
@@ -16,8 +16,8 @@ djhtmx/settings.py,sha256=ymFUMvrcXDkYU9KkhPOjRZSQMWz5GcUjlgh07x09-1s,1242
16
16
  djhtmx/testing.py,sha256=AdZKsT6sNTsyqSKx6EmfthOIHzSAPkTquheMfg9znbk,8301
17
17
  djhtmx/tracing.py,sha256=xkCXb7t_3yCj1PGzmQfHPu9sYQftDKwtALaEbFVnQ1E,1260
18
18
  djhtmx/urls.py,sha256=zWMlw_udCUWvo5DNxsvbebSNRFxy0C9ghBmRg08XlcU,3894
19
- djhtmx/utils.py,sha256=gjsJZrjrr7FDh-8wv9W5j3EWNlDl_Eztr5IeMvdY2BE,3617
20
- djhtmx/management/commands/htmx.py,sha256=EtJhQofJ4Dl3s34Uihz4WbSljzy5R6r0HXGaX4vkcDg,2011
19
+ djhtmx/utils.py,sha256=BcCdJHe0AqkRT_Kj-XJT_sHCpOyXtumo9mQGN2WqHek,4646
20
+ djhtmx/management/commands/htmx.py,sha256=tEtiJn_Z6byOFzBNIzTbdluA4T5q21zFwGvJ7yt90bw,3642
21
21
  djhtmx/static/htmx/django.js,sha256=G59uwy5hA4QUcAFJv21SMxizATpNZG3KfgFlO2zXeGc,7086
22
22
  djhtmx/static/htmx/2.0.4/htmx.amd.js,sha256=Hgmm_X5zw7ek0pjBaxhzH7OHx6Xfce5UYVa9ICWlWR0,165593
23
23
  djhtmx/static/htmx/2.0.4/htmx.cjs.js,sha256=4P3vh1eGwULBCT7wsKQ2bu4HiNQ_Kmnv2fP1RQ6_QW8,165586
@@ -30,7 +30,7 @@ djhtmx/templates/htmx/headers.html,sha256=rBQTBt9rnlxE8lgxN4U7nvzQZNw4JZKS4flD1V
30
30
  djhtmx/templates/htmx/lazy.html,sha256=LfAThtKmFj-lCUZ7JWF_sC1Y6XsIpEz8A3IgWASn-J8,52
31
31
  djhtmx/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  djhtmx/templatetags/htmx.py,sha256=HlH7_B9lJoTDoIkYPeEE55OwpBTrrCesE70j1KcRC70,8063
33
- djhtmx-1.1.2.dist-info/METADATA,sha256=vmEvS7oSkUGd2P-AI6cgBFJ2zCABXjYRWeWmACPmmbY,31423
34
- djhtmx-1.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
- djhtmx-1.1.2.dist-info/licenses/LICENSE,sha256=kCi_iSBUGsRZInQn96w7LXYzjiRjZ8FXl6vP--mFRPk,1085
36
- djhtmx-1.1.2.dist-info/RECORD,,
33
+ djhtmx-1.2.1.dist-info/METADATA,sha256=11WVimnQxDR07_6WtvEqxP0oeEQYdLrer3vOaYpVpOM,32245
34
+ djhtmx-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ djhtmx-1.2.1.dist-info/licenses/LICENSE,sha256=kCi_iSBUGsRZInQn96w7LXYzjiRjZ8FXl6vP--mFRPk,1085
36
+ djhtmx-1.2.1.dist-info/RECORD,,
File without changes