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.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. 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
@@ -0,0 +1,3 @@
1
+ # urls
2
+
3
+ Route requests to views.
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)
@@ -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]
@@ -0,0 +1,9 @@
1
+ from plain.http import Http404
2
+
3
+
4
+ class Resolver404(Http404):
5
+ pass
6
+
7
+
8
+ class NoReverseMatch(Exception):
9
+ pass