plain 0.1.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/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/test/utils.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import warnings
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from unittest import TestCase
|
|
6
|
+
|
|
7
|
+
from plain.packages import packages
|
|
8
|
+
from plain.runtime import settings
|
|
9
|
+
from plain.runtime.user_settings import UserSettingsHolder
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"ContextList",
|
|
13
|
+
"ignore_warnings",
|
|
14
|
+
"modify_settings",
|
|
15
|
+
"override_settings",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextList(list):
|
|
20
|
+
"""
|
|
21
|
+
A wrapper that provides direct key access to context items contained
|
|
22
|
+
in a list of context objects.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, key):
|
|
26
|
+
if isinstance(key, str):
|
|
27
|
+
for subcontext in self:
|
|
28
|
+
if key in subcontext:
|
|
29
|
+
return subcontext[key]
|
|
30
|
+
raise KeyError(key)
|
|
31
|
+
else:
|
|
32
|
+
return super().__getitem__(key)
|
|
33
|
+
|
|
34
|
+
def get(self, key, default=None):
|
|
35
|
+
try:
|
|
36
|
+
return self.__getitem__(key)
|
|
37
|
+
except KeyError:
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
def __contains__(self, key):
|
|
41
|
+
try:
|
|
42
|
+
self[key]
|
|
43
|
+
except KeyError:
|
|
44
|
+
return False
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def keys(self):
|
|
48
|
+
"""
|
|
49
|
+
Flattened keys of subcontexts.
|
|
50
|
+
"""
|
|
51
|
+
return set(chain.from_iterable(d for subcontext in self for d in subcontext))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestContextDecorator:
|
|
55
|
+
"""
|
|
56
|
+
A base class that can either be used as a context manager during tests
|
|
57
|
+
or as a test function or unittest.TestCase subclass decorator to perform
|
|
58
|
+
temporary alterations.
|
|
59
|
+
|
|
60
|
+
`attr_name`: attribute assigned the return value of enable() if used as
|
|
61
|
+
a class decorator.
|
|
62
|
+
|
|
63
|
+
`kwarg_name`: keyword argument passing the return value of enable() if
|
|
64
|
+
used as a function decorator.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, attr_name=None, kwarg_name=None):
|
|
68
|
+
self.attr_name = attr_name
|
|
69
|
+
self.kwarg_name = kwarg_name
|
|
70
|
+
|
|
71
|
+
def enable(self):
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
def disable(self):
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
def __enter__(self):
|
|
78
|
+
return self.enable()
|
|
79
|
+
|
|
80
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
81
|
+
self.disable()
|
|
82
|
+
|
|
83
|
+
def decorate_class(self, cls):
|
|
84
|
+
if issubclass(cls, TestCase):
|
|
85
|
+
decorated_setUp = cls.setUp
|
|
86
|
+
|
|
87
|
+
def setUp(inner_self):
|
|
88
|
+
context = self.enable()
|
|
89
|
+
inner_self.addCleanup(self.disable)
|
|
90
|
+
if self.attr_name:
|
|
91
|
+
setattr(inner_self, self.attr_name, context)
|
|
92
|
+
decorated_setUp(inner_self)
|
|
93
|
+
|
|
94
|
+
cls.setUp = setUp
|
|
95
|
+
return cls
|
|
96
|
+
raise TypeError("Can only decorate subclasses of unittest.TestCase")
|
|
97
|
+
|
|
98
|
+
def decorate_callable(self, func):
|
|
99
|
+
@wraps(func)
|
|
100
|
+
def inner(*args, **kwargs):
|
|
101
|
+
with self as context:
|
|
102
|
+
if self.kwarg_name:
|
|
103
|
+
kwargs[self.kwarg_name] = context
|
|
104
|
+
return func(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
return inner
|
|
107
|
+
|
|
108
|
+
def __call__(self, decorated):
|
|
109
|
+
if isinstance(decorated, type):
|
|
110
|
+
return self.decorate_class(decorated)
|
|
111
|
+
elif callable(decorated):
|
|
112
|
+
return self.decorate_callable(decorated)
|
|
113
|
+
raise TypeError("Cannot decorate object of type %s" % type(decorated))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class override_settings(TestContextDecorator):
|
|
117
|
+
"""
|
|
118
|
+
Act as either a decorator or a context manager. If it's a decorator, take a
|
|
119
|
+
function and return a wrapped function. If it's a contextmanager, use it
|
|
120
|
+
with the ``with`` statement. In either event, entering/exiting are called
|
|
121
|
+
before and after, respectively, the function/block is executed.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
enable_exception = None
|
|
125
|
+
|
|
126
|
+
def __init__(self, **kwargs):
|
|
127
|
+
self.options = kwargs
|
|
128
|
+
super().__init__()
|
|
129
|
+
|
|
130
|
+
def enable(self):
|
|
131
|
+
# Keep this code at the beginning to leave the settings unchanged
|
|
132
|
+
# in case it raises an exception because INSTALLED_PACKAGES is invalid.
|
|
133
|
+
if "INSTALLED_PACKAGES" in self.options:
|
|
134
|
+
try:
|
|
135
|
+
packages.set_installed_packages(self.options["INSTALLED_PACKAGES"])
|
|
136
|
+
except Exception:
|
|
137
|
+
packages.unset_installed_packages()
|
|
138
|
+
raise
|
|
139
|
+
override = UserSettingsHolder(settings._wrapped)
|
|
140
|
+
for key, new_value in self.options.items():
|
|
141
|
+
setattr(override, key, new_value)
|
|
142
|
+
self.wrapped = settings._wrapped
|
|
143
|
+
settings._wrapped = override
|
|
144
|
+
# for key, new_value in self.options.items():
|
|
145
|
+
# try:
|
|
146
|
+
# setting_changed.send(
|
|
147
|
+
# sender=settings._wrapped.__class__,
|
|
148
|
+
# setting=key,
|
|
149
|
+
# value=new_value,
|
|
150
|
+
# enter=True,
|
|
151
|
+
# )
|
|
152
|
+
# except Exception as exc:
|
|
153
|
+
# self.enable_exception = exc
|
|
154
|
+
# self.disable()
|
|
155
|
+
|
|
156
|
+
def disable(self):
|
|
157
|
+
if "INSTALLED_PACKAGES" in self.options:
|
|
158
|
+
packages.unset_installed_packages()
|
|
159
|
+
settings._wrapped = self.wrapped
|
|
160
|
+
del self.wrapped
|
|
161
|
+
responses = []
|
|
162
|
+
for key in self.options:
|
|
163
|
+
getattr(settings, key, None)
|
|
164
|
+
# responses_for_setting = setting_changed.send_robust(
|
|
165
|
+
# sender=settings._wrapped.__class__,
|
|
166
|
+
# setting=key,
|
|
167
|
+
# value=new_value,
|
|
168
|
+
# enter=False,
|
|
169
|
+
# )
|
|
170
|
+
# responses.extend(responses_for_setting)
|
|
171
|
+
if self.enable_exception is not None:
|
|
172
|
+
exc = self.enable_exception
|
|
173
|
+
self.enable_exception = None
|
|
174
|
+
raise exc
|
|
175
|
+
for _, response in responses:
|
|
176
|
+
if isinstance(response, Exception):
|
|
177
|
+
raise response
|
|
178
|
+
|
|
179
|
+
def save_options(self, test_func):
|
|
180
|
+
if test_func._overridden_settings is None:
|
|
181
|
+
test_func._overridden_settings = self.options
|
|
182
|
+
else:
|
|
183
|
+
# Duplicate dict to prevent subclasses from altering their parent.
|
|
184
|
+
test_func._overridden_settings = {
|
|
185
|
+
**test_func._overridden_settings,
|
|
186
|
+
**self.options,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class modify_settings(override_settings):
|
|
191
|
+
"""
|
|
192
|
+
Like override_settings, but makes it possible to append, prepend, or remove
|
|
193
|
+
items instead of redefining the entire list.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(self, *args, **kwargs):
|
|
197
|
+
if args:
|
|
198
|
+
# Hack used when instantiating from SimpleTestCase.setUpClass.
|
|
199
|
+
assert not kwargs
|
|
200
|
+
self.operations = args[0]
|
|
201
|
+
else:
|
|
202
|
+
assert not args
|
|
203
|
+
self.operations = list(kwargs.items())
|
|
204
|
+
super(override_settings, self).__init__()
|
|
205
|
+
|
|
206
|
+
def save_options(self, test_func):
|
|
207
|
+
if test_func._modified_settings is None:
|
|
208
|
+
test_func._modified_settings = self.operations
|
|
209
|
+
else:
|
|
210
|
+
# Duplicate list to prevent subclasses from altering their parent.
|
|
211
|
+
test_func._modified_settings = (
|
|
212
|
+
list(test_func._modified_settings) + self.operations
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def enable(self):
|
|
216
|
+
self.options = {}
|
|
217
|
+
for name, operations in self.operations:
|
|
218
|
+
try:
|
|
219
|
+
# When called from SimpleTestCase.setUpClass, values may be
|
|
220
|
+
# overridden several times; cumulate changes.
|
|
221
|
+
value = self.options[name]
|
|
222
|
+
except KeyError:
|
|
223
|
+
value = list(getattr(settings, name, []))
|
|
224
|
+
for action, items in operations.items():
|
|
225
|
+
# items my be a single value or an iterable.
|
|
226
|
+
if isinstance(items, str):
|
|
227
|
+
items = [items]
|
|
228
|
+
if action == "append":
|
|
229
|
+
value += [item for item in items if item not in value]
|
|
230
|
+
elif action == "prepend":
|
|
231
|
+
value = [item for item in items if item not in value] + value
|
|
232
|
+
elif action == "remove":
|
|
233
|
+
value = [item for item in value if item not in items]
|
|
234
|
+
else:
|
|
235
|
+
raise ValueError("Unsupported action: %s" % action)
|
|
236
|
+
self.options[name] = value
|
|
237
|
+
super().enable()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ignore_warnings(TestContextDecorator):
|
|
241
|
+
def __init__(self, **kwargs):
|
|
242
|
+
self.ignore_kwargs = kwargs
|
|
243
|
+
if "message" in self.ignore_kwargs or "module" in self.ignore_kwargs:
|
|
244
|
+
self.filter_func = warnings.filterwarnings
|
|
245
|
+
else:
|
|
246
|
+
self.filter_func = warnings.simplefilter
|
|
247
|
+
super().__init__()
|
|
248
|
+
|
|
249
|
+
def enable(self):
|
|
250
|
+
self.catch_warnings = warnings.catch_warnings()
|
|
251
|
+
self.catch_warnings.__enter__()
|
|
252
|
+
self.filter_func("ignore", **self.ignore_kwargs)
|
|
253
|
+
|
|
254
|
+
def disable(self):
|
|
255
|
+
self.catch_warnings.__exit__(*sys.exc_info())
|
plain/urls/README.md
ADDED
plain/urls/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .base import (
|
|
2
|
+
clear_url_caches,
|
|
3
|
+
get_urlconf,
|
|
4
|
+
is_valid_path,
|
|
5
|
+
resolve,
|
|
6
|
+
reverse,
|
|
7
|
+
reverse_lazy,
|
|
8
|
+
set_urlconf,
|
|
9
|
+
)
|
|
10
|
+
from .conf import include, path, re_path
|
|
11
|
+
from .converters import register_converter
|
|
12
|
+
from .exceptions import NoReverseMatch, Resolver404
|
|
13
|
+
from .resolvers import (
|
|
14
|
+
ResolverMatch,
|
|
15
|
+
URLPattern,
|
|
16
|
+
URLResolver,
|
|
17
|
+
get_ns_resolver,
|
|
18
|
+
get_resolver,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"NoReverseMatch",
|
|
23
|
+
"URLPattern",
|
|
24
|
+
"URLResolver",
|
|
25
|
+
"Resolver404",
|
|
26
|
+
"ResolverMatch",
|
|
27
|
+
"clear_url_caches",
|
|
28
|
+
"get_ns_resolver",
|
|
29
|
+
"get_resolver",
|
|
30
|
+
"get_urlconf",
|
|
31
|
+
"include",
|
|
32
|
+
"is_valid_path",
|
|
33
|
+
"path",
|
|
34
|
+
"re_path",
|
|
35
|
+
"register_converter",
|
|
36
|
+
"resolve",
|
|
37
|
+
"reverse",
|
|
38
|
+
"reverse_lazy",
|
|
39
|
+
"set_urlconf",
|
|
40
|
+
]
|
plain/urls/base.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from threading import local
|
|
2
|
+
|
|
3
|
+
from plain.utils.functional import lazy
|
|
4
|
+
|
|
5
|
+
from .exceptions import NoReverseMatch, Resolver404
|
|
6
|
+
from .resolvers import _get_cached_resolver, get_ns_resolver, get_resolver
|
|
7
|
+
|
|
8
|
+
# Overridden URLconfs for each thread are stored here.
|
|
9
|
+
_urlconfs = local()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve(path, urlconf=None):
|
|
13
|
+
if urlconf is None:
|
|
14
|
+
urlconf = get_urlconf()
|
|
15
|
+
return get_resolver(urlconf).resolve(path)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def reverse(viewname, urlconf=None, args=None, kwargs=None, using_namespace=None):
|
|
19
|
+
if urlconf is None:
|
|
20
|
+
urlconf = get_urlconf()
|
|
21
|
+
resolver = get_resolver(urlconf)
|
|
22
|
+
args = args or []
|
|
23
|
+
kwargs = kwargs or {}
|
|
24
|
+
|
|
25
|
+
if not isinstance(viewname, str):
|
|
26
|
+
view = viewname
|
|
27
|
+
else:
|
|
28
|
+
*path, view = viewname.split(":")
|
|
29
|
+
|
|
30
|
+
if using_namespace:
|
|
31
|
+
current_path = using_namespace.split(":")
|
|
32
|
+
current_path.reverse()
|
|
33
|
+
else:
|
|
34
|
+
current_path = None
|
|
35
|
+
|
|
36
|
+
resolved_path = []
|
|
37
|
+
ns_pattern = ""
|
|
38
|
+
ns_converters = {}
|
|
39
|
+
for ns in path:
|
|
40
|
+
current_ns = current_path.pop() if current_path else None
|
|
41
|
+
# Lookup the name to see if it could be an app identifier.
|
|
42
|
+
try:
|
|
43
|
+
app_list = resolver.app_dict[ns]
|
|
44
|
+
# Yes! Path part matches an app in the current Resolver.
|
|
45
|
+
if current_ns and current_ns in app_list:
|
|
46
|
+
# If we are reversing for a particular app, use that
|
|
47
|
+
# namespace.
|
|
48
|
+
ns = current_ns
|
|
49
|
+
elif ns not in app_list:
|
|
50
|
+
# The name isn't shared by one of the instances (i.e.,
|
|
51
|
+
# the default) so pick the first instance as the default.
|
|
52
|
+
ns = app_list[0]
|
|
53
|
+
except KeyError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
if ns != current_ns:
|
|
57
|
+
current_path = None
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
extra, resolver = resolver.namespace_dict[ns]
|
|
61
|
+
resolved_path.append(ns)
|
|
62
|
+
ns_pattern += extra
|
|
63
|
+
ns_converters.update(resolver.pattern.converters)
|
|
64
|
+
except KeyError as key:
|
|
65
|
+
if resolved_path:
|
|
66
|
+
raise NoReverseMatch(
|
|
67
|
+
"{} is not a registered namespace inside '{}'".format(
|
|
68
|
+
key, ":".join(resolved_path)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
raise NoReverseMatch("%s is not a registered namespace" % key)
|
|
73
|
+
if ns_pattern:
|
|
74
|
+
resolver = get_ns_resolver(
|
|
75
|
+
ns_pattern, resolver, tuple(ns_converters.items())
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return resolver.reverse(view, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
reverse_lazy = lazy(reverse, str)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def clear_url_caches():
|
|
85
|
+
_get_cached_resolver.cache_clear()
|
|
86
|
+
get_ns_resolver.cache_clear()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def set_urlconf(urlconf_name):
|
|
90
|
+
"""
|
|
91
|
+
Set the URLconf for the current thread (overriding the default one in
|
|
92
|
+
settings). If urlconf_name is None, revert back to the default.
|
|
93
|
+
"""
|
|
94
|
+
if urlconf_name:
|
|
95
|
+
_urlconfs.value = urlconf_name
|
|
96
|
+
else:
|
|
97
|
+
if hasattr(_urlconfs, "value"):
|
|
98
|
+
del _urlconfs.value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_urlconf(default=None):
|
|
102
|
+
"""
|
|
103
|
+
Return the root URLconf to use for the current thread if it has been
|
|
104
|
+
changed from the default one.
|
|
105
|
+
"""
|
|
106
|
+
return getattr(_urlconfs, "value", default)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_valid_path(path, urlconf=None):
|
|
110
|
+
"""
|
|
111
|
+
Return the ResolverMatch if the given path resolves against the default URL
|
|
112
|
+
resolver, False otherwise. This is a convenience method to make working
|
|
113
|
+
with "is this a match?" cases easier, avoiding try...except blocks.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
return resolve(path, urlconf)
|
|
117
|
+
except Resolver404:
|
|
118
|
+
return False
|
plain/urls/conf.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Functions for use in URLsconfs."""
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
from plain.exceptions import ImproperlyConfigured
|
|
5
|
+
|
|
6
|
+
from .resolvers import (
|
|
7
|
+
RegexPattern,
|
|
8
|
+
RoutePattern,
|
|
9
|
+
URLPattern,
|
|
10
|
+
URLResolver,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def include(arg, namespace=None):
|
|
15
|
+
default_namespace = None
|
|
16
|
+
if isinstance(arg, tuple):
|
|
17
|
+
# Callable returning a namespace hint.
|
|
18
|
+
try:
|
|
19
|
+
urlconf_module, default_namespace = arg
|
|
20
|
+
except ValueError:
|
|
21
|
+
if namespace:
|
|
22
|
+
raise ImproperlyConfigured(
|
|
23
|
+
"Cannot override the namespace for a dynamic module that "
|
|
24
|
+
"provides a namespace."
|
|
25
|
+
)
|
|
26
|
+
raise ImproperlyConfigured(
|
|
27
|
+
"Passing a %d-tuple to include() is not supported. Pass a "
|
|
28
|
+
"2-tuple containing the list of patterns and default_namespace, and "
|
|
29
|
+
"provide the namespace argument to include() instead." % len(arg)
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
# No namespace hint - use manually provided namespace.
|
|
33
|
+
urlconf_module = arg
|
|
34
|
+
|
|
35
|
+
patterns = getattr(urlconf_module, "urlpatterns", urlconf_module)
|
|
36
|
+
default_namespace = getattr(urlconf_module, "default_namespace", default_namespace)
|
|
37
|
+
if namespace and not default_namespace:
|
|
38
|
+
raise ImproperlyConfigured(
|
|
39
|
+
"Specifying a namespace in include() without providing an default_namespace "
|
|
40
|
+
"is not supported. Set the default_namespace attribute in the included "
|
|
41
|
+
"module, or pass a 2-tuple containing the list of patterns and "
|
|
42
|
+
"default_namespace instead.",
|
|
43
|
+
)
|
|
44
|
+
namespace = namespace or default_namespace
|
|
45
|
+
# Make sure the patterns can be iterated through (without this, some
|
|
46
|
+
# testcases will break).
|
|
47
|
+
if isinstance(patterns, list | tuple):
|
|
48
|
+
for url_pattern in patterns:
|
|
49
|
+
getattr(url_pattern, "pattern", None)
|
|
50
|
+
return (urlconf_module, default_namespace, namespace)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _path(route, view, kwargs=None, name=None, Pattern=None):
|
|
54
|
+
from plain.views import View
|
|
55
|
+
|
|
56
|
+
if kwargs is not None and not isinstance(kwargs, dict):
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f"kwargs argument must be a dict, but got {kwargs.__class__.__name__}."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if isinstance(view, list | tuple):
|
|
62
|
+
# For include(...) processing.
|
|
63
|
+
pattern = Pattern(route, is_endpoint=False)
|
|
64
|
+
urlconf_module, default_namespace, namespace = view
|
|
65
|
+
return URLResolver(
|
|
66
|
+
pattern,
|
|
67
|
+
urlconf_module,
|
|
68
|
+
kwargs,
|
|
69
|
+
default_namespace=default_namespace,
|
|
70
|
+
namespace=namespace,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if isinstance(view, View):
|
|
74
|
+
view_cls_name = view.__class__.__name__
|
|
75
|
+
raise TypeError(
|
|
76
|
+
f"view must be a callable, pass {view_cls_name} or {view_cls_name}.as_view(*args, **kwargs), not "
|
|
77
|
+
f"{view_cls_name}()."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Automatically call view.as_view() for class-based views
|
|
81
|
+
if as_view := getattr(view, "as_view", None):
|
|
82
|
+
pattern = Pattern(route, name=name, is_endpoint=True)
|
|
83
|
+
return URLPattern(pattern, as_view(), kwargs, name)
|
|
84
|
+
|
|
85
|
+
# Function-based views or view_class.as_view() usage
|
|
86
|
+
if callable(view):
|
|
87
|
+
pattern = Pattern(route, name=name, is_endpoint=True)
|
|
88
|
+
return URLPattern(pattern, view, kwargs, name)
|
|
89
|
+
|
|
90
|
+
raise TypeError("view must be a callable or a list/tuple in the case of include().")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
path = partial(_path, Pattern=RoutePattern)
|
|
94
|
+
re_path = partial(_path, Pattern=RegexPattern)
|
plain/urls/converters.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class IntConverter:
|
|
6
|
+
regex = "[0-9]+"
|
|
7
|
+
|
|
8
|
+
def to_python(self, value):
|
|
9
|
+
return int(value)
|
|
10
|
+
|
|
11
|
+
def to_url(self, value):
|
|
12
|
+
return str(value)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StringConverter:
|
|
16
|
+
regex = "[^/]+"
|
|
17
|
+
|
|
18
|
+
def to_python(self, value):
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
def to_url(self, value):
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UUIDConverter:
|
|
26
|
+
regex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
27
|
+
|
|
28
|
+
def to_python(self, value):
|
|
29
|
+
return uuid.UUID(value)
|
|
30
|
+
|
|
31
|
+
def to_url(self, value):
|
|
32
|
+
return str(value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SlugConverter(StringConverter):
|
|
36
|
+
regex = "[-a-zA-Z0-9_]+"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PathConverter(StringConverter):
|
|
40
|
+
regex = ".+"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
DEFAULT_CONVERTERS = {
|
|
44
|
+
"int": IntConverter(),
|
|
45
|
+
"path": PathConverter(),
|
|
46
|
+
"slug": SlugConverter(),
|
|
47
|
+
"str": StringConverter(),
|
|
48
|
+
"uuid": UUIDConverter(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
REGISTERED_CONVERTERS = {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def register_converter(converter, type_name):
|
|
56
|
+
REGISTERED_CONVERTERS[type_name] = converter()
|
|
57
|
+
get_converters.cache_clear()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@functools.cache
|
|
61
|
+
def get_converters():
|
|
62
|
+
return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_converter(raw_converter):
|
|
66
|
+
return get_converters()[raw_converter]
|