plain 0.24.1__py3-none-any.whl → 0.26.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/assets/README.md +5 -5
- plain/cli/README.md +2 -4
- plain/cli/cli.py +6 -6
- plain/csrf/middleware.py +0 -1
- plain/exceptions.py +1 -21
- plain/forms/fields.py +1 -1
- plain/forms/forms.py +0 -1
- plain/http/multipartparser.py +0 -2
- plain/http/request.py +18 -41
- plain/internal/files/base.py +1 -29
- plain/internal/handlers/wsgi.py +18 -1
- plain/packages/__init__.py +2 -2
- plain/packages/config.py +48 -160
- plain/packages/registry.py +64 -10
- plain/paginator.py +0 -4
- plain/preflight/urls.py +0 -7
- plain/templates/jinja/__init__.py +18 -13
- plain/urls/resolvers.py +1 -1
- plain/utils/cache.py +0 -202
- plain/utils/encoding.py +0 -105
- plain/utils/functional.py +0 -7
- plain/utils/html.py +1 -276
- plain/utils/http.py +2 -189
- plain/utils/inspect.py +0 -35
- plain/utils/module_loading.py +0 -19
- plain/utils/safestring.py +0 -3
- plain/utils/text.py +0 -253
- plain/validators.py +0 -11
- {plain-0.24.1.dist-info → plain-0.26.0.dist-info}/METADATA +1 -1
- {plain-0.24.1.dist-info → plain-0.26.0.dist-info}/RECORD +33 -37
- plain/utils/_os.py +0 -52
- plain/utils/dateformat.py +0 -330
- plain/utils/dates.py +0 -76
- plain/utils/email.py +0 -12
- {plain-0.24.1.dist-info → plain-0.26.0.dist-info}/WHEEL +0 -0
- {plain-0.24.1.dist-info → plain-0.26.0.dist-info}/entry_points.txt +0 -0
- {plain-0.24.1.dist-info → plain-0.26.0.dist-info}/licenses/LICENSE +0 -0
plain/packages/registry.py
CHANGED
@@ -4,11 +4,14 @@ import threading
|
|
4
4
|
import warnings
|
5
5
|
from collections import Counter, defaultdict
|
6
6
|
from functools import partial
|
7
|
+
from importlib import import_module
|
7
8
|
|
8
9
|
from plain.exceptions import ImproperlyConfigured, PackageRegistryNotReady
|
9
10
|
|
10
11
|
from .config import PackageConfig
|
11
12
|
|
13
|
+
CONFIG_MODULE_NAME = "config"
|
14
|
+
|
12
15
|
|
13
16
|
class PackagesRegistry:
|
14
17
|
"""
|
@@ -82,17 +85,35 @@ class PackagesRegistry:
|
|
82
85
|
# Phase 1: initialize app configs and import app modules.
|
83
86
|
for entry in installed_packages:
|
84
87
|
if isinstance(entry, PackageConfig):
|
85
|
-
|
88
|
+
# Some instances of the registry pass in the
|
89
|
+
# PackageConfig directly...
|
90
|
+
self.register_config(entry)
|
86
91
|
else:
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
92
|
+
try:
|
93
|
+
import_module(f"{entry}.{CONFIG_MODULE_NAME}")
|
94
|
+
except ModuleNotFoundError:
|
95
|
+
pass
|
96
|
+
|
97
|
+
# The config for the package should now be registered, if it existed.
|
98
|
+
# And if it didn't, now we can auto generate one.
|
99
|
+
entry_config = None
|
100
|
+
for config in self.package_configs.values():
|
101
|
+
if config.name == entry:
|
102
|
+
entry_config = config
|
103
|
+
break
|
104
|
+
|
105
|
+
if not entry_config:
|
106
|
+
# Use PackageConfig class as-is, without any customization.
|
107
|
+
entry_config = self.register_config(
|
108
|
+
PackageConfig, module_name=entry
|
109
|
+
)
|
110
|
+
|
111
|
+
# Make sure we have the same number of configs as we have installed packages
|
112
|
+
if len(self.package_configs) != len(installed_packages):
|
113
|
+
raise ImproperlyConfigured(
|
114
|
+
f"The number of installed packages ({len(installed_packages)}) does not match the number of "
|
115
|
+
f"registered configs ({len(self.package_configs)})."
|
116
|
+
)
|
96
117
|
|
97
118
|
# Check for duplicate app names.
|
98
119
|
counts = Counter(
|
@@ -372,5 +393,38 @@ class PackagesRegistry:
|
|
372
393
|
for function in self._pending_operations.pop(key, []):
|
373
394
|
function(model)
|
374
395
|
|
396
|
+
def register_config(self, package_config, module_name=""):
|
397
|
+
"""
|
398
|
+
Add a config to the registry.
|
399
|
+
|
400
|
+
Typically used as a decorator on a PackageConfig subclass. Example:
|
401
|
+
|
402
|
+
@register_config
|
403
|
+
class Config(PackageConfig):
|
404
|
+
pass
|
405
|
+
"""
|
406
|
+
if not module_name:
|
407
|
+
module_name = package_config.__module__
|
408
|
+
|
409
|
+
# If it is in .config like expected, return the parent module name
|
410
|
+
if module_name.endswith(f".{CONFIG_MODULE_NAME}"):
|
411
|
+
module_name = module_name[: -len(CONFIG_MODULE_NAME) - 1]
|
412
|
+
|
413
|
+
if isinstance(package_config, type) and issubclass(
|
414
|
+
package_config, PackageConfig
|
415
|
+
):
|
416
|
+
# A class was passed, so init it
|
417
|
+
package_config = package_config(module_name)
|
418
|
+
|
419
|
+
if package_config.label in self.package_configs:
|
420
|
+
raise ImproperlyConfigured(
|
421
|
+
f"Package labels aren't unique, duplicates: {package_config.label}"
|
422
|
+
)
|
423
|
+
self.package_configs[package_config.label] = package_config
|
424
|
+
package_config.packages = self
|
425
|
+
|
426
|
+
return package_config
|
427
|
+
|
375
428
|
|
376
429
|
packages_registry = PackagesRegistry(installed_packages=None)
|
430
|
+
register_config = packages_registry.register_config
|
plain/paginator.py
CHANGED
@@ -24,10 +24,6 @@ class EmptyPage(InvalidPage):
|
|
24
24
|
|
25
25
|
|
26
26
|
class Paginator:
|
27
|
-
# Translators: String used to replace omitted page numbers in elided page
|
28
|
-
# range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10].
|
29
|
-
ELLIPSIS = "…"
|
30
|
-
|
31
27
|
def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
|
32
28
|
self.object_list = object_list
|
33
29
|
self._check_object_list_is_ordered()
|
plain/preflight/urls.py
CHANGED
@@ -3,7 +3,7 @@ from importlib import import_module
|
|
3
3
|
from plain.packages import packages_registry
|
4
4
|
from plain.runtime import settings
|
5
5
|
from plain.utils.functional import LazyObject
|
6
|
-
from plain.utils.module_loading import import_string
|
6
|
+
from plain.utils.module_loading import import_string
|
7
7
|
|
8
8
|
from .environments import DefaultEnvironment, get_template_dirs
|
9
9
|
|
@@ -24,20 +24,25 @@ class JinjaEnvironment(LazyObject):
|
|
24
24
|
# We have to set _wrapped before we trigger the autoloading of "register" commands
|
25
25
|
self._wrapped = env
|
26
26
|
|
27
|
-
def _maybe_import_module(name):
|
28
|
-
if name not in self._imported_modules:
|
29
|
-
import_module(name)
|
30
|
-
self._imported_modules.add(name)
|
31
|
-
|
32
27
|
for package_config in packages_registry.get_package_configs():
|
33
|
-
if module_has_submodule(package_config.module, "templates"):
|
34
|
-
# Allow this to fail in case there are import errors inside of their file
|
35
|
-
_maybe_import_module(f"{package_config.name}.templates")
|
36
|
-
|
37
|
-
app = import_module("app")
|
38
|
-
if module_has_submodule(app, "templates"):
|
39
28
|
# Allow this to fail in case there are import errors inside of their file
|
40
|
-
|
29
|
+
import_name = f"{package_config.name}.templates"
|
30
|
+
if import_name in self._imported_modules:
|
31
|
+
continue
|
32
|
+
try:
|
33
|
+
import_module(import_name)
|
34
|
+
self._imported_modules.add(import_name)
|
35
|
+
except ModuleNotFoundError:
|
36
|
+
pass
|
37
|
+
|
38
|
+
# Allow this to fail in case there are import errors inside of their file
|
39
|
+
import_name = "app.templates"
|
40
|
+
if import_name not in self._imported_modules:
|
41
|
+
try:
|
42
|
+
import_module(import_name)
|
43
|
+
self._imported_modules.add(import_name)
|
44
|
+
except ModuleNotFoundError:
|
45
|
+
pass
|
41
46
|
|
42
47
|
|
43
48
|
environment = JinjaEnvironment()
|
plain/urls/resolvers.py
CHANGED
plain/utils/cache.py
CHANGED
@@ -15,14 +15,8 @@ An example: i18n middleware would need to distinguish caches by the
|
|
15
15
|
"Accept-language" header.
|
16
16
|
"""
|
17
17
|
|
18
|
-
import time
|
19
18
|
from collections import defaultdict
|
20
|
-
from hashlib import md5
|
21
19
|
|
22
|
-
from plain.http import Response, ResponseNotModified
|
23
|
-
from plain.logs import log_response
|
24
|
-
from plain.runtime import settings
|
25
|
-
from plain.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag
|
26
20
|
from plain.utils.regex_helper import _lazy_re_compile
|
27
21
|
|
28
22
|
cc_delim_re = _lazy_re_compile(r"\s*,\s*")
|
@@ -97,202 +91,6 @@ def patch_cache_control(response, **kwargs):
|
|
97
91
|
response.headers["Cache-Control"] = cc
|
98
92
|
|
99
93
|
|
100
|
-
def get_max_age(response):
|
101
|
-
"""
|
102
|
-
Return the max-age from the response Cache-Control header as an integer,
|
103
|
-
or None if it wasn't found or wasn't an integer.
|
104
|
-
"""
|
105
|
-
if "Cache-Control" not in response.headers:
|
106
|
-
return
|
107
|
-
cc = dict(
|
108
|
-
_to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
|
109
|
-
)
|
110
|
-
try:
|
111
|
-
return int(cc["max-age"])
|
112
|
-
except (ValueError, TypeError, KeyError):
|
113
|
-
pass
|
114
|
-
|
115
|
-
|
116
|
-
def set_response_etag(response):
|
117
|
-
if not response.streaming and response.content:
|
118
|
-
response.headers["ETag"] = quote_etag(
|
119
|
-
md5(response.content, usedforsecurity=False).hexdigest(),
|
120
|
-
)
|
121
|
-
return response
|
122
|
-
|
123
|
-
|
124
|
-
def _precondition_failed(request):
|
125
|
-
response = Response(status=412)
|
126
|
-
log_response(
|
127
|
-
"Precondition Failed: %s",
|
128
|
-
request.path,
|
129
|
-
response=response,
|
130
|
-
request=request,
|
131
|
-
)
|
132
|
-
return response
|
133
|
-
|
134
|
-
|
135
|
-
def _not_modified(request, response=None):
|
136
|
-
new_response = ResponseNotModified()
|
137
|
-
if response:
|
138
|
-
# Preserve the headers required by RFC 9110 Section 15.4.5, as well as
|
139
|
-
# Last-Modified.
|
140
|
-
for header in (
|
141
|
-
"Cache-Control",
|
142
|
-
"Content-Location",
|
143
|
-
"Date",
|
144
|
-
"ETag",
|
145
|
-
"Expires",
|
146
|
-
"Last-Modified",
|
147
|
-
"Vary",
|
148
|
-
):
|
149
|
-
if header in response.headers:
|
150
|
-
new_response.headers[header] = response.headers[header]
|
151
|
-
|
152
|
-
# Preserve cookies as per the cookie specification: "If a proxy server
|
153
|
-
# receives a response which contains a Set-cookie header, it should
|
154
|
-
# propagate the Set-cookie header to the client, regardless of whether
|
155
|
-
# the response was 304 (Not Modified) or 200 (OK).
|
156
|
-
# https://curl.haxx.se/rfc/cookie_spec.html
|
157
|
-
new_response.cookies = response.cookies
|
158
|
-
return new_response
|
159
|
-
|
160
|
-
|
161
|
-
def get_conditional_response(request, etag=None, last_modified=None, response=None):
|
162
|
-
# Only return conditional responses on successful requests.
|
163
|
-
if response and not (200 <= response.status_code < 300):
|
164
|
-
return response
|
165
|
-
|
166
|
-
# Get HTTP request headers.
|
167
|
-
if_match_etags = parse_etags(request.META.get("HTTP_IF_MATCH", ""))
|
168
|
-
if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
|
169
|
-
if_unmodified_since = if_unmodified_since and parse_http_date_safe(
|
170
|
-
if_unmodified_since
|
171
|
-
)
|
172
|
-
if_none_match_etags = parse_etags(request.META.get("HTTP_IF_NONE_MATCH", ""))
|
173
|
-
if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
|
174
|
-
if_modified_since = if_modified_since and parse_http_date_safe(if_modified_since)
|
175
|
-
|
176
|
-
# Evaluation of request preconditions below follows RFC 9110 Section
|
177
|
-
# 13.2.2.
|
178
|
-
# Step 1: Test the If-Match precondition.
|
179
|
-
if if_match_etags and not _if_match_passes(etag, if_match_etags):
|
180
|
-
return _precondition_failed(request)
|
181
|
-
|
182
|
-
# Step 2: Test the If-Unmodified-Since precondition.
|
183
|
-
if (
|
184
|
-
not if_match_etags
|
185
|
-
and if_unmodified_since
|
186
|
-
and not _if_unmodified_since_passes(last_modified, if_unmodified_since)
|
187
|
-
):
|
188
|
-
return _precondition_failed(request)
|
189
|
-
|
190
|
-
# Step 3: Test the If-None-Match precondition.
|
191
|
-
if if_none_match_etags and not _if_none_match_passes(etag, if_none_match_etags):
|
192
|
-
if request.method in ("GET", "HEAD"):
|
193
|
-
return _not_modified(request, response)
|
194
|
-
else:
|
195
|
-
return _precondition_failed(request)
|
196
|
-
|
197
|
-
# Step 4: Test the If-Modified-Since precondition.
|
198
|
-
if (
|
199
|
-
not if_none_match_etags
|
200
|
-
and if_modified_since
|
201
|
-
and not _if_modified_since_passes(last_modified, if_modified_since)
|
202
|
-
and request.method in ("GET", "HEAD")
|
203
|
-
):
|
204
|
-
return _not_modified(request, response)
|
205
|
-
|
206
|
-
# Step 5: Test the If-Range precondition (not supported).
|
207
|
-
# Step 6: Return original response since there isn't a conditional response.
|
208
|
-
return response
|
209
|
-
|
210
|
-
|
211
|
-
def _if_match_passes(target_etag, etags):
|
212
|
-
"""
|
213
|
-
Test the If-Match comparison as defined in RFC 9110 Section 13.1.1.
|
214
|
-
"""
|
215
|
-
if not target_etag:
|
216
|
-
# If there isn't an ETag, then there can't be a match.
|
217
|
-
return False
|
218
|
-
elif etags == ["*"]:
|
219
|
-
# The existence of an ETag means that there is "a current
|
220
|
-
# representation for the target resource", even if the ETag is weak,
|
221
|
-
# so there is a match to '*'.
|
222
|
-
return True
|
223
|
-
elif target_etag.startswith("W/"):
|
224
|
-
# A weak ETag can never strongly match another ETag.
|
225
|
-
return False
|
226
|
-
else:
|
227
|
-
# Since the ETag is strong, this will only return True if there's a
|
228
|
-
# strong match.
|
229
|
-
return target_etag in etags
|
230
|
-
|
231
|
-
|
232
|
-
def _if_unmodified_since_passes(last_modified, if_unmodified_since):
|
233
|
-
"""
|
234
|
-
Test the If-Unmodified-Since comparison as defined in RFC 9110 Section
|
235
|
-
13.1.4.
|
236
|
-
"""
|
237
|
-
return last_modified and last_modified <= if_unmodified_since
|
238
|
-
|
239
|
-
|
240
|
-
def _if_none_match_passes(target_etag, etags):
|
241
|
-
"""
|
242
|
-
Test the If-None-Match comparison as defined in RFC 9110 Section 13.1.2.
|
243
|
-
"""
|
244
|
-
if not target_etag:
|
245
|
-
# If there isn't an ETag, then there isn't a match.
|
246
|
-
return True
|
247
|
-
elif etags == ["*"]:
|
248
|
-
# The existence of an ETag means that there is "a current
|
249
|
-
# representation for the target resource", so there is a match to '*'.
|
250
|
-
return False
|
251
|
-
else:
|
252
|
-
# The comparison should be weak, so look for a match after stripping
|
253
|
-
# off any weak indicators.
|
254
|
-
target_etag = target_etag.strip("W/")
|
255
|
-
etags = (etag.strip("W/") for etag in etags)
|
256
|
-
return target_etag not in etags
|
257
|
-
|
258
|
-
|
259
|
-
def _if_modified_since_passes(last_modified, if_modified_since):
|
260
|
-
"""
|
261
|
-
Test the If-Modified-Since comparison as defined in RFC 9110 Section
|
262
|
-
13.1.3.
|
263
|
-
"""
|
264
|
-
return not last_modified or last_modified > if_modified_since
|
265
|
-
|
266
|
-
|
267
|
-
def patch_response_headers(response, cache_timeout=None):
|
268
|
-
"""
|
269
|
-
Add HTTP caching headers to the given Response: Expires and
|
270
|
-
Cache-Control.
|
271
|
-
|
272
|
-
Each header is only added if it isn't already set.
|
273
|
-
|
274
|
-
cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
|
275
|
-
by default.
|
276
|
-
"""
|
277
|
-
if cache_timeout is None:
|
278
|
-
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
|
279
|
-
if cache_timeout < 0:
|
280
|
-
cache_timeout = 0 # Can't have max-age negative
|
281
|
-
if "Expires" not in response.headers:
|
282
|
-
response.headers["Expires"] = http_date(time.time() + cache_timeout)
|
283
|
-
patch_cache_control(response, max_age=cache_timeout)
|
284
|
-
|
285
|
-
|
286
|
-
def add_never_cache_headers(response):
|
287
|
-
"""
|
288
|
-
Add headers to a response to indicate that a page should never be cached.
|
289
|
-
"""
|
290
|
-
patch_response_headers(response, cache_timeout=-1)
|
291
|
-
patch_cache_control(
|
292
|
-
response, no_cache=True, no_store=True, must_revalidate=True, private=True
|
293
|
-
)
|
294
|
-
|
295
|
-
|
296
94
|
def patch_vary_headers(response, newheaders):
|
297
95
|
"""
|
298
96
|
Add (or update) the "Vary" header in the given Response object.
|
plain/utils/encoding.py
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
import codecs
|
2
1
|
import datetime
|
3
|
-
import locale
|
4
2
|
from decimal import Decimal
|
5
3
|
from types import NoneType
|
6
4
|
from urllib.parse import quote
|
@@ -127,109 +125,6 @@ _hextobyte.update(
|
|
127
125
|
)
|
128
126
|
|
129
127
|
|
130
|
-
def uri_to_iri(uri):
|
131
|
-
"""
|
132
|
-
Convert a Uniform Resource Identifier(URI) into an Internationalized
|
133
|
-
Resource Identifier(IRI).
|
134
|
-
|
135
|
-
This is the algorithm from RFC 3987 Section 3.2, excluding step 4.
|
136
|
-
|
137
|
-
Take an URI in ASCII bytes (e.g. '/I%20%E2%99%A5%20Plain/') and return
|
138
|
-
a string containing the encoded result (e.g. '/I%20♥%20Plain/').
|
139
|
-
"""
|
140
|
-
if uri is None:
|
141
|
-
return uri
|
142
|
-
uri = force_bytes(uri)
|
143
|
-
# Fast selective unquote: First, split on '%' and then starting with the
|
144
|
-
# second block, decode the first 2 bytes if they represent a hex code to
|
145
|
-
# decode. The rest of the block is the part after '%AB', not containing
|
146
|
-
# any '%'. Add that to the output without further processing.
|
147
|
-
bits = uri.split(b"%")
|
148
|
-
if len(bits) == 1:
|
149
|
-
iri = uri
|
150
|
-
else:
|
151
|
-
parts = [bits[0]]
|
152
|
-
append = parts.append
|
153
|
-
hextobyte = _hextobyte
|
154
|
-
for item in bits[1:]:
|
155
|
-
hex = item[:2]
|
156
|
-
if hex in hextobyte:
|
157
|
-
append(hextobyte[item[:2]])
|
158
|
-
append(item[2:])
|
159
|
-
else:
|
160
|
-
append(b"%")
|
161
|
-
append(item)
|
162
|
-
iri = b"".join(parts)
|
163
|
-
return repercent_broken_unicode(iri).decode()
|
164
|
-
|
165
|
-
|
166
|
-
def escape_uri_path(path):
|
167
|
-
"""
|
168
|
-
Escape the unsafe characters from the path portion of a Uniform Resource
|
169
|
-
Identifier (URI).
|
170
|
-
"""
|
171
|
-
# These are the "reserved" and "unreserved" characters specified in RFC
|
172
|
-
# 3986 Sections 2.2 and 2.3:
|
173
|
-
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
|
174
|
-
# unreserved = alphanum | mark
|
175
|
-
# mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
|
176
|
-
# The list of safe characters here is constructed subtracting ";", "=",
|
177
|
-
# and "?" according to RFC 3986 Section 3.3.
|
178
|
-
# The reason for not subtracting and escaping "/" is that we are escaping
|
179
|
-
# the entire path, not a path segment.
|
180
|
-
return quote(path, safe="/:@&+$,-_.!~*'()")
|
181
|
-
|
182
|
-
|
183
128
|
def punycode(domain):
|
184
129
|
"""Return the Punycode of the given domain if it's non-ASCII."""
|
185
130
|
return domain.encode("idna").decode("ascii")
|
186
|
-
|
187
|
-
|
188
|
-
def repercent_broken_unicode(path):
|
189
|
-
"""
|
190
|
-
As per RFC 3987 Section 3.2, step three of converting a URI into an IRI,
|
191
|
-
repercent-encode any octet produced that is not part of a strictly legal
|
192
|
-
UTF-8 octet sequence.
|
193
|
-
"""
|
194
|
-
while True:
|
195
|
-
try:
|
196
|
-
path.decode()
|
197
|
-
except UnicodeDecodeError as e:
|
198
|
-
# CVE-2019-14235: A recursion shouldn't be used since the exception
|
199
|
-
# handling uses massive amounts of memory
|
200
|
-
repercent = quote(path[e.start : e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
|
201
|
-
path = path[: e.start] + repercent.encode() + path[e.end :]
|
202
|
-
else:
|
203
|
-
return path
|
204
|
-
|
205
|
-
|
206
|
-
def filepath_to_uri(path):
|
207
|
-
"""Convert a file system path to a URI portion that is suitable for
|
208
|
-
inclusion in a URL.
|
209
|
-
|
210
|
-
Encode certain chars that would normally be recognized as special chars
|
211
|
-
for URIs. Do not encode the ' character, as it is a valid character
|
212
|
-
within URIs. See the encodeURIComponent() JavaScript function for details.
|
213
|
-
"""
|
214
|
-
if path is None:
|
215
|
-
return path
|
216
|
-
# I know about `os.sep` and `os.altsep` but I want to leave
|
217
|
-
# some flexibility for hardcoding separators.
|
218
|
-
return quote(str(path).replace("\\", "/"), safe="/~!*()'")
|
219
|
-
|
220
|
-
|
221
|
-
def get_system_encoding():
|
222
|
-
"""
|
223
|
-
The encoding for the character type functions. Fallback to 'ascii' if the
|
224
|
-
#encoding is unsupported by Python or could not be determined. See tickets
|
225
|
-
#10335 and #5846.
|
226
|
-
"""
|
227
|
-
try:
|
228
|
-
encoding = locale.getlocale()[1] or "ascii"
|
229
|
-
codecs.lookup(encoding)
|
230
|
-
except Exception:
|
231
|
-
encoding = "ascii"
|
232
|
-
return encoding
|
233
|
-
|
234
|
-
|
235
|
-
DEFAULT_LOCALE_ENCODING = get_system_encoding()
|
plain/utils/functional.py
CHANGED
@@ -205,13 +205,6 @@ def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
|
|
205
205
|
return lazy(func, *resultclasses)(*args, **kwargs)
|
206
206
|
|
207
207
|
|
208
|
-
def lazystr(text):
|
209
|
-
"""
|
210
|
-
Shortcut for the common case of a lazy callable that returns str.
|
211
|
-
"""
|
212
|
-
return lazy(str, str)(text)
|
213
|
-
|
214
|
-
|
215
208
|
def keep_lazy(*resultclasses):
|
216
209
|
"""
|
217
210
|
A decorator that allows a function to be called with one or more lazy
|