plain 0.21.5__py3-none-any.whl → 0.22.0__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/.pytype/.gitignore +2 -0
- plain/.pytype/.ninja_log +17 -0
- plain/.pytype/build.ninja +318 -0
- plain/.pytype/imports/..packages.__init__.imports +8 -0
- plain/.pytype/imports/..packages.__init__.imports-1 +4 -0
- plain/.pytype/imports/__main__.imports +0 -0
- plain/.pytype/imports/assets.__init__.imports +0 -0
- plain/.pytype/imports/assets.compile.imports +10 -0
- plain/.pytype/imports/assets.finders.imports +7 -0
- plain/.pytype/imports/assets.fingerprints.imports +7 -0
- plain/.pytype/imports/assets.urls.imports +51 -0
- plain/.pytype/imports/assets.urls.imports-1 +35 -0
- plain/.pytype/imports/assets.views.imports +51 -0
- plain/.pytype/imports/assets.views.imports-1 +35 -0
- plain/.pytype/imports/cli.__init__.imports +16 -0
- plain/.pytype/imports/cli.cli.imports +15 -0
- plain/.pytype/imports/cli.formatting.imports +2 -0
- plain/.pytype/imports/cli.packages.imports +8 -0
- plain/.pytype/imports/cli.print.imports +1 -0
- plain/.pytype/imports/cli.startup.imports +7 -0
- plain/.pytype/imports/debug.imports +2 -0
- plain/.pytype/imports/default.pyi +3 -0
- plain/.pytype/imports/forms.__init__.imports +23 -0
- plain/.pytype/imports/forms.boundfield.imports +0 -0
- plain/.pytype/imports/forms.exceptions.imports +1 -0
- plain/.pytype/imports/forms.fields.imports +13 -0
- plain/.pytype/imports/forms.forms.imports +15 -0
- plain/.pytype/imports/http.__init__.imports +20 -0
- plain/.pytype/imports/http.__init__.imports-1 +17 -0
- plain/.pytype/imports/http.cookie.imports +0 -0
- plain/.pytype/imports/http.multipartparser.imports +14 -0
- plain/.pytype/imports/http.multipartparser.imports-1 +17 -0
- plain/.pytype/imports/http.request.imports +14 -0
- plain/.pytype/imports/http.request.imports-1 +25 -0
- plain/.pytype/imports/http.response.imports +16 -0
- plain/.pytype/imports/internal.__init__.imports +0 -0
- plain/.pytype/imports/internal.files.__init__.imports +1 -0
- plain/.pytype/imports/internal.files.base.imports +2 -0
- plain/.pytype/imports/internal.files.locks.imports +0 -0
- plain/.pytype/imports/internal.files.move.imports +1 -0
- plain/.pytype/imports/internal.files.temp.imports +1 -0
- plain/.pytype/imports/internal.files.uploadedfile.imports +10 -0
- plain/.pytype/imports/internal.files.uploadhandler.imports +12 -0
- plain/.pytype/imports/internal.files.utils.imports +1 -0
- plain/.pytype/imports/internal.handlers.__init__.imports +0 -0
- plain/.pytype/imports/internal.handlers.base.imports +53 -0
- plain/.pytype/imports/internal.handlers.exception.imports +10 -0
- plain/.pytype/imports/internal.handlers.wsgi.imports +29 -0
- plain/.pytype/imports/internal.middleware.__init__.imports +0 -0
- plain/.pytype/imports/internal.middleware.headers.imports +7 -0
- plain/.pytype/imports/internal.middleware.https.imports +8 -0
- plain/.pytype/imports/internal.middleware.slash.imports +51 -0
- plain/.pytype/imports/json.imports +12 -0
- plain/.pytype/imports/json.imports-1 +6 -0
- plain/.pytype/imports/json_encoding.imports +3 -0
- plain/.pytype/imports/logs.configure.imports +0 -0
- plain/.pytype/imports/logs.loggers.imports +0 -0
- plain/.pytype/imports/logs.utils.imports +0 -0
- plain/.pytype/imports/middleware.imports +13 -0
- plain/.pytype/imports/packages.__init__.imports +8 -0
- plain/.pytype/imports/packages.__init__.imports-1 +4 -0
- plain/.pytype/imports/packages.config.imports +0 -0
- plain/.pytype/imports/packages.registry.imports +7 -0
- plain/.pytype/imports/packages.registry.imports-1 +3 -0
- plain/.pytype/imports/paginator.imports +2 -0
- plain/.pytype/imports/preflight.__init__.imports +72 -0
- plain/.pytype/imports/preflight.__init__.imports-1 +53 -0
- plain/.pytype/imports/preflight.files.imports +8 -0
- plain/.pytype/imports/preflight.files.imports-1 +48 -0
- plain/.pytype/imports/preflight.messages.imports +7 -0
- plain/.pytype/imports/preflight.registry.imports +2 -0
- plain/.pytype/imports/preflight.security.imports +11 -0
- plain/.pytype/imports/preflight.urls.imports +51 -0
- plain/.pytype/imports/preflight.urls.imports-1 +48 -0
- plain/.pytype/imports/runtime.global_settings.imports +7 -0
- plain/.pytype/imports/runtime.user_settings.imports +7 -0
- plain/.pytype/imports/runtime.user_settings.imports-1 +3 -0
- plain/.pytype/imports/signals.dispatch.__init__.imports +10 -0
- plain/.pytype/imports/signals.dispatch.dispatcher.imports +8 -0
- plain/.pytype/imports/templates.__init__.imports +51 -0
- plain/.pytype/imports/templates.__init__.imports-1 +35 -0
- plain/.pytype/imports/templates.core.imports +51 -0
- plain/.pytype/imports/templates.core.imports-1 +35 -0
- plain/.pytype/imports/templates.jinja.__init__.imports +51 -0
- plain/.pytype/imports/templates.jinja.__init__.imports-1 +35 -0
- plain/.pytype/imports/templates.jinja.environments.imports +51 -0
- plain/.pytype/imports/templates.jinja.environments.imports-1 +35 -0
- plain/.pytype/imports/templates.jinja.extensions.imports +2 -0
- plain/.pytype/imports/templates.jinja.filters.imports +7 -0
- plain/.pytype/imports/templates.jinja.globals.imports +51 -0
- plain/.pytype/imports/templates.jinja.globals.imports-1 +35 -0
- plain/.pytype/imports/test.__init__.imports +0 -0
- plain/.pytype/imports/test.client.imports +58 -0
- plain/.pytype/imports/urls.base.imports +51 -0
- plain/.pytype/imports/urls.base.imports-1 +35 -0
- plain/.pytype/imports/urls.conf.imports +51 -0
- plain/.pytype/imports/urls.conf.imports-1 +35 -0
- plain/.pytype/imports/urls.converters.imports +0 -0
- plain/.pytype/imports/urls.exceptions.imports +1 -0
- plain/.pytype/imports/urls.resolvers.imports +51 -0
- plain/.pytype/imports/urls.resolvers.imports-1 +35 -0
- plain/.pytype/imports/utils.__init__.imports +0 -0
- plain/.pytype/imports/utils._os.imports +1 -0
- plain/.pytype/imports/utils.cache.imports +28 -0
- plain/.pytype/imports/utils.connection.imports +8 -0
- plain/.pytype/imports/utils.datastructures.imports +0 -0
- plain/.pytype/imports/utils.dateformat.imports +4 -0
- plain/.pytype/imports/utils.dateparse.imports +2 -0
- plain/.pytype/imports/utils.dates.imports +0 -0
- plain/.pytype/imports/utils.deconstruct.imports +0 -0
- plain/.pytype/imports/utils.decorators.imports +0 -0
- plain/.pytype/imports/utils.deprecation.imports +0 -0
- plain/.pytype/imports/utils.duration.imports +0 -0
- plain/.pytype/imports/utils.email.imports +0 -0
- plain/.pytype/imports/utils.encoding.imports +1 -0
- plain/.pytype/imports/utils.hashable.imports +1 -0
- plain/.pytype/imports/utils.html.imports +10 -0
- plain/.pytype/imports/utils.inspect.imports +0 -0
- plain/.pytype/imports/utils.ipv6.imports +1 -0
- plain/.pytype/imports/utils.itercompat.imports +0 -0
- plain/.pytype/imports/utils.module_loading.imports +0 -0
- plain/.pytype/imports/utils.regex_helper.imports +1 -0
- plain/.pytype/imports/utils.safestring.imports +1 -0
- plain/.pytype/imports/utils.text.imports +3 -0
- plain/.pytype/imports/utils.timesince.imports +6 -0
- plain/.pytype/imports/utils.timezone.imports +8 -0
- plain/.pytype/imports/utils.tree.imports +2 -0
- plain/.pytype/imports/validators.imports +7 -0
- plain/.pytype/imports/views.__init__.imports +72 -0
- plain/.pytype/imports/views.__init__.imports-1 +53 -0
- plain/.pytype/imports/views.base.imports +3 -0
- plain/.pytype/imports/views.csrf.imports +1 -0
- plain/.pytype/imports/views.errors.imports +51 -0
- plain/.pytype/imports/views.exceptions.imports +0 -0
- plain/.pytype/imports/views.forms.imports +51 -0
- plain/.pytype/imports/views.forms.imports-1 +35 -0
- plain/.pytype/imports/views.imports +51 -0
- plain/.pytype/imports/views.objects.imports +51 -0
- plain/.pytype/imports/views.objects.imports-1 +35 -0
- plain/.pytype/imports/views.redirect.imports +51 -0
- plain/.pytype/imports/views.redirect.imports-1 +35 -0
- plain/.pytype/imports/views.templates.imports +51 -0
- plain/.pytype/imports/views.templates.imports-1 +35 -0
- plain/.pytype/imports/wsgi.imports +8 -0
- plain/.pytype/pyi/json.pyi +15 -0
- plain/.pytype/pyi/json.pyi-1 +15 -0
- plain/.pytype/pyi/logs/configure.pyi +8 -0
- plain/.pytype/pyi/logs/loggers.pyi +20 -0
- plain/.pytype/pyi/logs/utils.pyi +7 -0
- plain/.pytype/pyi/packages/__init__.pyi +8 -0
- plain/.pytype/pyi/packages/__init__.pyi-1 +8 -0
- plain/.pytype/pyi/packages/config.pyi +35 -0
- plain/.pytype/pyi/packages/registry.pyi +49 -0
- plain/.pytype/pyi/packages/registry.pyi-1 +49 -0
- plain/.pytype/pyi/runtime/user_settings.pyi +64 -0
- plain/.pytype/pyi/runtime/user_settings.pyi-1 +64 -0
- plain/.pytype/pyi/utils/duration.pyi +9 -0
- plain/.pytype/pyi/utils/hashable.pyi +9 -0
- plain/.pytype/pyi/utils/itercompat.pyi +3 -0
- plain/.pytype/pyi/utils/module_loading.pyi +14 -0
- plain/assets/urls.py +9 -8
- plain/assets/views.py +4 -4
- plain/cli/cli.py +90 -7
- plain/http/request.py +1 -2
- plain/internal/handlers/base.py +9 -23
- plain/internal/handlers/exception.py +2 -0
- plain/internal/middleware/slash.py +15 -7
- plain/packages/registry.py +3 -77
- plain/preflight/urls.py +2 -2
- plain/runtime/global_settings.py +1 -1
- plain/templates/jinja/globals.py +2 -8
- plain/test/client.py +10 -10
- plain/urls/__init__.py +8 -19
- plain/urls/patterns.py +271 -0
- plain/urls/resolvers.py +48 -360
- plain/urls/routers.py +90 -0
- plain/urls/{base.py → utils.py} +5 -61
- plain/utils/functional.py +1 -2
- plain/views/redirect.py +1 -1
- plain/views/templates.py +1 -1
- {plain-0.21.5.dist-info → plain-0.22.0.dist-info}/METADATA +1 -1
- plain-0.22.0.dist-info/RECORD +305 -0
- plain/urls/conf.py +0 -95
- plain/utils/deprecation.py +0 -6
- plain-0.21.5.dist-info/RECORD +0 -145
- {plain-0.21.5.dist-info → plain-0.22.0.dist-info}/WHEEL +0 -0
- {plain-0.21.5.dist-info → plain-0.22.0.dist-info}/entry_points.txt +0 -0
- {plain-0.21.5.dist-info → plain-0.22.0.dist-info}/licenses/LICENSE +0 -0
plain/urls/resolvers.py
CHANGED
@@ -7,25 +7,21 @@ attributes of the resolved URL match.
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
import functools
|
10
|
-
import inspect
|
11
10
|
import re
|
12
|
-
import string
|
13
11
|
from importlib import import_module
|
14
12
|
from pickle import PicklingError
|
15
13
|
from threading import local
|
16
14
|
from urllib.parse import quote
|
17
15
|
|
18
|
-
from plain.exceptions import ImproperlyConfigured
|
19
|
-
from plain.preflight import Error, Warning
|
20
16
|
from plain.preflight.urls import check_resolver
|
21
17
|
from plain.runtime import settings
|
22
18
|
from plain.utils.datastructures import MultiValueDict
|
23
19
|
from plain.utils.functional import cached_property
|
24
20
|
from plain.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
|
25
|
-
from plain.utils.regex_helper import
|
21
|
+
from plain.utils.regex_helper import normalize
|
26
22
|
|
27
|
-
from .converters import get_converter
|
28
23
|
from .exceptions import NoReverseMatch, Resolver404
|
24
|
+
from .patterns import RegexPattern, URLPattern
|
29
25
|
|
30
26
|
|
31
27
|
class ResolverMatch:
|
@@ -35,7 +31,6 @@ class ResolverMatch:
|
|
35
31
|
args,
|
36
32
|
kwargs,
|
37
33
|
url_name=None,
|
38
|
-
default_namespaces=None,
|
39
34
|
namespaces=None,
|
40
35
|
route=None,
|
41
36
|
tried=None,
|
@@ -51,12 +46,8 @@ class ResolverMatch:
|
|
51
46
|
self.captured_kwargs = captured_kwargs
|
52
47
|
self.extra_kwargs = extra_kwargs
|
53
48
|
|
54
|
-
# If a URLRegexResolver doesn't have a namespace or
|
49
|
+
# If a URLRegexResolver doesn't have a namespace or namespace, it passes
|
55
50
|
# in an empty value.
|
56
|
-
self.default_namespaces = (
|
57
|
-
[x for x in default_namespaces if x] if default_namespaces else []
|
58
|
-
)
|
59
|
-
self.default_namespace = ":".join(self.default_namespaces)
|
60
51
|
self.namespaces = [x for x in namespaces if x] if namespaces else []
|
61
52
|
self.namespace = ":".join(self.namespaces)
|
62
53
|
|
@@ -72,9 +63,6 @@ class ResolverMatch:
|
|
72
63
|
view_path = url_name or self._func_path
|
73
64
|
self.view_name = ":".join(self.namespaces + [view_path])
|
74
65
|
|
75
|
-
def __getitem__(self, index):
|
76
|
-
return (self.func, self.args, self.kwargs)[index]
|
77
|
-
|
78
66
|
def __repr__(self):
|
79
67
|
if isinstance(self.func, functools.partial):
|
80
68
|
func = repr(self.func)
|
@@ -82,12 +70,11 @@ class ResolverMatch:
|
|
82
70
|
func = self._func_path
|
83
71
|
return (
|
84
72
|
"ResolverMatch(func={}, args={!r}, kwargs={!r}, url_name={!r}, "
|
85
|
-
"
|
73
|
+
"namespaces={!r}, route={!r}{}{})".format(
|
86
74
|
func,
|
87
75
|
self.args,
|
88
76
|
self.kwargs,
|
89
77
|
self.url_name,
|
90
|
-
self.default_namespaces,
|
91
78
|
self.namespaces,
|
92
79
|
self.route,
|
93
80
|
f", captured_kwargs={self.captured_kwargs!r}"
|
@@ -101,324 +88,70 @@ class ResolverMatch:
|
|
101
88
|
raise PicklingError(f"Cannot pickle {self.__class__.__qualname__}.")
|
102
89
|
|
103
90
|
|
104
|
-
def get_resolver(
|
105
|
-
if
|
106
|
-
|
107
|
-
|
91
|
+
def get_resolver(urls_module=None):
|
92
|
+
if urls_module is None:
|
93
|
+
urls_module = settings.URLS_MODULE
|
94
|
+
|
95
|
+
return _get_cached_resolver(urls_module)
|
108
96
|
|
109
97
|
|
110
98
|
@functools.cache
|
111
|
-
def _get_cached_resolver(
|
112
|
-
|
99
|
+
def _get_cached_resolver(urls_module):
|
100
|
+
from .routers import routers
|
101
|
+
|
102
|
+
if isinstance(urls_module, str):
|
103
|
+
# Need to trigger an import in order for the @register_router
|
104
|
+
# decorators to run. So this is a sensible entrypoint to do that,
|
105
|
+
# usually just for the root URLS_MODULE but could be for anything.
|
106
|
+
urls_module = import_module(urls_module)
|
107
|
+
|
108
|
+
router = routers.get_module_router(urls_module)
|
109
|
+
return URLResolver(pattern=RegexPattern(r"^/"), router_class=router)
|
113
110
|
|
114
111
|
|
115
112
|
@functools.cache
|
116
113
|
def get_ns_resolver(ns_pattern, resolver, converters):
|
117
|
-
|
114
|
+
from .routers import RouterBase
|
115
|
+
|
116
|
+
# Build a namespaced resolver for the given parent urls_module pattern.
|
118
117
|
# This makes it possible to have captured parameters in the parent
|
119
|
-
#
|
118
|
+
# urls_module pattern.
|
120
119
|
pattern = RegexPattern(ns_pattern)
|
121
120
|
pattern.converters = dict(converters)
|
122
|
-
ns_resolver = URLResolver(pattern, resolver.url_patterns)
|
123
|
-
return URLResolver(RegexPattern(r"^/"), [ns_resolver])
|
124
|
-
|
125
|
-
|
126
|
-
class CheckURLMixin:
|
127
|
-
def describe(self):
|
128
|
-
"""
|
129
|
-
Format the URL pattern for display in warning messages.
|
130
|
-
"""
|
131
|
-
description = f"'{self}'"
|
132
|
-
if self.name:
|
133
|
-
description += f" [name='{self.name}']"
|
134
|
-
return description
|
135
|
-
|
136
|
-
def _check_pattern_startswith_slash(self):
|
137
|
-
"""
|
138
|
-
Check that the pattern does not begin with a forward slash.
|
139
|
-
"""
|
140
|
-
regex_pattern = self.regex.pattern
|
141
|
-
if not settings.APPEND_SLASH:
|
142
|
-
# Skip check as it can be useful to start a URL pattern with a slash
|
143
|
-
# when APPEND_SLASH=False.
|
144
|
-
return []
|
145
|
-
if regex_pattern.startswith(("/", "^/", "^\\/")) and not regex_pattern.endswith(
|
146
|
-
"/"
|
147
|
-
):
|
148
|
-
warning = Warning(
|
149
|
-
f"Your URL pattern {self.describe()} has a route beginning with a '/'. Remove this "
|
150
|
-
"slash as it is unnecessary. If this pattern is targeted in an "
|
151
|
-
"include(), ensure the include() pattern has a trailing '/'.",
|
152
|
-
id="urls.W002",
|
153
|
-
)
|
154
|
-
return [warning]
|
155
|
-
else:
|
156
|
-
return []
|
157
|
-
|
158
|
-
|
159
|
-
class RegexPattern(CheckURLMixin):
|
160
|
-
def __init__(self, regex, name=None, is_endpoint=False):
|
161
|
-
self._regex = regex
|
162
|
-
self._regex_dict = {}
|
163
|
-
self._is_endpoint = is_endpoint
|
164
|
-
self.name = name
|
165
|
-
self.converters = {}
|
166
|
-
self.regex = self._compile(str(regex))
|
167
|
-
|
168
|
-
def match(self, path):
|
169
|
-
match = (
|
170
|
-
self.regex.fullmatch(path)
|
171
|
-
if self._is_endpoint and self.regex.pattern.endswith("$")
|
172
|
-
else self.regex.search(path)
|
173
|
-
)
|
174
|
-
if match:
|
175
|
-
# If there are any named groups, use those as kwargs, ignoring
|
176
|
-
# non-named groups. Otherwise, pass all non-named arguments as
|
177
|
-
# positional arguments.
|
178
|
-
kwargs = match.groupdict()
|
179
|
-
args = () if kwargs else match.groups()
|
180
|
-
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
181
|
-
return path[match.end() :], args, kwargs
|
182
|
-
return None
|
183
121
|
|
184
|
-
|
185
|
-
|
186
|
-
warnings.extend(self._check_pattern_startswith_slash())
|
187
|
-
if not self._is_endpoint:
|
188
|
-
warnings.extend(self._check_include_trailing_dollar())
|
189
|
-
return warnings
|
190
|
-
|
191
|
-
def _check_include_trailing_dollar(self):
|
192
|
-
regex_pattern = self.regex.pattern
|
193
|
-
if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
|
194
|
-
return [
|
195
|
-
Warning(
|
196
|
-
f"Your URL pattern {self.describe()} uses include with a route ending with a '$'. "
|
197
|
-
"Remove the dollar from the route to avoid problems including "
|
198
|
-
"URLs.",
|
199
|
-
id="urls.W001",
|
200
|
-
)
|
201
|
-
]
|
202
|
-
else:
|
203
|
-
return []
|
122
|
+
class _NestedRouter(RouterBase):
|
123
|
+
urls = resolver.url_patterns
|
204
124
|
|
205
|
-
|
206
|
-
"""Compile and return the given regular expression."""
|
207
|
-
try:
|
208
|
-
return re.compile(regex)
|
209
|
-
except re.error as e:
|
210
|
-
raise ImproperlyConfigured(
|
211
|
-
f'"{regex}" is not a valid regular expression: {e}'
|
212
|
-
) from e
|
213
|
-
|
214
|
-
def __str__(self):
|
215
|
-
return str(self._regex)
|
216
|
-
|
217
|
-
|
218
|
-
_PATH_PARAMETER_COMPONENT_RE = _lazy_re_compile(
|
219
|
-
r"<(?:(?P<converter>[^>:]+):)?(?P<parameter>[^>]+)>"
|
220
|
-
)
|
221
|
-
|
222
|
-
|
223
|
-
def _route_to_regex(route, is_endpoint=False):
|
224
|
-
"""
|
225
|
-
Convert a path pattern into a regular expression. Return the regular
|
226
|
-
expression and a dictionary mapping the capture names to the converters.
|
227
|
-
For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
|
228
|
-
and {'pk': <plain.urls.converters.IntConverter>}.
|
229
|
-
"""
|
230
|
-
original_route = route
|
231
|
-
parts = ["^"]
|
232
|
-
converters = {}
|
233
|
-
while True:
|
234
|
-
match = _PATH_PARAMETER_COMPONENT_RE.search(route)
|
235
|
-
if not match:
|
236
|
-
parts.append(re.escape(route))
|
237
|
-
break
|
238
|
-
elif not set(match.group()).isdisjoint(string.whitespace):
|
239
|
-
raise ImproperlyConfigured(
|
240
|
-
f"URL route '{original_route}' cannot contain whitespace in angle brackets "
|
241
|
-
"<…>."
|
242
|
-
)
|
243
|
-
parts.append(re.escape(route[: match.start()]))
|
244
|
-
route = route[match.end() :]
|
245
|
-
parameter = match["parameter"]
|
246
|
-
if not parameter.isidentifier():
|
247
|
-
raise ImproperlyConfigured(
|
248
|
-
f"URL route '{original_route}' uses parameter name {parameter!r} which isn't a valid "
|
249
|
-
"Python identifier."
|
250
|
-
)
|
251
|
-
raw_converter = match["converter"]
|
252
|
-
if raw_converter is None:
|
253
|
-
# If a converter isn't specified, the default is `str`.
|
254
|
-
raw_converter = "str"
|
255
|
-
try:
|
256
|
-
converter = get_converter(raw_converter)
|
257
|
-
except KeyError as e:
|
258
|
-
raise ImproperlyConfigured(
|
259
|
-
f"URL route {original_route!r} uses invalid converter {raw_converter!r}."
|
260
|
-
) from e
|
261
|
-
converters[parameter] = converter
|
262
|
-
parts.append("(?P<" + parameter + ">" + converter.regex + ")")
|
263
|
-
if is_endpoint:
|
264
|
-
parts.append(r"\Z")
|
265
|
-
return "".join(parts), converters
|
266
|
-
|
267
|
-
|
268
|
-
class RoutePattern(CheckURLMixin):
|
269
|
-
def __init__(self, route, name=None, is_endpoint=False):
|
270
|
-
self._route = route
|
271
|
-
self._regex_dict = {}
|
272
|
-
self._is_endpoint = is_endpoint
|
273
|
-
self.name = name
|
274
|
-
self.converters = _route_to_regex(str(route), is_endpoint)[1]
|
275
|
-
self.regex = self._compile(str(route))
|
276
|
-
|
277
|
-
def match(self, path):
|
278
|
-
match = self.regex.search(path)
|
279
|
-
if match:
|
280
|
-
# RoutePattern doesn't allow non-named groups so args are ignored.
|
281
|
-
kwargs = match.groupdict()
|
282
|
-
for key, value in kwargs.items():
|
283
|
-
converter = self.converters[key]
|
284
|
-
try:
|
285
|
-
kwargs[key] = converter.to_python(value)
|
286
|
-
except ValueError:
|
287
|
-
return None
|
288
|
-
return path[match.end() :], (), kwargs
|
289
|
-
return None
|
290
|
-
|
291
|
-
def check(self):
|
292
|
-
warnings = self._check_pattern_startswith_slash()
|
293
|
-
route = self._route
|
294
|
-
if "(?P<" in route or route.startswith("^") or route.endswith("$"):
|
295
|
-
warnings.append(
|
296
|
-
Warning(
|
297
|
-
f"Your URL pattern {self.describe()} has a route that contains '(?P<', begins "
|
298
|
-
"with a '^', or ends with a '$'. This was likely an oversight "
|
299
|
-
"when migrating to plain.urls.path().",
|
300
|
-
id="2_0.W001",
|
301
|
-
)
|
302
|
-
)
|
303
|
-
return warnings
|
304
|
-
|
305
|
-
def _compile(self, route):
|
306
|
-
return re.compile(_route_to_regex(route, self._is_endpoint)[0])
|
125
|
+
ns_resolver = URLResolver(pattern=pattern, router_class=_NestedRouter)
|
307
126
|
|
308
|
-
|
309
|
-
|
127
|
+
class _NamespacedRouter(RouterBase):
|
128
|
+
urls = [ns_resolver]
|
310
129
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
self.callback = callback # the view
|
316
|
-
self.default_args = default_args or {}
|
317
|
-
self.name = name
|
318
|
-
|
319
|
-
def __repr__(self):
|
320
|
-
return f"<{self.__class__.__name__} {self.pattern.describe()}>"
|
321
|
-
|
322
|
-
def check(self):
|
323
|
-
warnings = self._check_pattern_name()
|
324
|
-
warnings.extend(self.pattern.check())
|
325
|
-
warnings.extend(self._check_callback())
|
326
|
-
return warnings
|
327
|
-
|
328
|
-
def _check_pattern_name(self):
|
329
|
-
"""
|
330
|
-
Check that the pattern name does not contain a colon.
|
331
|
-
"""
|
332
|
-
if self.pattern.name is not None and ":" in self.pattern.name:
|
333
|
-
warning = Warning(
|
334
|
-
f"Your URL pattern {self.pattern.describe()} has a name including a ':'. Remove the colon, to "
|
335
|
-
"avoid ambiguous namespace references.",
|
336
|
-
id="urls.W003",
|
337
|
-
)
|
338
|
-
return [warning]
|
339
|
-
else:
|
340
|
-
return []
|
341
|
-
|
342
|
-
def _check_callback(self):
|
343
|
-
from plain.views import View
|
344
|
-
|
345
|
-
view = self.callback
|
346
|
-
if inspect.isclass(view) and issubclass(view, View):
|
347
|
-
return [
|
348
|
-
Error(
|
349
|
-
f"Your URL pattern {self.pattern.describe()} has an invalid view, pass {view.__name__}.as_view() "
|
350
|
-
f"instead of {view.__name__}.",
|
351
|
-
id="urls.E009",
|
352
|
-
)
|
353
|
-
]
|
354
|
-
return []
|
355
|
-
|
356
|
-
def resolve(self, path):
|
357
|
-
match = self.pattern.match(path)
|
358
|
-
if match:
|
359
|
-
new_path, args, captured_kwargs = match
|
360
|
-
# Pass any default args as **kwargs.
|
361
|
-
kwargs = {**captured_kwargs, **self.default_args}
|
362
|
-
return ResolverMatch(
|
363
|
-
self.callback,
|
364
|
-
args,
|
365
|
-
kwargs,
|
366
|
-
self.pattern.name,
|
367
|
-
route=str(self.pattern),
|
368
|
-
captured_kwargs=captured_kwargs,
|
369
|
-
extra_kwargs=self.default_args,
|
370
|
-
)
|
371
|
-
|
372
|
-
@cached_property
|
373
|
-
def lookup_str(self):
|
374
|
-
"""
|
375
|
-
A string that identifies the view (e.g. 'path.to.view_function' or
|
376
|
-
'path.to.ClassBasedView').
|
377
|
-
"""
|
378
|
-
callback = self.callback
|
379
|
-
if isinstance(callback, functools.partial):
|
380
|
-
callback = callback.func
|
381
|
-
if hasattr(callback, "view_class"):
|
382
|
-
callback = callback.view_class
|
383
|
-
elif not hasattr(callback, "__name__"):
|
384
|
-
return callback.__module__ + "." + callback.__class__.__name__
|
385
|
-
return callback.__module__ + "." + callback.__qualname__
|
130
|
+
return URLResolver(
|
131
|
+
pattern=RegexPattern(r"^/"),
|
132
|
+
router_class=_NamespacedRouter,
|
133
|
+
)
|
386
134
|
|
387
135
|
|
388
136
|
class URLResolver:
|
389
137
|
def __init__(
|
390
138
|
self,
|
139
|
+
*,
|
391
140
|
pattern,
|
392
|
-
|
393
|
-
default_kwargs=None,
|
394
|
-
default_namespace=None,
|
141
|
+
router_class,
|
395
142
|
namespace=None,
|
396
143
|
):
|
397
144
|
self.pattern = pattern
|
398
|
-
|
399
|
-
# urlpatterns. It may also be an object with an urlpatterns attribute
|
400
|
-
# or urlpatterns itself.
|
401
|
-
self.urlconf_name = urlconf_name
|
402
|
-
self.callback = None
|
403
|
-
self.default_kwargs = default_kwargs or {}
|
145
|
+
self.router_class = router_class
|
404
146
|
self.namespace = namespace
|
405
|
-
self.default_namespace = default_namespace
|
406
147
|
self._reverse_dict = {}
|
407
148
|
self._namespace_dict = {}
|
408
149
|
self._app_dict = {}
|
409
|
-
# set of dotted paths to all functions and classes that are used in
|
410
|
-
# urlpatterns
|
411
|
-
self._callback_strs = set()
|
412
150
|
self._populated = False
|
413
151
|
self._local = local()
|
414
152
|
|
415
153
|
def __repr__(self):
|
416
|
-
|
417
|
-
# Don't bother to output the whole list, it can be huge
|
418
|
-
urlconf_repr = f"<{self.urlconf_name[0].__class__.__name__} list>"
|
419
|
-
else:
|
420
|
-
urlconf_repr = repr(self.urlconf_name)
|
421
|
-
return f"<{self.__class__.__name__} {urlconf_repr} ({self.default_namespace}:{self.namespace}) {self.pattern.describe()}>"
|
154
|
+
return f"<{self.__class__.__name__} {repr(self.router_class)} ({self.namespace}) {self.pattern.describe()}>"
|
422
155
|
|
423
156
|
def check(self):
|
424
157
|
messages = []
|
@@ -442,14 +175,12 @@ class URLResolver:
|
|
442
175
|
p_pattern = url_pattern.pattern.regex.pattern
|
443
176
|
p_pattern = p_pattern.removeprefix("^")
|
444
177
|
if isinstance(url_pattern, URLPattern):
|
445
|
-
self._callback_strs.add(url_pattern.lookup_str)
|
446
178
|
bits = normalize(url_pattern.pattern.regex.pattern)
|
447
179
|
lookups.appendlist(
|
448
|
-
url_pattern.
|
180
|
+
url_pattern.view,
|
449
181
|
(
|
450
182
|
bits,
|
451
183
|
p_pattern,
|
452
|
-
url_pattern.default_args,
|
453
184
|
url_pattern.pattern.converters,
|
454
185
|
),
|
455
186
|
)
|
@@ -459,14 +190,13 @@ class URLResolver:
|
|
459
190
|
(
|
460
191
|
bits,
|
461
192
|
p_pattern,
|
462
|
-
url_pattern.default_args,
|
463
193
|
url_pattern.pattern.converters,
|
464
194
|
),
|
465
195
|
)
|
466
196
|
else: # url_pattern is a URLResolver.
|
467
197
|
url_pattern._populate()
|
468
|
-
if url_pattern.
|
469
|
-
packages.setdefault(url_pattern.
|
198
|
+
if url_pattern.namespace:
|
199
|
+
packages.setdefault(url_pattern.namespace, []).append(
|
470
200
|
url_pattern.namespace
|
471
201
|
)
|
472
202
|
namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
|
@@ -475,7 +205,6 @@ class URLResolver:
|
|
475
205
|
for (
|
476
206
|
matches,
|
477
207
|
pat,
|
478
|
-
defaults,
|
479
208
|
converters,
|
480
209
|
) in url_pattern.reverse_dict.getlist(name):
|
481
210
|
new_matches = normalize(p_pattern + pat)
|
@@ -484,7 +213,6 @@ class URLResolver:
|
|
484
213
|
(
|
485
214
|
new_matches,
|
486
215
|
p_pattern + pat,
|
487
|
-
{**defaults, **url_pattern.default_kwargs},
|
488
216
|
{
|
489
217
|
**self.pattern.converters,
|
490
218
|
**url_pattern.pattern.converters,
|
@@ -500,13 +228,10 @@ class URLResolver:
|
|
500
228
|
sub_pattern.pattern.converters.update(current_converters)
|
501
229
|
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
|
502
230
|
for (
|
503
|
-
|
231
|
+
namespace,
|
504
232
|
namespace_list,
|
505
233
|
) in url_pattern.app_dict.items():
|
506
|
-
packages.setdefault(
|
507
|
-
namespace_list
|
508
|
-
)
|
509
|
-
self._callback_strs.update(url_pattern._callback_strs)
|
234
|
+
packages.setdefault(namespace, []).extend(namespace_list)
|
510
235
|
self._namespace_dict = namespaces
|
511
236
|
self._app_dict = packages
|
512
237
|
self._reverse_dict = lookups
|
@@ -547,11 +272,6 @@ class URLResolver:
|
|
547
272
|
route2 = route2.removeprefix("^")
|
548
273
|
return route1 + route2
|
549
274
|
|
550
|
-
def _is_callback(self, name):
|
551
|
-
if not self._populated:
|
552
|
-
self._populate()
|
553
|
-
return name in self._callback_strs
|
554
|
-
|
555
275
|
def resolve(self, path):
|
556
276
|
path = str(path) # path may be a reverse_lazy object
|
557
277
|
tried = []
|
@@ -566,9 +286,8 @@ class URLResolver:
|
|
566
286
|
else:
|
567
287
|
if sub_match:
|
568
288
|
# Merge captured arguments in match with submatch
|
569
|
-
sub_match_dict = {**kwargs, **self.default_kwargs}
|
570
289
|
# Update the sub_match_dict with the kwargs from the sub_match.
|
571
|
-
sub_match_dict
|
290
|
+
sub_match_dict = {**kwargs, **sub_match.kwargs}
|
572
291
|
# If there are *any* named groups, ignore all non-named groups.
|
573
292
|
# Otherwise, pass all non-named arguments as positional
|
574
293
|
# arguments.
|
@@ -586,42 +305,20 @@ class URLResolver:
|
|
586
305
|
sub_match_args,
|
587
306
|
sub_match_dict,
|
588
307
|
sub_match.url_name,
|
589
|
-
[self.default_namespace] + sub_match.default_namespaces,
|
590
308
|
[self.namespace] + sub_match.namespaces,
|
591
309
|
self._join_route(current_route, sub_match.route),
|
592
310
|
tried,
|
593
311
|
captured_kwargs=sub_match.captured_kwargs,
|
594
|
-
extra_kwargs=
|
595
|
-
**self.default_kwargs,
|
596
|
-
**sub_match.extra_kwargs,
|
597
|
-
},
|
312
|
+
extra_kwargs=sub_match.extra_kwargs,
|
598
313
|
)
|
599
314
|
tried.append([pattern])
|
600
315
|
raise Resolver404({"tried": tried, "path": new_path})
|
601
316
|
raise Resolver404({"path": path})
|
602
317
|
|
603
|
-
@cached_property
|
604
|
-
def urlconf_module(self):
|
605
|
-
if isinstance(self.urlconf_name, str):
|
606
|
-
return import_module(self.urlconf_name)
|
607
|
-
else:
|
608
|
-
return self.urlconf_name
|
609
|
-
|
610
318
|
@cached_property
|
611
319
|
def url_patterns(self):
|
612
|
-
#
|
613
|
-
|
614
|
-
try:
|
615
|
-
iter(patterns)
|
616
|
-
except TypeError as e:
|
617
|
-
msg = (
|
618
|
-
"The included URLconf '{name}' does not appear to have "
|
619
|
-
"any patterns in it. If you see the 'urlpatterns' variable "
|
620
|
-
"with valid patterns in the file then the issue is probably "
|
621
|
-
"caused by a circular import."
|
622
|
-
)
|
623
|
-
raise ImproperlyConfigured(msg.format(name=self.urlconf_name)) from e
|
624
|
-
return patterns
|
320
|
+
# Don't need to instantiate the class because they are just class attributes for now.
|
321
|
+
return self.router_class.urls
|
625
322
|
|
626
323
|
def reverse(self, lookup_view, *args, **kwargs):
|
627
324
|
if args and kwargs:
|
@@ -632,23 +329,14 @@ class URLResolver:
|
|
632
329
|
|
633
330
|
possibilities = self.reverse_dict.getlist(lookup_view)
|
634
331
|
|
635
|
-
for possibility, pattern,
|
332
|
+
for possibility, pattern, converters in possibilities:
|
636
333
|
for result, params in possibility:
|
637
334
|
if args:
|
638
335
|
if len(args) != len(params):
|
639
336
|
continue
|
640
337
|
candidate_subs = dict(zip(params, args))
|
641
338
|
else:
|
642
|
-
if set(kwargs).symmetric_difference(params)
|
643
|
-
continue
|
644
|
-
matches = True
|
645
|
-
for k, v in defaults.items():
|
646
|
-
if k in params:
|
647
|
-
continue
|
648
|
-
if kwargs.get(k, v) != v:
|
649
|
-
matches = False
|
650
|
-
break
|
651
|
-
if not matches:
|
339
|
+
if set(kwargs).symmetric_difference(params):
|
652
340
|
continue
|
653
341
|
candidate_subs = kwargs
|
654
342
|
# Convert the candidate subs to text using Converter.to_url().
|
plain/urls/routers.py
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
from abc import ABC
|
2
|
+
|
3
|
+
from plain.exceptions import ImproperlyConfigured
|
4
|
+
|
5
|
+
from .patterns import RoutePattern, URLPattern
|
6
|
+
from .resolvers import (
|
7
|
+
URLResolver,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class RouterBase(ABC):
|
12
|
+
namespace: str = ""
|
13
|
+
urls: list # Required
|
14
|
+
|
15
|
+
|
16
|
+
class RoutersRegistry:
|
17
|
+
"""Keep track of all the Routers that are explicitly registered in packages."""
|
18
|
+
|
19
|
+
def __init__(self):
|
20
|
+
self._routers = {}
|
21
|
+
|
22
|
+
def register_router(self, router_class):
|
23
|
+
router_module_name = router_class.__module__
|
24
|
+
self._routers[router_module_name] = router_class
|
25
|
+
return router_class
|
26
|
+
|
27
|
+
def get_module_router(self, module):
|
28
|
+
if isinstance(module, str):
|
29
|
+
module_name = module
|
30
|
+
else:
|
31
|
+
module_name = module.__name__
|
32
|
+
|
33
|
+
try:
|
34
|
+
return self._routers[module_name]
|
35
|
+
except KeyError as e:
|
36
|
+
registered_routers = ", ".join(self._routers.keys()) or "None"
|
37
|
+
raise ImproperlyConfigured(
|
38
|
+
f"Router {module_name} is not registered with the resolver. Use @register_router on the Router class in urls.py.\n\nRegistered routers: {registered_routers}"
|
39
|
+
) from e
|
40
|
+
|
41
|
+
|
42
|
+
def include(route, module_or_urls, *, Pattern=RoutePattern):
|
43
|
+
pattern = Pattern(route, is_endpoint=False)
|
44
|
+
|
45
|
+
if isinstance(module_or_urls, list | tuple):
|
46
|
+
# We were given an explicit list of sub-patterns,
|
47
|
+
# so we generate a router for it
|
48
|
+
class _IncludeRouter(RouterBase):
|
49
|
+
urls = module_or_urls
|
50
|
+
|
51
|
+
return URLResolver(pattern=pattern, router_class=_IncludeRouter, namespace=None)
|
52
|
+
else:
|
53
|
+
# We were given a module, so we need to look up the router for that module
|
54
|
+
module = module_or_urls
|
55
|
+
router = routers.get_module_router(module)
|
56
|
+
namespace = router.namespace
|
57
|
+
|
58
|
+
return URLResolver(
|
59
|
+
pattern=pattern,
|
60
|
+
router_class=router,
|
61
|
+
namespace=namespace,
|
62
|
+
)
|
63
|
+
|
64
|
+
|
65
|
+
def path(route, view, *, name=None, Pattern=RoutePattern):
|
66
|
+
from plain.views import View
|
67
|
+
|
68
|
+
pattern = Pattern(route, name=name, is_endpoint=True)
|
69
|
+
|
70
|
+
# You can't pass a View() instance to path()
|
71
|
+
if isinstance(view, View):
|
72
|
+
view_cls_name = view.__class__.__name__
|
73
|
+
raise TypeError(
|
74
|
+
f"view must be a callable, pass {view_cls_name} or {view_cls_name}.as_view(*args, **kwargs), not "
|
75
|
+
f"{view_cls_name}()."
|
76
|
+
)
|
77
|
+
|
78
|
+
# You typically pass a View class and we call as_view() for you
|
79
|
+
if issubclass(view, View):
|
80
|
+
return URLPattern(pattern=pattern, view=view.as_view(), name=name)
|
81
|
+
|
82
|
+
# If you called View.as_view() yourself (or technically any callable)
|
83
|
+
if callable(view):
|
84
|
+
return URLPattern(pattern=pattern, view=view, name=name)
|
85
|
+
|
86
|
+
raise TypeError("view must be a View class or View.as_view()")
|
87
|
+
|
88
|
+
|
89
|
+
routers = RoutersRegistry()
|
90
|
+
register_router = routers.register_router
|