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/test/encoding.py CHANGED
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  import mimetypes
2
4
  import os
5
+ from typing import Any
3
6
 
4
7
  from plain.runtime import settings
5
8
  from plain.utils.encoding import force_bytes
6
9
  from plain.utils.itercompat import is_iterable
7
10
 
8
11
 
9
- def encode_multipart(boundary, data):
12
+ def encode_multipart(boundary: str, data: dict[str, Any]) -> bytes:
10
13
  """
11
14
  Encode multipart POST data from a dictionary of form values.
12
15
 
@@ -14,13 +17,13 @@ def encode_multipart(boundary, data):
14
17
  as content. If the value is a file, the contents of the file will be sent
15
18
  as an application/octet-stream; otherwise, str(value) will be sent.
16
19
  """
17
- lines = []
20
+ lines: list[bytes] = []
18
21
 
19
- def to_bytes(s):
22
+ def to_bytes(s: str) -> bytes:
20
23
  return force_bytes(s, settings.DEFAULT_CHARSET)
21
24
 
22
25
  # Not by any means perfect, but good enough for our purposes.
23
- def is_file(thing):
26
+ def is_file(thing: Any) -> bool:
24
27
  return hasattr(thing, "read") and callable(thing.read)
25
28
 
26
29
  # Each bit of the multipart form data could be either a form value or a
@@ -68,8 +71,8 @@ def encode_multipart(boundary, data):
68
71
  return b"\r\n".join(lines)
69
72
 
70
73
 
71
- def encode_file(boundary, key, file):
72
- def to_bytes(s):
74
+ def encode_file(boundary: str, key: str, file: Any) -> list[bytes]:
75
+ def to_bytes(s: str) -> bytes:
73
76
  return force_bytes(s, settings.DEFAULT_CHARSET)
74
77
 
75
78
  # file.name might not be a string. For example, it's an int for
plain/test/exceptions.py CHANGED
@@ -1,7 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
1
6
  class RedirectCycleError(Exception):
2
7
  """The test client has been asked to follow a redirect loop."""
3
8
 
4
- def __init__(self, message, last_response):
9
+ def __init__(self, message: str, last_response: Any) -> None:
5
10
  super().__init__(message)
6
11
  self.last_response = last_response
7
- self.redirect_chain = last_response.redirect_chain
12
+ self.redirect_chain: list[tuple[str, int]] = last_response.redirect_chain
plain/urls/README.md CHANGED
@@ -1,27 +1,68 @@
1
1
  # URLs
2
2
 
3
- **Route requests to views.**
3
+ **Route incoming requests to views based on URL patterns.**
4
4
 
5
5
  - [Overview](#overview)
6
+ - [Defining paths](#defining-paths)
7
+ - [Including sub-routers](#including-sub-routers)
8
+ - [Path converters](#path-converters)
9
+ - [Built-in converters](#built-in-converters)
10
+ - [Custom converters](#custom-converters)
6
11
  - [Reversing URLs](#reversing-urls)
7
- - [URL args and kwargs](#url-args-and-kwargs)
8
- - [Package routers](#package-routers)
12
+ - [In templates](#in-templates)
13
+ - [In Python code](#in-python-code)
14
+ - [Lazy reverse](#lazy-reverse)
15
+ - [Regex patterns](#regex-patterns)
16
+ - [FAQs](#faqs)
17
+ - [Installation](#installation)
9
18
 
10
19
  ## Overview
11
20
 
12
- URLs are typically the "entrypoint" to your app. Virtually all request handling up to this point happens behind the scenes, and then you decide how to route specific URL patterns to your views.
21
+ You define URL routing by creating a `Router` class with a list of URL patterns. Each pattern maps a URL path to a view.
13
22
 
14
- The `URLS_ROUTER` is the primary router that handles all incoming requests. It is defined in your `app/settings.py` file. This will typically point to a `Router` class in your `app.urls` module.
23
+ ```python
24
+ # app/urls.py
25
+ from plain.urls import Router, path
26
+ from . import views
27
+
28
+
29
+ class AppRouter(Router):
30
+ namespace = ""
31
+ urls = [
32
+ path("", views.HomeView),
33
+ path("about/", views.AboutView, name="about"),
34
+ path("contact/", views.ContactView, name="contact"),
35
+ ]
36
+ ```
37
+
38
+ The `URLS_ROUTER` setting in your `app/settings.py` tells Plain which router handles incoming requests:
15
39
 
16
40
  ```python
17
41
  # app/settings.py
18
42
  URLS_ROUTER = "app.urls.AppRouter"
19
43
  ```
20
44
 
21
- The root router often has an empty namespace (`""`) and some combination of individual paths and sub-routers.
45
+ When a request comes in, Plain matches the URL against your patterns in order and calls the corresponding view.
46
+
47
+ ## Defining paths
48
+
49
+ Use `path()` to map a URL pattern to a view class:
50
+
51
+ ```python
52
+ path("about/", views.AboutView, name="about")
53
+ ```
54
+
55
+ The `name` parameter is optional but required if you want to reverse the URL later. You can pass the view class directly (Plain calls `as_view()` for you) or call `as_view()` yourself to pass arguments:
56
+
57
+ ```python
58
+ path("dashboard/", views.DashboardView.as_view(template_name="custom.html"), name="dashboard")
59
+ ```
60
+
61
+ ## Including sub-routers
62
+
63
+ Use `include()` to nest routers under a URL prefix. This keeps your URL configuration modular.
22
64
 
23
65
  ```python
24
- # app/urls.py
25
66
  from plain.urls import Router, path, include
26
67
  from plain.admin.urls import AdminRouter
27
68
  from . import views
@@ -31,127 +72,170 @@ class AppRouter(Router):
31
72
  namespace = ""
32
73
  urls = [
33
74
  include("admin/", AdminRouter),
34
- path("about/", views.AboutView, name="about"), # A named URL
35
- path("", views.HomeView), # An unnamed URL
75
+ include("api/", ApiRouter),
76
+ path("", views.HomeView),
36
77
  ]
37
78
  ```
38
79
 
39
- ## Reversing URLs
80
+ Each included router has its own `namespace` that prefixes URL names. For example, if `AdminRouter` has `namespace = "admin"` and a URL named `"dashboard"`, you reverse it as `"admin:dashboard"`.
40
81
 
41
- In templates, you will use the `{{ url("<url name>") }}` function to look up full URLs by name.
82
+ You can also include a list of patterns directly without creating a separate router class:
42
83
 
43
- ```html
44
- <a href="{{ url('about') }}">About</a>
84
+ ```python
85
+ include("api/", [
86
+ path("users/", views.UsersAPIView, name="users"),
87
+ path("posts/", views.PostsAPIView, name="posts"),
88
+ ])
45
89
  ```
46
90
 
47
- And the same can be done in Python code with the `reverse` (or `reverse_lazy`) function.
91
+ ## Path converters
48
92
 
49
- ```python
50
- from plain.urls import reverse
93
+ Capture dynamic segments from URLs using angle bracket syntax:
51
94
 
52
- url = reverse("about")
95
+ ```python
96
+ path("user/<int:user_id>/", views.UserView, name="user")
97
+ path("post/<slug:post_slug>/", views.PostView, name="post")
53
98
  ```
54
99
 
55
- A URL path has to include a `name` attribute if you want to reverse it. The router's `namespace` will be used as a prefix to the URL name.
100
+ Captured values are available in your view as `self.url_kwargs`:
56
101
 
57
102
  ```python
58
- from plain.urls import reverse
103
+ class UserView(View):
104
+ def get(self):
105
+ user_id = self.url_kwargs["user_id"] # Already converted to int
106
+ # ...
107
+ ```
108
+
109
+ ### Built-in converters
59
110
 
60
- url = reverse("admin:dashboard")
111
+ | Converter | Matches | Python type |
112
+ | --------- | ------------------------------------------------------- | ----------- |
113
+ | `str` | Any non-empty string excluding `/` (default) | `str` |
114
+ | `int` | Zero or positive integers | `int` |
115
+ | `slug` | ASCII letters, numbers, hyphens, underscores | `str` |
116
+ | `uuid` | UUID format like `075194d3-6885-417e-a8a8-6c931e272f00` | `uuid.UUID` |
117
+ | `path` | Any non-empty string including `/` | `str` |
118
+
119
+ When no converter is specified, `str` is used:
120
+
121
+ ```python
122
+ path("search/<query>/", views.SearchView) # Same as <str:query>
61
123
  ```
62
124
 
63
- ## URL args and kwargs
125
+ ### Custom converters
64
126
 
65
- URL patterns can include arguments and keyword arguments.
127
+ You can register your own converters using [`register_converter()`](./converters.py#register_converter). A converter class needs a `regex` attribute and `to_python()` / `to_url()` methods:
66
128
 
67
129
  ```python
68
- # app/urls.py
69
- from plain.urls import Router, path
70
- from . import views
130
+ from plain.urls import register_converter
71
131
 
72
132
 
73
- class AppRouter(Router):
74
- namespace = ""
75
- urls = [
76
- path("user/<int:user_id>/", views.UserView, name="user"),
77
- path("search/<str:query>/", views.SearchView, name="search"),
78
- ]
133
+ class YearConverter:
134
+ regex = "[0-9]{4}"
135
+
136
+ def to_python(self, value):
137
+ return int(value)
138
+
139
+ def to_url(self, value):
140
+ return str(value)
141
+
142
+
143
+ register_converter(YearConverter, "year")
79
144
  ```
80
145
 
81
- These will be accessible inside the view as `self.url_args` and `self.url_kwargs`.
146
+ Then use it in your patterns:
82
147
 
83
148
  ```python
84
- # app/views.py
85
- from plain.views import View
149
+ path("archive/<year:year>/", views.ArchiveView, name="archive")
150
+ ```
86
151
 
152
+ ## Reversing URLs
87
153
 
88
- class SearchView(View):
89
- def get(self):
90
- query = self.url_kwargs["query"]
91
- print(f"Searching for {query}")
92
- # ...
154
+ ### In templates
155
+
156
+ Use the `url()` function to generate URLs by name:
157
+
158
+ ```html
159
+ <a href="{{ url('about') }}">About</a>
160
+ <a href="{{ url('user', user_id=42) }}">User Profile</a>
161
+ <a href="{{ url('admin:dashboard') }}">Admin Dashboard</a>
93
162
  ```
94
163
 
95
- To reverse a URL with args or kwargs, simply pass them in the `reverse` function.
164
+ ### In Python code
165
+
166
+ Use `reverse()` to generate URLs programmatically:
96
167
 
97
168
  ```python
98
169
  from plain.urls import reverse
99
170
 
100
- url = reverse("search", query="example")
171
+ url = reverse("about") # "/about/"
172
+ url = reverse("user", user_id=42) # "/user/42/"
173
+ url = reverse("admin:dashboard") # "/admin/dashboard/"
101
174
  ```
102
175
 
103
- There are a handful of built-in [converters](converters.py#DEFAULT_CONVERTERS) that can be used in URL patterns.
176
+ If the URL name does not exist or the arguments do not match, `reverse()` raises [`NoReverseMatch`](./exceptions.py#NoReverseMatch).
177
+
178
+ ### Lazy reverse
179
+
180
+ Use `reverse_lazy()` when you need a URL at module load time (such as in class attributes or default arguments):
104
181
 
105
182
  ```python
106
- from plain.urls import Router, path
107
- from . import views
183
+ from plain.urls import reverse_lazy
108
184
 
109
185
 
110
- class AppRouter(Router):
111
- namespace = ""
112
- urls = [
113
- path("user/<int:user_id>/", views.UserView, name="user"),
114
- path("search/<str:query>/", views.SearchView, name="search"),
115
- path("post/<slug:post_slug>/", views.PostView, name="post"),
116
- path("document/<uuid:uuid>/", views.DocumentView, name="document"),
117
- path("path/<path:subpath>/", views.PathView, name="path"),
118
- ]
186
+ class MyView(View):
187
+ success_url = reverse_lazy("home")
119
188
  ```
120
189
 
121
- ## Package routers
190
+ The URL is not resolved until it is actually used as a string.
191
+
192
+ ## Regex patterns
122
193
 
123
- Installed packages will often provide a URL router to include in your root URL router.
194
+ For complex matching that path converters cannot handle, you can use regular expressions:
124
195
 
125
196
  ```python
126
- # plain/assets/urls.py
127
- from plain.urls import Router, path
128
- from .views import AssetView
197
+ import re
198
+ from plain.urls import path
129
199
 
200
+ path(re.compile(r"^articles/(?P<year>[0-9]{4})/$"), views.ArticleView, name="article")
201
+ ```
130
202
 
131
- class AssetsRouter(Router):
132
- """
133
- The router for serving static assets.
203
+ Named groups become keyword arguments. Unnamed groups become positional arguments accessible via `self.url_args`.
134
204
 
135
- Include this router in your app router if you are serving assets yourself.
136
- """
205
+ ## FAQs
137
206
 
138
- namespace = "assets"
139
- urls = [
140
- path("<path:path>", AssetView, name="asset"),
141
- ]
142
- ```
207
+ #### Why does my URL pattern need a trailing slash?
208
+
209
+ By default, Plain's `APPEND_SLASH` setting redirects URLs without a trailing slash to URLs with one. Define your patterns with trailing slashes to match this behavior. If you prefer URLs without trailing slashes, set `APPEND_SLASH = False` in your settings.
210
+
211
+ #### How do I debug URL routing issues?
212
+
213
+ Check that your URL patterns are in the correct order. Plain matches patterns top to bottom and uses the first match. More specific patterns should come before general ones.
214
+
215
+ #### Can I access URL arguments as positional args instead of kwargs?
143
216
 
144
- Import the package's router and `include` it at any path you choose.
217
+ If you use regex patterns with unnamed groups (no `?P<name>`), values are passed as positional arguments in `self.url_args`. Named groups always populate `self.url_kwargs`.
218
+
219
+ ## Installation
220
+
221
+ The `plain.urls` module is included with Plain by default. No additional installation is required.
222
+
223
+ To set up URL routing, create a router in `app/urls.py` and point to it in your settings:
145
224
 
146
225
  ```python
147
- from plain.urls import include, Router
148
- from plain.assets.urls import AssetsRouter
226
+ # app/settings.py
227
+ URLS_ROUTER = "app.urls.AppRouter"
228
+ ```
229
+
230
+ ```python
231
+ # app/urls.py
232
+ from plain.urls import Router, path
233
+ from . import views
149
234
 
150
235
 
151
236
  class AppRouter(Router):
152
237
  namespace = ""
153
238
  urls = [
154
- include("assets/", AssetsRouter),
155
- # Your other URLs here...
239
+ path("", views.HomeView),
156
240
  ]
157
241
  ```
plain/urls/converters.py CHANGED
@@ -1,34 +1,37 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import uuid
5
+ from typing import Any
3
6
 
4
7
 
5
8
  class IntConverter:
6
9
  regex = "[0-9]+"
7
10
 
8
- def to_python(self, value):
11
+ def to_python(self, value: str) -> int:
9
12
  return int(value)
10
13
 
11
- def to_url(self, value):
14
+ def to_url(self, value: int) -> str:
12
15
  return str(value)
13
16
 
14
17
 
15
18
  class StringConverter:
16
19
  regex = "[^/]+"
17
20
 
18
- def to_python(self, value):
21
+ def to_python(self, value: str) -> str:
19
22
  return value
20
23
 
21
- def to_url(self, value):
24
+ def to_url(self, value: str) -> str:
22
25
  return value
23
26
 
24
27
 
25
28
  class UUIDConverter:
26
29
  regex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
27
30
 
28
- def to_python(self, value):
31
+ def to_python(self, value: str) -> uuid.UUID:
29
32
  return uuid.UUID(value)
30
33
 
31
- def to_url(self, value):
34
+ def to_url(self, value: uuid.UUID) -> str:
32
35
  return str(value)
33
36
 
34
37
 
@@ -40,7 +43,7 @@ class PathConverter(StringConverter):
40
43
  regex = ".+"
41
44
 
42
45
 
43
- DEFAULT_CONVERTERS = {
46
+ _DEFAULT_CONVERTERS = {
44
47
  "int": IntConverter(),
45
48
  "path": PathConverter(),
46
49
  "slug": SlugConverter(),
@@ -49,18 +52,18 @@ DEFAULT_CONVERTERS = {
49
52
  }
50
53
 
51
54
 
52
- REGISTERED_CONVERTERS = {}
55
+ _REGISTERED_CONVERTERS: dict[str, Any] = {}
53
56
 
54
57
 
55
- def register_converter(converter, type_name):
56
- REGISTERED_CONVERTERS[type_name] = converter()
57
- get_converters.cache_clear()
58
+ def register_converter(converter: type, type_name: str) -> None:
59
+ _REGISTERED_CONVERTERS[type_name] = converter()
60
+ _get_converters.cache_clear()
58
61
 
59
62
 
60
63
  @functools.cache
61
- def get_converters():
62
- return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS}
64
+ def _get_converters() -> dict[str, Any]:
65
+ return {**_DEFAULT_CONVERTERS, **_REGISTERED_CONVERTERS}
63
66
 
64
67
 
65
- def get_converter(raw_converter):
66
- return get_converters()[raw_converter]
68
+ def _get_converter(raw_converter: str) -> Any:
69
+ return _get_converters()[raw_converter]
plain/urls/exceptions.py CHANGED
@@ -1,7 +1,7 @@
1
- from plain.http import Http404
1
+ from plain.http import NotFoundError404
2
2
 
3
3
 
4
- class Resolver404(Http404):
4
+ class Resolver404(NotFoundError404):
5
5
  pass
6
6
 
7
7