plain 0.66.0__py3-none-any.whl → 0.101.2__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.
- plain/CHANGELOG.md +684 -0
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -53
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +112 -28
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +175 -102
- plain/cli/print.py +4 -4
- plain/cli/registry.py +95 -26
- plain/cli/request.py +206 -0
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -13
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +40 -15
- plain/paginator.py +31 -21
- plain/preflight/README.md +208 -23
- plain/preflight/__init__.py +5 -24
- plain/preflight/checks.py +12 -0
- plain/preflight/files.py +19 -13
- plain/preflight/registry.py +80 -58
- plain/preflight/results.py +37 -0
- plain/preflight/security.py +65 -71
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +10 -48
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +43 -33
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +14 -27
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +56 -40
- plain/urls/resolvers.py +38 -28
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/cli/agent/request.py +0 -181
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/preflight/messages.py +0 -81
- plain/templates/AGENTS.md +0 -3
- plain-0.66.0.dist-info/RECORD +0 -168
- plain-0.66.0.dist-info/entry_points.txt +0 -4
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/templates/README.md
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# Templates
|
|
2
2
|
|
|
3
|
-
**Render HTML templates using
|
|
3
|
+
**Render HTML templates using Jinja2.**
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
6
|
- [Template files](#template-files)
|
|
7
|
-
- [
|
|
7
|
+
- [Template context](#template-context)
|
|
8
|
+
- [Built-in globals](#built-in-globals)
|
|
9
|
+
- [Built-in filters](#built-in-filters)
|
|
10
|
+
- [Custom globals and filters](#custom-globals-and-filters)
|
|
11
|
+
- [Custom template extensions](#custom-template-extensions)
|
|
8
12
|
- [Rendering templates manually](#rendering-templates-manually)
|
|
13
|
+
- [Custom Jinja environment](#custom-jinja-environment)
|
|
14
|
+
- [FAQs](#faqs)
|
|
15
|
+
- [Installation](#installation)
|
|
9
16
|
|
|
10
17
|
## Overview
|
|
11
18
|
|
|
12
|
-
Plain uses Jinja2 for template rendering. You can refer to the [Jinja documentation](https://jinja.palletsprojects.com/en/stable/
|
|
19
|
+
Plain uses Jinja2 for template rendering. You can refer to the [Jinja documentation](https://jinja.palletsprojects.com/en/stable/) for all of the features available.
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
Templates are typically used with `TemplateView` or one of its subclasses.
|
|
15
22
|
|
|
16
23
|
```python
|
|
24
|
+
# app/views.py
|
|
17
25
|
from plain.views import TemplateView
|
|
18
26
|
|
|
19
27
|
|
|
@@ -27,7 +35,7 @@ class ExampleView(TemplateView):
|
|
|
27
35
|
```
|
|
28
36
|
|
|
29
37
|
```html
|
|
30
|
-
<!-- example.html -->
|
|
38
|
+
<!-- app/templates/example.html -->
|
|
31
39
|
{% extends "base.html" %}
|
|
32
40
|
|
|
33
41
|
{% block content %}
|
|
@@ -37,50 +45,233 @@ class ExampleView(TemplateView):
|
|
|
37
45
|
|
|
38
46
|
## Template files
|
|
39
47
|
|
|
40
|
-
Template files can
|
|
41
|
-
or the `<pkg>/templates` directory of any installed package.
|
|
48
|
+
Template files can live in two locations:
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
1. **`app/templates/`** - Your app's templates (highest priority)
|
|
51
|
+
2. **`{package}/templates/`** - Templates inside any installed package
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
All template directories are merged together, so you can override templates from installed packages by creating a file with the same name in `app/templates/`.
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
## Template context
|
|
56
|
+
|
|
57
|
+
When using `TemplateView`, you pass data to templates by overriding `get_template_context()`.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
class ProductView(TemplateView):
|
|
61
|
+
template_name = "product.html"
|
|
62
|
+
|
|
63
|
+
def get_template_context(self):
|
|
64
|
+
context = super().get_template_context()
|
|
65
|
+
context["product"] = Product.objects.get(id=self.url_kwargs["id"])
|
|
66
|
+
context["related_products"] = Product.objects.filter(category=context["product"].category)[:5]
|
|
67
|
+
return context
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The context is then available in your template:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<h1>{{ product.name }}</h1>
|
|
74
|
+
<ul>
|
|
75
|
+
{% for item in related_products %}
|
|
76
|
+
<li>{{ item.name }}</li>
|
|
77
|
+
{% endfor %}
|
|
78
|
+
</ul>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Built-in globals
|
|
82
|
+
|
|
83
|
+
Plain provides several [global functions](./jinja/globals.py) available in all templates:
|
|
84
|
+
|
|
85
|
+
| Global | Description |
|
|
86
|
+
| ---------------------------- | ---------------------------------- |
|
|
87
|
+
| `asset(path)` | Returns the URL for a static asset |
|
|
88
|
+
| `url(name, *args, **kwargs)` | Reverses a URL by name |
|
|
89
|
+
| `Paginator` | The Paginator class for pagination |
|
|
90
|
+
| `now()` | Returns the current datetime |
|
|
91
|
+
| `timedelta` | The timedelta class for date math |
|
|
92
|
+
| `localtime(dt)` | Converts a datetime to local time |
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
|
|
96
|
+
<a href="{{ url('product_detail', id=product.id) }}">View</a>
|
|
97
|
+
<p>Generated at {{ now() }}</p>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Built-in filters
|
|
101
|
+
|
|
102
|
+
Plain includes several [filters](./jinja/filters.py) for common operations:
|
|
103
|
+
|
|
104
|
+
| Filter | Description |
|
|
105
|
+
| ----------------------------- | ------------------------------------ |
|
|
106
|
+
| `strftime(format)` | Formats a datetime |
|
|
107
|
+
| `strptime(format)` | Parses a string to datetime |
|
|
108
|
+
| `fromtimestamp(ts)` | Creates datetime from timestamp |
|
|
109
|
+
| `fromisoformat(s)` | Creates datetime from ISO string |
|
|
110
|
+
| `localtime(tz)` | Converts to local timezone |
|
|
111
|
+
| `timeuntil` | Human-readable time until a date |
|
|
112
|
+
| `timesince` | Human-readable time since a date |
|
|
113
|
+
| `json_script(id)` | Outputs JSON safely in a script tag |
|
|
114
|
+
| `islice(stop)` | Slices iterables (useful for dicts) |
|
|
115
|
+
| `pluralize(singular, plural)` | Returns plural suffix based on count |
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<p>Posted {{ post.created_at|timesince }} ago</p>
|
|
119
|
+
<p>{{ items|length }} item{{ items|length|pluralize }}</p>
|
|
120
|
+
<p>{{ 5 }} ox{{ 5|pluralize("en") }}</p>
|
|
121
|
+
{{ data|json_script("page-data") }}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Custom globals and filters
|
|
125
|
+
|
|
126
|
+
You can register your own globals and filters in `app/templates.py` (or `{package}/templates.py`). These files are automatically imported when the template environment loads.
|
|
48
127
|
|
|
49
128
|
```python
|
|
50
129
|
# app/templates.py
|
|
51
|
-
from plain.templates import register_template_filter, register_template_global
|
|
52
|
-
from plain.templates.jinja.extensions import InclusionTagExtension
|
|
53
|
-
from plain.runtime import settings
|
|
130
|
+
from plain.templates import register_template_filter, register_template_global
|
|
54
131
|
|
|
55
132
|
|
|
56
133
|
@register_template_filter
|
|
57
134
|
def camel_case(value):
|
|
135
|
+
"""Convert a string to CamelCase."""
|
|
58
136
|
return value.replace("_", " ").title().replace(" ", "")
|
|
59
137
|
|
|
60
138
|
|
|
61
139
|
@register_template_global
|
|
62
140
|
def app_version():
|
|
141
|
+
"""Return the current app version."""
|
|
63
142
|
return "1.0.0"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Now you can use these in templates:
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<p>{{ "my_variable"|camel_case }}</p> <!-- outputs: MyVariable -->
|
|
149
|
+
<footer>Version {{ app_version() }}</footer>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
You can also register non-callable values as globals by providing a name:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from plain.templates import register_template_global
|
|
156
|
+
|
|
157
|
+
register_template_global("1.0.0", name="APP_VERSION")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Custom template extensions
|
|
161
|
+
|
|
162
|
+
For more complex template features, you can create Jinja extensions. The [`InclusionTagExtension`](./jinja/extensions.py#InclusionTagExtension) base class makes it easy to create custom tags that render their own templates.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# app/templates.py
|
|
166
|
+
from plain.templates import register_template_extension
|
|
167
|
+
from plain.templates.jinja.extensions import InclusionTagExtension
|
|
168
|
+
from plain.runtime import settings
|
|
64
169
|
|
|
65
170
|
|
|
66
171
|
@register_template_extension
|
|
67
|
-
class
|
|
68
|
-
tags = {"
|
|
69
|
-
template_name = "
|
|
172
|
+
class AlertExtension(InclusionTagExtension):
|
|
173
|
+
tags = {"alert"}
|
|
174
|
+
template_name = "components/alert.html"
|
|
70
175
|
|
|
71
176
|
def get_context(self, context, *args, **kwargs):
|
|
72
177
|
return {
|
|
73
|
-
"
|
|
74
|
-
"
|
|
178
|
+
"message": args[0] if args else "",
|
|
179
|
+
"type": kwargs.get("type", "info"),
|
|
75
180
|
}
|
|
76
181
|
```
|
|
77
182
|
|
|
183
|
+
```html
|
|
184
|
+
<!-- app/templates/components/alert.html -->
|
|
185
|
+
<div class="alert alert-{{ type }}">{{ message }}</div>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<!-- Usage in any template -->
|
|
190
|
+
{% alert "Something happened!" type="warning" %}
|
|
191
|
+
```
|
|
192
|
+
|
|
78
193
|
## Rendering templates manually
|
|
79
194
|
|
|
80
|
-
|
|
195
|
+
You can render templates outside of views using the [`Template`](./core.py#Template) class.
|
|
81
196
|
|
|
82
197
|
```python
|
|
83
198
|
from plain.templates import Template
|
|
84
199
|
|
|
85
|
-
|
|
200
|
+
html = Template("email/welcome.html").render({
|
|
201
|
+
"user_name": "Alice",
|
|
202
|
+
"activation_url": "https://example.com/activate/abc123",
|
|
203
|
+
})
|
|
86
204
|
```
|
|
205
|
+
|
|
206
|
+
If the template file doesn't exist, a [`TemplateFileMissing`](./core.py#TemplateFileMissing) exception is raised.
|
|
207
|
+
|
|
208
|
+
## Custom Jinja environment
|
|
209
|
+
|
|
210
|
+
By default, Plain uses a [`DefaultEnvironment`](./jinja/environments.py#DefaultEnvironment) that configures Jinja2 with sensible defaults:
|
|
211
|
+
|
|
212
|
+
- **Autoescaping** enabled for security
|
|
213
|
+
- **StrictUndefined** so undefined variables raise errors
|
|
214
|
+
- **Auto-reload** in debug mode
|
|
215
|
+
- **Loop controls** extension (`break`, `continue`)
|
|
216
|
+
- **Debug** extension
|
|
217
|
+
|
|
218
|
+
You can customize the environment by creating your own class and pointing to it in settings:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# app/jinja.py
|
|
222
|
+
from plain.templates.jinja.environments import DefaultEnvironment
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class CustomEnvironment(DefaultEnvironment):
|
|
226
|
+
def __init__(self):
|
|
227
|
+
super().__init__()
|
|
228
|
+
# Add your customizations here
|
|
229
|
+
self.globals["CUSTOM_SETTING"] = "value"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# app/settings.py
|
|
234
|
+
TEMPLATES_JINJA_ENVIRONMENT = "app.jinja.CustomEnvironment"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## FAQs
|
|
238
|
+
|
|
239
|
+
#### Why am I getting "undefined variable" errors?
|
|
240
|
+
|
|
241
|
+
Plain uses Jinja's `StrictUndefined` mode, which raises an error when you reference a variable that doesn't exist in the context. This helps catch typos and missing data early. Make sure you're passing all required variables in `get_template_context()`.
|
|
242
|
+
|
|
243
|
+
#### Why does my template show an error about a callable?
|
|
244
|
+
|
|
245
|
+
Plain's template environment prevents accidentally rendering callables (functions, methods) directly. If you see an error like "X is callable, did you forget parentheses?", you probably need to add `()` to call the function:
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<!-- Wrong -->
|
|
249
|
+
{{ user.get_full_name }}
|
|
250
|
+
|
|
251
|
+
<!-- Correct -->
|
|
252
|
+
{{ user.get_full_name() }}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### How do I use Jinja's loop controls?
|
|
256
|
+
|
|
257
|
+
Plain enables the `loopcontrols` extension by default, so you can use `break` and `continue` in loops:
|
|
258
|
+
|
|
259
|
+
```html
|
|
260
|
+
{% for item in items %}
|
|
261
|
+
{% if item.skip %}
|
|
262
|
+
{% continue %}
|
|
263
|
+
{% endif %}
|
|
264
|
+
{% if item.stop %}
|
|
265
|
+
{% break %}
|
|
266
|
+
{% endif %}
|
|
267
|
+
<p>{{ item.name }}</p>
|
|
268
|
+
{% endfor %}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Where can I learn more about Jinja2?
|
|
272
|
+
|
|
273
|
+
The [Jinja2 documentation](https://jinja.palletsprojects.com/en/stable/) covers all the template syntax, including conditionals, loops, macros, and inheritance.
|
|
274
|
+
|
|
275
|
+
## Installation
|
|
276
|
+
|
|
277
|
+
The `plain.templates` module is included with Plain by default. No additional installation is required.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
3
5
|
|
|
4
6
|
from plain.packages import packages_registry
|
|
5
7
|
from plain.runtime import settings
|
|
@@ -10,11 +12,7 @@ from .environments import DefaultEnvironment, get_template_dirs
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class JinjaEnvironment(LazyObject):
|
|
13
|
-
def
|
|
14
|
-
self.__dict__["_imported_modules"] = set()
|
|
15
|
-
super().__init__(*args, **kwargs)
|
|
16
|
-
|
|
17
|
-
def _setup(self):
|
|
15
|
+
def _setup(self) -> None:
|
|
18
16
|
environment_setting = settings.TEMPLATES_JINJA_ENVIRONMENT
|
|
19
17
|
|
|
20
18
|
if isinstance(environment_setting, str):
|
|
@@ -25,33 +23,19 @@ class JinjaEnvironment(LazyObject):
|
|
|
25
23
|
# We have to set _wrapped before we trigger the autoloading of "register" commands
|
|
26
24
|
self._wrapped = env
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import_name = f"{package_config.name}.templates"
|
|
31
|
-
if import_name in self._imported_modules:
|
|
32
|
-
continue
|
|
33
|
-
if importlib.util.find_spec(import_name) is None:
|
|
34
|
-
continue
|
|
35
|
-
import_module(import_name)
|
|
36
|
-
self._imported_modules.add(import_name)
|
|
37
|
-
|
|
38
|
-
# Autoload template helpers from the local ``app`` package if present
|
|
39
|
-
import_name = "app.templates"
|
|
40
|
-
if import_name not in self._imported_modules:
|
|
41
|
-
if importlib.util.find_spec(import_name) is not None:
|
|
42
|
-
import_module(import_name)
|
|
43
|
-
self._imported_modules.add(import_name)
|
|
26
|
+
# Autoload template helpers using the registry method
|
|
27
|
+
packages_registry.autodiscover_modules("templates", include_app=True)
|
|
44
28
|
|
|
45
29
|
|
|
46
30
|
environment = JinjaEnvironment()
|
|
47
31
|
|
|
48
32
|
|
|
49
|
-
def register_template_extension(extension_class):
|
|
33
|
+
def register_template_extension(extension_class: type) -> type:
|
|
50
34
|
environment.add_extension(extension_class)
|
|
51
35
|
return extension_class
|
|
52
36
|
|
|
53
37
|
|
|
54
|
-
def register_template_global(value, name=None):
|
|
38
|
+
def register_template_global(value: Any, name: str | None = None) -> Any:
|
|
55
39
|
"""
|
|
56
40
|
Adds a global to the Jinja environment.
|
|
57
41
|
|
|
@@ -75,9 +59,12 @@ def register_template_global(value, name=None):
|
|
|
75
59
|
return value
|
|
76
60
|
|
|
77
61
|
|
|
78
|
-
def register_template_filter(
|
|
62
|
+
def register_template_filter(
|
|
63
|
+
func: Callable[..., Any], name: str | None = None
|
|
64
|
+
) -> Callable[..., Any]:
|
|
79
65
|
"""Adds a filter to the Jinja environment."""
|
|
80
|
-
|
|
66
|
+
filter_name = name if name is not None else func.__name__ # type: ignore[attr-defined]
|
|
67
|
+
environment.filters[filter_name] = func
|
|
81
68
|
return func
|
|
82
69
|
|
|
83
70
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
from jinja2 import Environment, StrictUndefined
|
|
5
6
|
from jinja2.loaders import FileSystemLoader
|
|
@@ -11,7 +12,7 @@ from .filters import default_filters
|
|
|
11
12
|
from .globals import default_globals
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
+
def _finalize_callable_error(obj: Any) -> Any:
|
|
15
16
|
"""Prevent direct rendering of a callable (likely just forgotten ()) by raising a TypeError"""
|
|
16
17
|
if callable(obj):
|
|
17
18
|
raise TypeError(f"{obj} is callable, did you forget parentheses?")
|
|
@@ -23,14 +24,14 @@ def finalize_callable_error(obj):
|
|
|
23
24
|
return obj
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
def get_template_dirs():
|
|
27
|
+
def get_template_dirs() -> tuple[Path, ...]:
|
|
27
28
|
jinja_templates = Path(__file__).parent / "templates"
|
|
28
29
|
app_templates = settings.path.parent / "templates"
|
|
29
30
|
return (jinja_templates, app_templates) + _get_app_template_dirs()
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@functools.lru_cache
|
|
33
|
-
def _get_app_template_dirs():
|
|
34
|
+
def _get_app_template_dirs() -> tuple[Path, ...]:
|
|
34
35
|
"""
|
|
35
36
|
Return an iterable of paths of directories to load app templates from.
|
|
36
37
|
|
|
@@ -54,7 +55,7 @@ class DefaultEnvironment(Environment):
|
|
|
54
55
|
autoescape=True,
|
|
55
56
|
auto_reload=settings.DEBUG,
|
|
56
57
|
undefined=StrictUndefined,
|
|
57
|
-
finalize=
|
|
58
|
+
finalize=_finalize_callable_error,
|
|
58
59
|
extensions=["jinja2.ext.loopcontrols", "jinja2.ext.debug"],
|
|
59
60
|
)
|
|
60
61
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
1
5
|
from jinja2 import nodes
|
|
2
6
|
from jinja2.ext import Extension
|
|
7
|
+
from jinja2.runtime import Context
|
|
3
8
|
|
|
4
9
|
|
|
5
10
|
class InclusionTagExtension(Extension):
|
|
@@ -9,7 +14,7 @@ class InclusionTagExtension(Extension):
|
|
|
9
14
|
tags: set[str]
|
|
10
15
|
template_name: str
|
|
11
16
|
|
|
12
|
-
def parse(self, parser):
|
|
17
|
+
def parse(self, parser: Any) -> nodes.Node:
|
|
13
18
|
lineno = next(parser.stream).lineno
|
|
14
19
|
args = [
|
|
15
20
|
nodes.DerivedContextReference(),
|
|
@@ -28,12 +33,14 @@ class InclusionTagExtension(Extension):
|
|
|
28
33
|
call = self.call_method("_render", args=args, kwargs=kwargs, lineno=lineno)
|
|
29
34
|
return nodes.CallBlock(call, [], [], []).set_lineno(lineno)
|
|
30
35
|
|
|
31
|
-
def _render(self, context, *args, **kwargs):
|
|
32
|
-
|
|
36
|
+
def _render(self, context: Context, *args: Any, **kwargs: Any) -> str:
|
|
37
|
+
render_context = self.get_context(context, *args, **kwargs)
|
|
33
38
|
template = self.environment.get_template(self.template_name)
|
|
34
|
-
return template.render(
|
|
39
|
+
return template.render(render_context)
|
|
35
40
|
|
|
36
|
-
def get_context(
|
|
41
|
+
def get_context(
|
|
42
|
+
self, context: Context, *args: Any, **kwargs: Any
|
|
43
|
+
) -> Context | dict[str, Any]:
|
|
37
44
|
raise NotImplementedError(
|
|
38
45
|
"You need to implement the `get_context` method in your subclass."
|
|
39
46
|
)
|
plain/templates/jinja/filters.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
4
|
from itertools import islice
|
|
5
|
+
from typing import Any
|
|
3
6
|
|
|
4
7
|
from plain.utils.html import json_script
|
|
5
8
|
from plain.utils.timesince import timesince, timeuntil
|
|
6
9
|
from plain.utils.timezone import localtime
|
|
7
10
|
|
|
8
11
|
|
|
9
|
-
def localtime_filter(
|
|
12
|
+
def localtime_filter(
|
|
13
|
+
value: datetime.datetime | None, timezone: Any = None
|
|
14
|
+
) -> datetime.datetime:
|
|
10
15
|
"""Converts a datetime to local time in a template."""
|
|
11
16
|
if not value:
|
|
12
17
|
# Without this, we get the current localtime
|
|
@@ -15,7 +20,7 @@ def localtime_filter(value, timezone=None):
|
|
|
15
20
|
return localtime(value, timezone)
|
|
16
21
|
|
|
17
22
|
|
|
18
|
-
def pluralize_filter(value, singular="", plural="s"):
|
|
23
|
+
def pluralize_filter(value: Any, singular: str = "", plural: str = "s") -> str:
|
|
19
24
|
"""Returns plural suffix based on the value count.
|
|
20
25
|
|
|
21
26
|
Usage:
|
plain/templates/jinja/globals.py
CHANGED
|
@@ -5,7 +5,7 @@ from plain.urls import reverse
|
|
|
5
5
|
from plain.utils import timezone
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def _asset(url_path: str) -> str:
|
|
9
9
|
# An explicit callable we can control, but also delay the import of asset.urls->views->templates
|
|
10
10
|
# for circular import reasons
|
|
11
11
|
from plain.assets.urls import get_asset_url
|
|
@@ -14,7 +14,7 @@ def asset(url_path):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
default_globals = {
|
|
17
|
-
"asset":
|
|
17
|
+
"asset": _asset,
|
|
18
18
|
"url": reverse,
|
|
19
19
|
"Paginator": Paginator,
|
|
20
20
|
"now": timezone.now,
|