django-components-lite 0.1.0__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.
Files changed (39) hide show
  1. django_components_lite-0.1.0/.gitignore +31 -0
  2. django_components_lite-0.1.0/LICENSE +22 -0
  3. django_components_lite-0.1.0/PKG-INFO +128 -0
  4. django_components_lite-0.1.0/README.md +101 -0
  5. django_components_lite-0.1.0/django_components_lite/__init__.py +72 -0
  6. django_components_lite-0.1.0/django_components_lite/app_settings.py +316 -0
  7. django_components_lite-0.1.0/django_components_lite/apps.py +46 -0
  8. django_components_lite-0.1.0/django_components_lite/attributes.py +434 -0
  9. django_components_lite-0.1.0/django_components_lite/autodiscovery.py +45 -0
  10. django_components_lite-0.1.0/django_components_lite/component.py +2051 -0
  11. django_components_lite-0.1.0/django_components_lite/component_media.py +70 -0
  12. django_components_lite-0.1.0/django_components_lite/component_registry.py +540 -0
  13. django_components_lite-0.1.0/django_components_lite/components/__init__.py +1 -0
  14. django_components_lite-0.1.0/django_components_lite/constants.py +3 -0
  15. django_components_lite-0.1.0/django_components_lite/context.py +42 -0
  16. django_components_lite-0.1.0/django_components_lite/dependencies.py +34 -0
  17. django_components_lite-0.1.0/django_components_lite/finders.py +166 -0
  18. django_components_lite-0.1.0/django_components_lite/library.py +47 -0
  19. django_components_lite-0.1.0/django_components_lite/node.py +594 -0
  20. django_components_lite-0.1.0/django_components_lite/py.typed +0 -0
  21. django_components_lite-0.1.0/django_components_lite/slots.py +1513 -0
  22. django_components_lite-0.1.0/django_components_lite/template.py +320 -0
  23. django_components_lite-0.1.0/django_components_lite/template_loader.py +32 -0
  24. django_components_lite-0.1.0/django_components_lite/templatetags/__init__.py +0 -0
  25. django_components_lite-0.1.0/django_components_lite/templatetags/component_tags.py +42 -0
  26. django_components_lite-0.1.0/django_components_lite/util/__init__.py +0 -0
  27. django_components_lite-0.1.0/django_components_lite/util/cache.py +115 -0
  28. django_components_lite-0.1.0/django_components_lite/util/context.py +151 -0
  29. django_components_lite-0.1.0/django_components_lite/util/django_monkeypatch.py +172 -0
  30. django_components_lite-0.1.0/django_components_lite/util/exception.py +78 -0
  31. django_components_lite-0.1.0/django_components_lite/util/loader.py +228 -0
  32. django_components_lite-0.1.0/django_components_lite/util/logger.py +104 -0
  33. django_components_lite-0.1.0/django_components_lite/util/misc.py +316 -0
  34. django_components_lite-0.1.0/django_components_lite/util/nanoid.py +28 -0
  35. django_components_lite-0.1.0/django_components_lite/util/template_parser.py +219 -0
  36. django_components_lite-0.1.0/django_components_lite/util/template_tag.py +103 -0
  37. django_components_lite-0.1.0/django_components_lite/util/types.py +28 -0
  38. django_components_lite-0.1.0/django_components_lite/util/weakref.py +28 -0
  39. django_components_lite-0.1.0/pyproject.toml +177 -0
@@ -0,0 +1,31 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Unit test / coverage reports
13
+ htmlcov/
14
+ .coverage
15
+ .coverage.*
16
+ coverage.xml
17
+ .pytest_cache/
18
+
19
+ # Django
20
+ *.log
21
+ *.sqlite3
22
+
23
+ # Python environment
24
+ .venv/
25
+ .mypy_cache/
26
+ .ruff_cache/
27
+
28
+ # Build artifacts
29
+ site/
30
+ .DS_Store
31
+ .claude/worktrees/
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Emil Stenström
4
+ Copyright (c) 2026 Oliver Haas
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-components-lite
3
+ Version: 0.1.0
4
+ Summary: Lightweight reusable template components for Django. An exploratory fork of django-components.
5
+ Project-URL: Homepage, https://github.com/oliverhaas/django-components-lite
6
+ Project-URL: Documentation, https://oliverhaas.github.io/django-components-lite/
7
+ Project-URL: Issues, https://github.com/oliverhaas/django-components-lite/issues
8
+ Author-email: Oliver Haas <ohaas@e1plus.de>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: components,css,django,html,js
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: django<7,>=5.2
26
+ Description-Content-Type: text/markdown
27
+
28
+ # django-components-lite
29
+
30
+ **An exploratory, lightweight fork of [django-components](https://github.com/django-components/django-components).**
31
+
32
+ This package strips django-components down to its core: simple, reusable template components for Django, just templates with some optional python logic. The goal is to see how a minimal django-components feels in practice.
33
+
34
+ ## Attribution
35
+
36
+ This project is built on the excellent work of the **[django-components](https://github.com/django-components/django-components)** project by **[Emil Stenström](https://github.com/EmilStenstrom)**, **[Juro Oravec](https://github.com/JuroOravec)**, and [all contributors](https://github.com/django-components/django-components/graphs/contributors). Their years of work made this possible.
37
+
38
+ **If you're looking for a mature, full-featured, and battle-tested component library for Django, use [django-components](https://github.com/django-components/django-components).** It has an active community, extensive documentation, and a rich feature set.
39
+
40
+
41
+ ## Features
42
+
43
+ What django-components-lite keeps from django-components:
44
+
45
+ - Component classes with Python logic and Django templates
46
+ - `{% component %}` / `{% endcomponent %}` template tags
47
+ - Slots and fills (`{% slot %}`, `{% fill %}`)
48
+ - Component autodiscovery
49
+ - Component registry
50
+ - Static file handling (JS/CSS)
51
+ - Isolated component context
52
+ - HTML attribute rendering utilities
53
+
54
+ ## What's removed?
55
+
56
+ Compared to django-components, the following have been stripped out:
57
+
58
+ - Extension system
59
+ - Built-in components (DynamicComponent, ErrorFallback)
60
+ - Component caching
61
+ - Provide/Inject system
62
+ - Template expressions
63
+ - Management commands
64
+ - JS/CSS data methods and dependency management
65
+ - Type validation (Args/Kwargs/Slots/TemplateData)
66
+ - `on_render()` generator system and deferred rendering
67
+ - `context_behavior` setting (always isolated, like Django's `inclusion_tag`)
68
+ - Tag formatters
69
+ - Component views and URLs
70
+ - `libraries` setting and `import_libraries()`
71
+ - `reload_on_file_change` setting
72
+ - All deprecated setting aliases
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install django-components-lite
78
+ ```
79
+
80
+ Add to your Django settings:
81
+
82
+ ```python
83
+ INSTALLED_APPS = [
84
+ # ...
85
+ "django_components_lite",
86
+ ]
87
+ ```
88
+
89
+ ## Quick example
90
+
91
+ ```python
92
+ # myapp/components/greeting/greeting.py
93
+ from django_components_lite import Component
94
+
95
+ class Greeting(Component):
96
+ template_name = "greeting/greeting.html"
97
+
98
+ def get_context_data(self, name):
99
+ return {"name": name}
100
+ ```
101
+
102
+ ```html
103
+ <!-- myapp/components/greeting/greeting.html -->
104
+ <div class="greeting">
105
+ Hello, {{ name }}!
106
+ {% slot "extra" %}{% endslot %}
107
+ </div>
108
+ ```
109
+
110
+ ```html
111
+ <!-- In any template -->
112
+ {% load component_tags %}
113
+ {% component "greeting" name="World" %}
114
+ {% fill "extra" %}
115
+ <p>Welcome!</p>
116
+ {% endfill %}
117
+ {% endcomponent %}
118
+ ```
119
+
120
+ ## Links
121
+
122
+ - [django-components (original)](https://github.com/django-components/django-components) - the full-featured upstream project
123
+ - [Documentation](https://oliverhaas.github.io/django-components-lite/)
124
+ - [Issues](https://github.com/oliverhaas/django-components-lite/issues)
125
+
126
+ ## License
127
+
128
+ MIT - see [LICENSE](LICENSE).
@@ -0,0 +1,101 @@
1
+ # django-components-lite
2
+
3
+ **An exploratory, lightweight fork of [django-components](https://github.com/django-components/django-components).**
4
+
5
+ This package strips django-components down to its core: simple, reusable template components for Django, just templates with some optional python logic. The goal is to see how a minimal django-components feels in practice.
6
+
7
+ ## Attribution
8
+
9
+ This project is built on the excellent work of the **[django-components](https://github.com/django-components/django-components)** project by **[Emil Stenström](https://github.com/EmilStenstrom)**, **[Juro Oravec](https://github.com/JuroOravec)**, and [all contributors](https://github.com/django-components/django-components/graphs/contributors). Their years of work made this possible.
10
+
11
+ **If you're looking for a mature, full-featured, and battle-tested component library for Django, use [django-components](https://github.com/django-components/django-components).** It has an active community, extensive documentation, and a rich feature set.
12
+
13
+
14
+ ## Features
15
+
16
+ What django-components-lite keeps from django-components:
17
+
18
+ - Component classes with Python logic and Django templates
19
+ - `{% component %}` / `{% endcomponent %}` template tags
20
+ - Slots and fills (`{% slot %}`, `{% fill %}`)
21
+ - Component autodiscovery
22
+ - Component registry
23
+ - Static file handling (JS/CSS)
24
+ - Isolated component context
25
+ - HTML attribute rendering utilities
26
+
27
+ ## What's removed?
28
+
29
+ Compared to django-components, the following have been stripped out:
30
+
31
+ - Extension system
32
+ - Built-in components (DynamicComponent, ErrorFallback)
33
+ - Component caching
34
+ - Provide/Inject system
35
+ - Template expressions
36
+ - Management commands
37
+ - JS/CSS data methods and dependency management
38
+ - Type validation (Args/Kwargs/Slots/TemplateData)
39
+ - `on_render()` generator system and deferred rendering
40
+ - `context_behavior` setting (always isolated, like Django's `inclusion_tag`)
41
+ - Tag formatters
42
+ - Component views and URLs
43
+ - `libraries` setting and `import_libraries()`
44
+ - `reload_on_file_change` setting
45
+ - All deprecated setting aliases
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install django-components-lite
51
+ ```
52
+
53
+ Add to your Django settings:
54
+
55
+ ```python
56
+ INSTALLED_APPS = [
57
+ # ...
58
+ "django_components_lite",
59
+ ]
60
+ ```
61
+
62
+ ## Quick example
63
+
64
+ ```python
65
+ # myapp/components/greeting/greeting.py
66
+ from django_components_lite import Component
67
+
68
+ class Greeting(Component):
69
+ template_name = "greeting/greeting.html"
70
+
71
+ def get_context_data(self, name):
72
+ return {"name": name}
73
+ ```
74
+
75
+ ```html
76
+ <!-- myapp/components/greeting/greeting.html -->
77
+ <div class="greeting">
78
+ Hello, {{ name }}!
79
+ {% slot "extra" %}{% endslot %}
80
+ </div>
81
+ ```
82
+
83
+ ```html
84
+ <!-- In any template -->
85
+ {% load component_tags %}
86
+ {% component "greeting" name="World" %}
87
+ {% fill "extra" %}
88
+ <p>Welcome!</p>
89
+ {% endfill %}
90
+ {% endcomponent %}
91
+ ```
92
+
93
+ ## Links
94
+
95
+ - [django-components (original)](https://github.com/django-components/django-components) - the full-featured upstream project
96
+ - [Documentation](https://oliverhaas.github.io/django-components-lite/)
97
+ - [Issues](https://github.com/oliverhaas/django-components-lite/issues)
98
+
99
+ ## License
100
+
101
+ MIT - see [LICENSE](LICENSE).
@@ -0,0 +1,72 @@
1
+ """Main package for Django Components."""
2
+
3
+ # Public API
4
+ # isort: off
5
+ from django_components_lite.app_settings import ComponentsSettings
6
+ from django_components_lite.attributes import format_attributes, merge_attributes
7
+ from django_components_lite.autodiscovery import autodiscover
8
+ from django_components_lite.component import (
9
+ Component,
10
+ ComponentNode,
11
+ ComponentVars,
12
+ all_components,
13
+ get_component_by_class_id,
14
+ )
15
+ from django_components_lite.component_registry import (
16
+ AlreadyRegisteredError,
17
+ ComponentRegistry,
18
+ NotRegisteredError,
19
+ RegistrySettings,
20
+ register,
21
+ registry,
22
+ all_registries,
23
+ )
24
+
25
+ from django_components_lite.library import TagProtectedError
26
+ from django_components_lite.slots import (
27
+ FillNode,
28
+ Slot,
29
+ SlotContext,
30
+ SlotFallback,
31
+ SlotFunc,
32
+ SlotInput,
33
+ SlotNode,
34
+ SlotResult,
35
+ )
36
+ from django_components_lite.util.loader import ComponentFileEntry, get_component_dirs, get_component_files
37
+ from django_components_lite.util.types import Empty
38
+
39
+ # isort: on
40
+
41
+
42
+ __all__ = [
43
+ "AlreadyRegisteredError",
44
+ "Component",
45
+ "ComponentFileEntry",
46
+ "ComponentNode",
47
+ "ComponentRegistry",
48
+ "ComponentVars",
49
+ "ComponentsSettings",
50
+ "Empty",
51
+ "FillNode",
52
+ "NotRegisteredError",
53
+ "RegistrySettings",
54
+ "Slot",
55
+ "SlotContext",
56
+ "SlotFallback",
57
+ "SlotFunc",
58
+ "SlotInput",
59
+ "SlotNode",
60
+ "SlotResult",
61
+ "TagProtectedError",
62
+ "all_components",
63
+ "all_registries",
64
+ "autodiscover",
65
+ "format_attributes",
66
+ "get_component_by_class_id",
67
+ "get_component_dirs",
68
+ "get_component_files",
69
+ "merge_attributes",
70
+ "register",
71
+ "registry",
72
+ ]
@@ -0,0 +1,316 @@
1
+ # ruff: noqa: N802
2
+ import re
3
+ from collections.abc import Callable, Sequence
4
+ from dataclasses import dataclass
5
+ from os import PathLike
6
+ from pathlib import Path
7
+ from typing import (
8
+ NamedTuple,
9
+ TypeVar,
10
+ cast,
11
+ )
12
+
13
+ from django.conf import settings
14
+
15
+ from django_components_lite.util.misc import default
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ # This is the source of truth for the settings that are available. If the documentation
21
+ # or the defaults do NOT match this, they should be updated.
22
+ class ComponentsSettings(NamedTuple):
23
+ """
24
+ Settings available for django_components_lite.
25
+
26
+ **Example:**
27
+
28
+ ```python
29
+ COMPONENTS = ComponentsSettings(
30
+ autodiscover=False,
31
+ dirs = [BASE_DIR / "components"],
32
+ )
33
+ ```
34
+ """
35
+
36
+ autodiscover: bool | None = None
37
+ """
38
+ Toggle whether to run [autodiscovery](../concepts/fundamentals/autodiscovery.md) at the Django server startup.
39
+
40
+ Defaults to `True`
41
+
42
+ ```python
43
+ COMPONENTS = ComponentsSettings(
44
+ autodiscover=False,
45
+ )
46
+ ```
47
+ """
48
+
49
+ dirs: Sequence[str | PathLike | tuple[str, str] | tuple[str, PathLike]] | None = None
50
+ """
51
+ Specify the directories that contain your components.
52
+
53
+ Defaults to `[Path(settings.BASE_DIR) / "components"]`. That is, the root `components/` app.
54
+
55
+ Directories must be full paths, same as with
56
+ [STATICFILES_DIRS](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS).
57
+
58
+ These locations are searched during [autodiscovery](../concepts/fundamentals/autodiscovery.md),
59
+ or when you [define HTML, JS, or CSS as separate files](../concepts/fundamentals/html_js_css_files.md).
60
+
61
+ ```python
62
+ COMPONENTS = ComponentsSettings(
63
+ dirs=[BASE_DIR / "components"],
64
+ )
65
+ ```
66
+
67
+ Set to empty list to disable global components directories:
68
+
69
+ ```python
70
+ COMPONENTS = ComponentsSettings(
71
+ dirs=[],
72
+ )
73
+ ```
74
+ """
75
+
76
+ app_dirs: Sequence[str] | None = None
77
+ """
78
+ Specify the app-level directories that contain your components.
79
+
80
+ Defaults to `["components"]`. That is, for each Django app, we search `<app>/components/` for components.
81
+
82
+ The paths must be relative to app, e.g.:
83
+
84
+ ```python
85
+ COMPONENTS = ComponentsSettings(
86
+ app_dirs=["my_comps"],
87
+ )
88
+ ```
89
+
90
+ To search for `<app>/my_comps/`.
91
+
92
+ These locations are searched during [autodiscovery](../concepts/fundamentals/autodiscovery.md),
93
+ or when you [define HTML, JS, or CSS as separate files](../concepts/fundamentals/html_js_css_files.md).
94
+
95
+ Set to empty list to disable app-level components:
96
+
97
+ ```python
98
+ COMPONENTS = ComponentsSettings(
99
+ app_dirs=[],
100
+ )
101
+ ```
102
+ """
103
+
104
+ multiline_tags: bool | None = None
105
+ """
106
+ Enable / disable
107
+ [multiline support for template tags](../concepts/fundamentals/template_tag_syntax.md#multiline-tags).
108
+ If `True`, template tags like `{% component %}` or `{{ my_var }}` can span multiple lines.
109
+
110
+ Defaults to `True`.
111
+
112
+ Disable this setting if you are making custom modifications to Django's
113
+ regular expression for parsing templates at `django.template.base.tag_re`.
114
+
115
+ ```python
116
+ COMPONENTS = ComponentsSettings(
117
+ multiline_tags=False,
118
+ )
119
+ ```
120
+ """
121
+
122
+ static_files_allowed: list[str | re.Pattern] | None = None
123
+ """
124
+ A list of file extensions (including the leading dot) that define which files within
125
+ [`COMPONENTS.dirs`](./settings.md#django_components_lite.app_settings.ComponentsSettings.dirs)
126
+ or
127
+ [`COMPONENTS.app_dirs`](./settings.md#django_components_lite.app_settings.ComponentsSettings.app_dirs)
128
+ are treated as [static files](https://docs.djangoproject.com/en/5.2/howto/static-files/).
129
+
130
+ If a file is matched against any of the patterns, it's considered a static file. Such files are collected
131
+ when running [`collectstatic`](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#collectstatic),
132
+ and can be accessed under the
133
+ [static file endpoint](https://docs.djangoproject.com/en/5.2/ref/settings/#static-url).
134
+
135
+ You can also pass in compiled regexes ([`re.Pattern`](https://docs.python.org/3/library/re.html#re.Pattern))
136
+ for more advanced patterns.
137
+
138
+ By default, JS, CSS, and common image and font file formats are considered static files:
139
+
140
+ ```python
141
+ COMPONENTS = ComponentsSettings(
142
+ static_files_allowed=[
143
+ ".css",
144
+ ".js", ".jsx", ".ts", ".tsx",
145
+ # Images
146
+ ".apng", ".png", ".avif", ".gif", ".jpg",
147
+ ".jpeg", ".jfif", ".pjpeg", ".pjp", ".svg",
148
+ ".webp", ".bmp", ".ico", ".cur", ".tif", ".tiff",
149
+ # Fonts
150
+ ".eot", ".ttf", ".woff", ".otf", ".svg",
151
+ ],
152
+ )
153
+ ```
154
+
155
+ !!! warning
156
+
157
+ Exposing your Python files can be a security vulnerability.
158
+ See [Security notes](../overview/security_notes.md).
159
+ """
160
+
161
+ static_files_forbidden: list[str | re.Pattern] | None = None
162
+ """
163
+ A list of file extensions (including the leading dot) that define which files within
164
+ [`COMPONENTS.dirs`](./settings.md#django_components_lite.app_settings.ComponentsSettings.dirs)
165
+ or
166
+ [`COMPONENTS.app_dirs`](./settings.md#django_components_lite.app_settings.ComponentsSettings.app_dirs)
167
+ will NEVER be treated as [static files](https://docs.djangoproject.com/en/5.2/howto/static-files/).
168
+
169
+ If a file is matched against any of the patterns, it will never be considered a static file,
170
+ even if the file matches a pattern in
171
+ [`static_files_allowed`](./settings.md#django_components_lite.app_settings.ComponentsSettings.static_files_allowed).
172
+
173
+ Use this setting together with
174
+ [`static_files_allowed`](./settings.md#django_components_lite.app_settings.ComponentsSettings.static_files_allowed)
175
+ for a fine control over what file types will be exposed.
176
+
177
+ You can also pass in compiled regexes ([`re.Pattern`](https://docs.python.org/3/library/re.html#re.Pattern))
178
+ for more advanced patterns.
179
+
180
+ By default, any HTML and Python are considered NOT static files:
181
+
182
+ ```python
183
+ COMPONENTS = ComponentsSettings(
184
+ static_files_forbidden=[
185
+ ".html", ".django", ".dj", ".tpl",
186
+ # Python files
187
+ ".py", ".pyc",
188
+ ],
189
+ )
190
+ ```
191
+
192
+ !!! warning
193
+
194
+ Exposing your Python files can be a security vulnerability.
195
+ See [Security notes](../overview/security_notes.md).
196
+ """
197
+
198
+
199
+ # NOTE: Some defaults depend on the Django settings, which may not yet be
200
+ # initialized at the time that these settings are generated. For such cases
201
+ # we define the defaults as a factory function, and use the `Dynamic` class to
202
+ # mark such fields.
203
+ @dataclass(frozen=True)
204
+ class Dynamic[T]:
205
+ getter: Callable[[], T]
206
+
207
+
208
+ # This is the source of truth for the settings defaults. If the documentation
209
+ # does NOT match it, the documentation should be updated.
210
+ #
211
+ # NOTE: Because we need to access Django settings to generate default dirs
212
+ # for `COMPONENTS.dirs`, we do it lazily.
213
+ # NOTE 2: We show the defaults in the documentation, together with the comments
214
+ # (except for the `Dynamic` instances and comments like `type: ignore`).
215
+ # So `fmt: off` turns off Black/Ruff formatting and `snippet:defaults` allows
216
+ # us to extract the snippet from the file.
217
+ #
218
+ # fmt: off
219
+ # --snippet:defaults--
220
+ defaults = ComponentsSettings(
221
+ autodiscover=True,
222
+ # Root-level "components" dirs, e.g. `/path/to/proj/components/`
223
+ dirs=Dynamic(lambda: [Path(settings.BASE_DIR) / "components"]), # type: ignore[arg-type]
224
+ # App-level "components" dirs, e.g. `[app]/components/`
225
+ app_dirs=["components"],
226
+ multiline_tags=True,
227
+ static_files_allowed=[
228
+ ".css",
229
+ ".js", ".jsx", ".ts", ".tsx",
230
+ # Images
231
+ ".apng", ".png", ".avif", ".gif", ".jpg",
232
+ ".jpeg", ".jfif", ".pjpeg", ".pjp", ".svg",
233
+ ".webp", ".bmp", ".ico", ".cur", ".tif", ".tiff",
234
+ # Fonts
235
+ ".eot", ".ttf", ".woff", ".otf", ".svg",
236
+ ],
237
+ static_files_forbidden=[
238
+ # See https://marketplace.visualstudio.com/items?itemName=junstyle.vscode-django-support
239
+ ".html", ".django", ".dj", ".tpl",
240
+ # Python files
241
+ ".py", ".pyc",
242
+ ],
243
+ )
244
+ # --endsnippet:defaults--
245
+ # fmt: on
246
+
247
+
248
+ # Interface through which we access the settings.
249
+ #
250
+ # This is the only place where we actually access the settings.
251
+ # The settings are merged with defaults, and then validated.
252
+ #
253
+ # The settings are then available through the `app_settings` object.
254
+ #
255
+ # Settings are loaded from Django settings only once, at `apps.py` in `ready()`.
256
+ class InternalSettings:
257
+ def __init__(self) -> None:
258
+ self._settings: ComponentsSettings | None = None
259
+
260
+ def _load_settings(self) -> None:
261
+ data = getattr(settings, "COMPONENTS", {})
262
+ components_settings = ComponentsSettings(**data) if not isinstance(data, ComponentsSettings) else data
263
+
264
+ # Merge we defaults and otherwise initialize if necessary
265
+
266
+ # For DIRS setting, we use a getter for the default value, because the default value
267
+ # uses Django settings, which may not yet be initialized at the time these settings are generated.
268
+ dirs_default_fn = cast("Dynamic[Sequence[str | tuple[str, str]]]", defaults.dirs)
269
+ dirs_default = dirs_default_fn.getter()
270
+
271
+ self._settings = ComponentsSettings(
272
+ autodiscover=default(components_settings.autodiscover, defaults.autodiscover),
273
+ dirs=default(components_settings.dirs, dirs_default),
274
+ app_dirs=default(components_settings.app_dirs, defaults.app_dirs),
275
+ multiline_tags=default(components_settings.multiline_tags, defaults.multiline_tags),
276
+ static_files_allowed=default(components_settings.static_files_allowed, defaults.static_files_allowed),
277
+ static_files_forbidden=self._prepare_static_files_forbidden(components_settings),
278
+ )
279
+
280
+ def _get_settings(self) -> ComponentsSettings:
281
+ if self._settings is None:
282
+ self._load_settings()
283
+ return cast("ComponentsSettings", self._settings)
284
+
285
+ def _prepare_static_files_forbidden(self, new_settings: ComponentsSettings) -> list[str | re.Pattern]:
286
+ return default(
287
+ new_settings.static_files_forbidden,
288
+ cast("list[str | re.Pattern]", defaults.static_files_forbidden),
289
+ )
290
+
291
+ @property
292
+ def AUTODISCOVER(self) -> bool:
293
+ return self._get_settings().autodiscover # type: ignore[return-value]
294
+
295
+ @property
296
+ def DIRS(self) -> Sequence[str | PathLike | tuple[str, str] | tuple[str, PathLike]]:
297
+ return self._get_settings().dirs # type: ignore[return-value]
298
+
299
+ @property
300
+ def APP_DIRS(self) -> Sequence[str]:
301
+ return self._get_settings().app_dirs # type: ignore[return-value]
302
+
303
+ @property
304
+ def MULTILINE_TAGS(self) -> bool:
305
+ return self._get_settings().multiline_tags # type: ignore[return-value]
306
+
307
+ @property
308
+ def STATIC_FILES_ALLOWED(self) -> Sequence[str | re.Pattern]:
309
+ return self._get_settings().static_files_allowed # type: ignore[return-value]
310
+
311
+ @property
312
+ def STATIC_FILES_FORBIDDEN(self) -> Sequence[str | re.Pattern]:
313
+ return self._get_settings().static_files_forbidden # type: ignore[return-value]
314
+
315
+
316
+ app_settings = InternalSettings()