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.
Files changed (197) hide show
  1. plain/CHANGELOG.md +684 -0
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -53
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +112 -28
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +175 -102
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +95 -26
  27. plain/cli/request.py +206 -0
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -13
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +40 -15
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +208 -23
  84. plain/preflight/__init__.py +5 -24
  85. plain/preflight/checks.py +12 -0
  86. plain/preflight/files.py +19 -13
  87. plain/preflight/registry.py +80 -58
  88. plain/preflight/results.py +37 -0
  89. plain/preflight/security.py +65 -71
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +10 -48
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +43 -33
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +14 -27
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +56 -40
  145. plain/urls/resolvers.py +38 -28
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/cli/agent/request.py +0 -181
  191. plain/csrf/views.py +0 -31
  192. plain/logs/utils.py +0 -46
  193. plain/preflight/messages.py +0 -81
  194. plain/templates/AGENTS.md +0 -3
  195. plain-0.66.0.dist-info/RECORD +0 -168
  196. plain-0.66.0.dist-info/entry_points.txt +0 -4
  197. {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 Jinja.**
3
+ **Render HTML templates using Jinja2.**
4
4
 
5
5
  - [Overview](#overview)
6
6
  - [Template files](#template-files)
7
- - [Extending Jinja](#extending-jinja)
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/api/) for all of the features available.
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
- In general, templates are used in combination with `TemplateView` or a more specific subclass of it.
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 be located in either a root `app/templates`,
41
- or the `<pkg>/templates` directory of any installed package.
48
+ Template files can live in two locations:
42
49
 
43
- All template directories are "merged" together, allowing you to override templates from other packages. The `app/templates` will take priority, followed by `INSTALLED_PACKAGES` in the order they are defined.
50
+ 1. **`app/templates/`** - Your app's templates (highest priority)
51
+ 2. **`{package}/templates/`** - Templates inside any installed package
44
52
 
45
- ## Extending Jinja
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
- Plain includes a set of default [global variables](./jinja/globals.py) and [filters](./jinja/filters.py). You can register additional extensions, globals, or filters either in a package or in your app. Typically this will be in `app/templates.py` or `<pkg>/templates.py`, which are automatically imported.
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, register_template_extension
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 HTMXJSExtension(InclusionTagExtension):
68
- tags = {"htmx_js"}
69
- template_name = "htmx/js.html"
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
- "DEBUG": settings.DEBUG,
74
- "extensions": kwargs.get("extensions", []),
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
- Templates can also be rendered manually using the [`Template` class](./core.py#Template).
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
- comment_body = Template("comment.md").render({"message": "Hello, world!",})
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 importlib.util
2
- from importlib import import_module
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 __init__(self, *args, **kwargs):
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
- for package_config in packages_registry.get_package_configs():
29
- # Autoload template helpers if the package provides a ``templates`` module
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(func, name=None):
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
- environment.filters[name or func.__name__] = func
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 finalize_callable_error(obj):
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=finalize_callable_error,
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
- context = self.get_context(context, *args, **kwargs)
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(context)
39
+ return template.render(render_context)
35
40
 
36
- def get_context(self, context, *args, **kwargs):
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
  )
@@ -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(value, timezone=None):
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:
@@ -5,7 +5,7 @@ from plain.urls import reverse
5
5
  from plain.utils import timezone
6
6
 
7
7
 
8
- def asset(url_path):
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": asset,
17
+ "asset": _asset,
18
18
  "url": reverse,
19
19
  "Paginator": Paginator,
20
20
  "now": timezone.now,