plain 0.21.5__py3-none-any.whl → 0.22.1__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/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 +51 -361
- plain/urls/routers.py +112 -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.1.dist-info}/METADATA +1 -1
- {plain-0.21.5.dist-info → plain-0.22.1.dist-info}/RECORD +25 -25
- plain/urls/conf.py +0 -95
- plain/utils/deprecation.py +0 -6
- {plain-0.21.5.dist-info → plain-0.22.1.dist-info}/WHEEL +0 -0
- {plain-0.21.5.dist-info → plain-0.22.1.dist-info}/entry_points.txt +0 -0
- {plain-0.21.5.dist-info → plain-0.22.1.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,68 @@ 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_registry
|
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_registry.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,
|
395
|
-
namespace=None,
|
141
|
+
router_class,
|
396
142
|
):
|
397
143
|
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 {}
|
404
|
-
self.namespace = namespace
|
405
|
-
self.default_namespace = default_namespace
|
144
|
+
self.router_class = router_class
|
406
145
|
self._reverse_dict = {}
|
407
146
|
self._namespace_dict = {}
|
408
147
|
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
148
|
self._populated = False
|
413
149
|
self._local = local()
|
414
150
|
|
415
151
|
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()}>"
|
152
|
+
return f"<{self.__class__.__name__} {repr(self.router_class)} ({self.namespace}) {self.pattern.describe()}>"
|
422
153
|
|
423
154
|
def check(self):
|
424
155
|
messages = []
|
@@ -442,14 +173,12 @@ class URLResolver:
|
|
442
173
|
p_pattern = url_pattern.pattern.regex.pattern
|
443
174
|
p_pattern = p_pattern.removeprefix("^")
|
444
175
|
if isinstance(url_pattern, URLPattern):
|
445
|
-
self._callback_strs.add(url_pattern.lookup_str)
|
446
176
|
bits = normalize(url_pattern.pattern.regex.pattern)
|
447
177
|
lookups.appendlist(
|
448
|
-
url_pattern.
|
178
|
+
url_pattern.view,
|
449
179
|
(
|
450
180
|
bits,
|
451
181
|
p_pattern,
|
452
|
-
url_pattern.default_args,
|
453
182
|
url_pattern.pattern.converters,
|
454
183
|
),
|
455
184
|
)
|
@@ -459,14 +188,13 @@ class URLResolver:
|
|
459
188
|
(
|
460
189
|
bits,
|
461
190
|
p_pattern,
|
462
|
-
url_pattern.default_args,
|
463
191
|
url_pattern.pattern.converters,
|
464
192
|
),
|
465
193
|
)
|
466
194
|
else: # url_pattern is a URLResolver.
|
467
195
|
url_pattern._populate()
|
468
|
-
if url_pattern.
|
469
|
-
packages.setdefault(url_pattern.
|
196
|
+
if url_pattern.namespace:
|
197
|
+
packages.setdefault(url_pattern.namespace, []).append(
|
470
198
|
url_pattern.namespace
|
471
199
|
)
|
472
200
|
namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
|
@@ -475,7 +203,6 @@ class URLResolver:
|
|
475
203
|
for (
|
476
204
|
matches,
|
477
205
|
pat,
|
478
|
-
defaults,
|
479
206
|
converters,
|
480
207
|
) in url_pattern.reverse_dict.getlist(name):
|
481
208
|
new_matches = normalize(p_pattern + pat)
|
@@ -484,7 +211,6 @@ class URLResolver:
|
|
484
211
|
(
|
485
212
|
new_matches,
|
486
213
|
p_pattern + pat,
|
487
|
-
{**defaults, **url_pattern.default_kwargs},
|
488
214
|
{
|
489
215
|
**self.pattern.converters,
|
490
216
|
**url_pattern.pattern.converters,
|
@@ -500,13 +226,10 @@ class URLResolver:
|
|
500
226
|
sub_pattern.pattern.converters.update(current_converters)
|
501
227
|
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
|
502
228
|
for (
|
503
|
-
|
229
|
+
namespace,
|
504
230
|
namespace_list,
|
505
231
|
) in url_pattern.app_dict.items():
|
506
|
-
packages.setdefault(
|
507
|
-
namespace_list
|
508
|
-
)
|
509
|
-
self._callback_strs.update(url_pattern._callback_strs)
|
232
|
+
packages.setdefault(namespace, []).extend(namespace_list)
|
510
233
|
self._namespace_dict = namespaces
|
511
234
|
self._app_dict = packages
|
512
235
|
self._reverse_dict = lookups
|
@@ -547,11 +270,6 @@ class URLResolver:
|
|
547
270
|
route2 = route2.removeprefix("^")
|
548
271
|
return route1 + route2
|
549
272
|
|
550
|
-
def _is_callback(self, name):
|
551
|
-
if not self._populated:
|
552
|
-
self._populate()
|
553
|
-
return name in self._callback_strs
|
554
|
-
|
555
273
|
def resolve(self, path):
|
556
274
|
path = str(path) # path may be a reverse_lazy object
|
557
275
|
tried = []
|
@@ -566,9 +284,8 @@ class URLResolver:
|
|
566
284
|
else:
|
567
285
|
if sub_match:
|
568
286
|
# Merge captured arguments in match with submatch
|
569
|
-
sub_match_dict = {**kwargs, **self.default_kwargs}
|
570
287
|
# Update the sub_match_dict with the kwargs from the sub_match.
|
571
|
-
sub_match_dict
|
288
|
+
sub_match_dict = {**kwargs, **sub_match.kwargs}
|
572
289
|
# If there are *any* named groups, ignore all non-named groups.
|
573
290
|
# Otherwise, pass all non-named arguments as positional
|
574
291
|
# arguments.
|
@@ -586,42 +303,24 @@ class URLResolver:
|
|
586
303
|
sub_match_args,
|
587
304
|
sub_match_dict,
|
588
305
|
sub_match.url_name,
|
589
|
-
[self.default_namespace] + sub_match.default_namespaces,
|
590
306
|
[self.namespace] + sub_match.namespaces,
|
591
307
|
self._join_route(current_route, sub_match.route),
|
592
308
|
tried,
|
593
309
|
captured_kwargs=sub_match.captured_kwargs,
|
594
|
-
extra_kwargs=
|
595
|
-
**self.default_kwargs,
|
596
|
-
**sub_match.extra_kwargs,
|
597
|
-
},
|
310
|
+
extra_kwargs=sub_match.extra_kwargs,
|
598
311
|
)
|
599
312
|
tried.append([pattern])
|
600
313
|
raise Resolver404({"tried": tried, "path": new_path})
|
601
314
|
raise Resolver404({"path": path})
|
602
315
|
|
603
316
|
@cached_property
|
604
|
-
def
|
605
|
-
|
606
|
-
|
607
|
-
else:
|
608
|
-
return self.urlconf_name
|
317
|
+
def url_patterns(self):
|
318
|
+
# Don't need to instantiate the class because they are just class attributes for now.
|
319
|
+
return self.router_class.urls
|
609
320
|
|
610
321
|
@cached_property
|
611
|
-
def
|
612
|
-
|
613
|
-
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
|
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
|
322
|
+
def namespace(self):
|
323
|
+
return self.router_class.namespace
|
625
324
|
|
626
325
|
def reverse(self, lookup_view, *args, **kwargs):
|
627
326
|
if args and kwargs:
|
@@ -632,23 +331,14 @@ class URLResolver:
|
|
632
331
|
|
633
332
|
possibilities = self.reverse_dict.getlist(lookup_view)
|
634
333
|
|
635
|
-
for possibility, pattern,
|
334
|
+
for possibility, pattern, converters in possibilities:
|
636
335
|
for result, params in possibility:
|
637
336
|
if args:
|
638
337
|
if len(args) != len(params):
|
639
338
|
continue
|
640
339
|
candidate_subs = dict(zip(params, args))
|
641
340
|
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:
|
341
|
+
if set(kwargs).symmetric_difference(params):
|
652
342
|
continue
|
653
343
|
candidate_subs = kwargs
|
654
344
|
# Convert the candidate subs to text using Converter.to_url().
|