17.12
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import tempfile
|
||||
from os.path import abspath, dirname, join, normcase, sep
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
|
||||
# For backwards-compatibility in Django 2.0
|
||||
abspathu = abspath
|
||||
|
||||
|
||||
def upath(path):
|
||||
"""Always return a unicode path (did something for Python 2)."""
|
||||
return path
|
||||
|
||||
|
||||
def npath(path):
|
||||
"""
|
||||
Always return a native path, that is unicode on Python 3 and bytestring on
|
||||
Python 2. Noop for Python 3.
|
||||
"""
|
||||
return path
|
||||
|
||||
|
||||
def safe_join(base, *paths):
|
||||
"""
|
||||
Join one or more path components to the base path component intelligently.
|
||||
Return a normalized, absolute version of the final path.
|
||||
|
||||
Raise ValueError if the final path isn't located inside of the base path
|
||||
component.
|
||||
"""
|
||||
final_path = abspath(join(base, *paths))
|
||||
base_path = abspath(base)
|
||||
# Ensure final_path starts with base_path (using normcase to ensure we
|
||||
# don't false-negative on case insensitive operating systems like Windows),
|
||||
# further, one of the following conditions must be true:
|
||||
# a) The next character is the path separator (to prevent conditions like
|
||||
# safe_join("/dir", "/../d"))
|
||||
# b) The final path must be the same as the base path.
|
||||
# c) The base path must be the most root path (meaning either "/" or "C:\\")
|
||||
if (not normcase(final_path).startswith(normcase(base_path + sep)) and
|
||||
normcase(final_path) != normcase(base_path) and
|
||||
dirname(normcase(base_path)) != normcase(base_path)):
|
||||
raise SuspiciousFileOperation(
|
||||
'The joined path ({}) is located outside of the base path '
|
||||
'component ({})'.format(final_path, base_path))
|
||||
return final_path
|
||||
|
||||
|
||||
def symlinks_supported():
|
||||
"""
|
||||
Return whether or not creating symlinks are supported in the host platform
|
||||
and/or if they are allowed to be created (e.g. on Windows it requires admin
|
||||
permissions).
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
original_path = os.path.join(temp_dir, 'original')
|
||||
symlink_path = os.path.join(temp_dir, 'symlink')
|
||||
os.makedirs(original_path)
|
||||
try:
|
||||
os.symlink(original_path, symlink_path)
|
||||
supported = True
|
||||
except (OSError, NotImplementedError):
|
||||
supported = False
|
||||
return supported
|
||||
@@ -0,0 +1,101 @@
|
||||
# Copyright (c) 2010 Guilherme Gondim. All rights reserved.
|
||||
# Copyright (c) 2009 Simon Willison. All rights reserved.
|
||||
# Copyright (c) 2002 Drew Perttula. All rights reserved.
|
||||
#
|
||||
# License:
|
||||
# Python Software Foundation License version 2
|
||||
#
|
||||
# See the file "LICENSE" for terms & conditions for usage, and a DISCLAIMER OF
|
||||
# ALL WARRANTIES.
|
||||
#
|
||||
# This Baseconv distribution contains no GNU General Public Licensed (GPLed)
|
||||
# code so it may be used in proprietary projects just like prior ``baseconv``
|
||||
# distributions.
|
||||
#
|
||||
# All trademarks referenced herein are property of their respective holders.
|
||||
#
|
||||
|
||||
"""
|
||||
Convert numbers from base 10 integers to base X strings and back again.
|
||||
|
||||
Sample usage::
|
||||
|
||||
>>> base20 = BaseConverter('0123456789abcdefghij')
|
||||
>>> base20.encode(1234)
|
||||
'31e'
|
||||
>>> base20.decode('31e')
|
||||
1234
|
||||
>>> base20.encode(-1234)
|
||||
'-31e'
|
||||
>>> base20.decode('-31e')
|
||||
-1234
|
||||
>>> base11 = BaseConverter('0123456789-', sign='$')
|
||||
>>> base11.encode('$1234')
|
||||
'$-22'
|
||||
>>> base11.decode('$-22')
|
||||
'$1234'
|
||||
|
||||
"""
|
||||
|
||||
BASE2_ALPHABET = '01'
|
||||
BASE16_ALPHABET = '0123456789ABCDEF'
|
||||
BASE56_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz'
|
||||
BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
BASE64_ALPHABET = BASE62_ALPHABET + '-_'
|
||||
|
||||
|
||||
class BaseConverter:
|
||||
decimal_digits = '0123456789'
|
||||
|
||||
def __init__(self, digits, sign='-'):
|
||||
self.sign = sign
|
||||
self.digits = digits
|
||||
if sign in self.digits:
|
||||
raise ValueError('Sign character found in converter base digits.')
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: base%s (%s)>" % (self.__class__.__name__, len(self.digits), self.digits)
|
||||
|
||||
def encode(self, i):
|
||||
neg, value = self.convert(i, self.decimal_digits, self.digits, '-')
|
||||
if neg:
|
||||
return self.sign + value
|
||||
return value
|
||||
|
||||
def decode(self, s):
|
||||
neg, value = self.convert(s, self.digits, self.decimal_digits, self.sign)
|
||||
if neg:
|
||||
value = '-' + value
|
||||
return int(value)
|
||||
|
||||
def convert(self, number, from_digits, to_digits, sign):
|
||||
if str(number)[0] == sign:
|
||||
number = str(number)[1:]
|
||||
neg = 1
|
||||
else:
|
||||
neg = 0
|
||||
|
||||
# make an integer out of the number
|
||||
x = 0
|
||||
for digit in str(number):
|
||||
x = x * len(from_digits) + from_digits.index(digit)
|
||||
|
||||
# create the result in base 'len(to_digits)'
|
||||
if x == 0:
|
||||
res = to_digits[0]
|
||||
else:
|
||||
res = ''
|
||||
while x > 0:
|
||||
digit = x % len(to_digits)
|
||||
res = to_digits[digit] + res
|
||||
x = int(x // len(to_digits))
|
||||
return neg, res
|
||||
|
||||
|
||||
base2 = BaseConverter(BASE2_ALPHABET)
|
||||
base16 = BaseConverter(BASE16_ALPHABET)
|
||||
base36 = BaseConverter(BASE36_ALPHABET)
|
||||
base56 = BaseConverter(BASE56_ALPHABET)
|
||||
base62 = BaseConverter(BASE62_ALPHABET)
|
||||
base64 = BaseConverter(BASE64_ALPHABET, sign='$')
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Django's standard crypto functions and utilities.
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
# Use the system PRNG if possible
|
||||
try:
|
||||
random = random.SystemRandom()
|
||||
using_sysrandom = True
|
||||
except NotImplementedError:
|
||||
import warnings
|
||||
warnings.warn('A secure pseudo-random number generator is not available '
|
||||
'on your system. Falling back to Mersenne Twister.')
|
||||
using_sysrandom = False
|
||||
|
||||
|
||||
def salted_hmac(key_salt, value, secret=None):
|
||||
"""
|
||||
Return the HMAC-SHA1 of 'value', using a key generated from key_salt and a
|
||||
secret (which defaults to settings.SECRET_KEY).
|
||||
|
||||
A different key_salt should be passed in for every application of HMAC.
|
||||
"""
|
||||
if secret is None:
|
||||
secret = settings.SECRET_KEY
|
||||
|
||||
key_salt = force_bytes(key_salt)
|
||||
secret = force_bytes(secret)
|
||||
|
||||
# We need to generate a derived key from our base key. We can do this by
|
||||
# passing the key_salt and our base key through a pseudo-random function and
|
||||
# SHA1 works nicely.
|
||||
key = hashlib.sha1(key_salt + secret).digest()
|
||||
|
||||
# If len(key_salt + secret) > sha_constructor().block_size, the above
|
||||
# line is redundant and could be replaced by key = key_salt + secret, since
|
||||
# the hmac module does the same thing for keys longer than the block size.
|
||||
# However, we need to ensure that we *always* do this.
|
||||
return hmac.new(key, msg=force_bytes(value), digestmod=hashlib.sha1)
|
||||
|
||||
|
||||
def get_random_string(length=12,
|
||||
allowed_chars='abcdefghijklmnopqrstuvwxyz'
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
|
||||
"""
|
||||
Return a securely generated random string.
|
||||
|
||||
The default length of 12 with the a-z, A-Z, 0-9 character set returns
|
||||
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
|
||||
"""
|
||||
if not using_sysrandom:
|
||||
# This is ugly, and a hack, but it makes things better than
|
||||
# the alternative of predictability. This re-seeds the PRNG
|
||||
# using a value that is hard for an attacker to predict, every
|
||||
# time a random string is required. This may change the
|
||||
# properties of the chosen random sequence slightly, but this
|
||||
# is better than absolute predictability.
|
||||
random.seed(
|
||||
hashlib.sha256(
|
||||
('%s%s%s' % (random.getstate(), time.time(), settings.SECRET_KEY)).encode()
|
||||
).digest()
|
||||
)
|
||||
return ''.join(random.choice(allowed_chars) for i in range(length))
|
||||
|
||||
|
||||
def constant_time_compare(val1, val2):
|
||||
"""Return True if the two strings are equal, False otherwise."""
|
||||
return hmac.compare_digest(force_bytes(val1), force_bytes(val2))
|
||||
|
||||
|
||||
def pbkdf2(password, salt, iterations, dklen=0, digest=None):
|
||||
"""Return the hash of password using pbkdf2."""
|
||||
if digest is None:
|
||||
digest = hashlib.sha256
|
||||
dklen = dklen or None
|
||||
password = force_bytes(password)
|
||||
salt = force_bytes(salt)
|
||||
return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)
|
||||
@@ -0,0 +1,55 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
||||
def deconstructible(*args, path=None):
|
||||
"""
|
||||
Class decorator that allows the decorated class to be serialized
|
||||
by the migrations subsystem.
|
||||
|
||||
The `path` kwarg specifies the import path.
|
||||
"""
|
||||
def decorator(klass):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We capture the arguments to make returning them trivial
|
||||
obj = super(klass, cls).__new__(cls)
|
||||
obj._constructor_args = (args, kwargs)
|
||||
return obj
|
||||
|
||||
def deconstruct(obj):
|
||||
"""
|
||||
Return a 3-tuple of class import path, positional arguments,
|
||||
and keyword arguments.
|
||||
"""
|
||||
# Fallback version
|
||||
if path:
|
||||
module_name, _, name = path.rpartition('.')
|
||||
else:
|
||||
module_name = obj.__module__
|
||||
name = obj.__class__.__name__
|
||||
# Make sure it's actually there and not an inner class
|
||||
module = import_module(module_name)
|
||||
if not hasattr(module, name):
|
||||
raise ValueError(
|
||||
"Could not find object %s in %s.\n"
|
||||
"Please note that you cannot serialize things like inner "
|
||||
"classes. Please move the object into the main module "
|
||||
"body to use migrations.\n"
|
||||
"For more information, see "
|
||||
"https://docs.djangoproject.com/en/%s/topics/migrations/#serializing-values"
|
||||
% (name, module_name, get_docs_version()))
|
||||
return (
|
||||
path or '%s.%s' % (obj.__class__.__module__, name),
|
||||
obj._constructor_args[0],
|
||||
obj._constructor_args[1],
|
||||
)
|
||||
|
||||
klass.__new__ = staticmethod(__new__)
|
||||
klass.deconstruct = deconstruct
|
||||
|
||||
return klass
|
||||
|
||||
if not args:
|
||||
return decorator
|
||||
return decorator(*args)
|
||||
@@ -0,0 +1,97 @@
|
||||
import inspect
|
||||
import warnings
|
||||
|
||||
|
||||
class RemovedInDjango30Warning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class RemovedInDjango31Warning(PendingDeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
RemovedInNextVersionWarning = RemovedInDjango30Warning
|
||||
|
||||
|
||||
class warn_about_renamed_method:
|
||||
def __init__(self, class_name, old_method_name, new_method_name, deprecation_warning):
|
||||
self.class_name = class_name
|
||||
self.old_method_name = old_method_name
|
||||
self.new_method_name = new_method_name
|
||||
self.deprecation_warning = deprecation_warning
|
||||
|
||||
def __call__(self, f):
|
||||
def wrapped(*args, **kwargs):
|
||||
warnings.warn(
|
||||
"`%s.%s` is deprecated, use `%s` instead." %
|
||||
(self.class_name, self.old_method_name, self.new_method_name),
|
||||
self.deprecation_warning, 2)
|
||||
return f(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
class RenameMethodsBase(type):
|
||||
"""
|
||||
Handles the deprecation paths when renaming a method.
|
||||
|
||||
It does the following:
|
||||
1) Define the new method if missing and complain about it.
|
||||
2) Define the old method if missing.
|
||||
3) Complain whenever an old method is called.
|
||||
|
||||
See #15363 for more details.
|
||||
"""
|
||||
|
||||
renamed_methods = ()
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
new_class = super().__new__(cls, name, bases, attrs)
|
||||
|
||||
for base in inspect.getmro(new_class):
|
||||
class_name = base.__name__
|
||||
for renamed_method in cls.renamed_methods:
|
||||
old_method_name = renamed_method[0]
|
||||
old_method = base.__dict__.get(old_method_name)
|
||||
new_method_name = renamed_method[1]
|
||||
new_method = base.__dict__.get(new_method_name)
|
||||
deprecation_warning = renamed_method[2]
|
||||
wrapper = warn_about_renamed_method(class_name, *renamed_method)
|
||||
|
||||
# Define the new method if missing and complain about it
|
||||
if not new_method and old_method:
|
||||
warnings.warn(
|
||||
"`%s.%s` method should be renamed `%s`." %
|
||||
(class_name, old_method_name, new_method_name),
|
||||
deprecation_warning, 2)
|
||||
setattr(base, new_method_name, old_method)
|
||||
setattr(base, old_method_name, wrapper(old_method))
|
||||
|
||||
# Define the old method as a wrapped call to the new method.
|
||||
if not old_method and new_method:
|
||||
setattr(base, old_method_name, wrapper(new_method))
|
||||
|
||||
return new_class
|
||||
|
||||
|
||||
class DeprecationInstanceCheck(type):
|
||||
def __instancecheck__(self, instance):
|
||||
warnings.warn(
|
||||
"`%s` is deprecated, use `%s` instead." % (self.__name__, self.alternative),
|
||||
self.deprecation_warning, 2
|
||||
)
|
||||
return super().__instancecheck__(instance)
|
||||
|
||||
|
||||
class MiddlewareMixin:
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, request):
|
||||
response = None
|
||||
if hasattr(self, 'process_request'):
|
||||
response = self.process_request(request)
|
||||
response = response or self.get_response(request)
|
||||
if hasattr(self, 'process_response'):
|
||||
response = self.process_response(request, response)
|
||||
return response
|
||||
@@ -0,0 +1,44 @@
|
||||
import datetime
|
||||
|
||||
|
||||
def _get_duration_components(duration):
|
||||
days = duration.days
|
||||
seconds = duration.seconds
|
||||
microseconds = duration.microseconds
|
||||
|
||||
minutes = seconds // 60
|
||||
seconds = seconds % 60
|
||||
|
||||
hours = minutes // 60
|
||||
minutes = minutes % 60
|
||||
|
||||
return days, hours, minutes, seconds, microseconds
|
||||
|
||||
|
||||
def duration_string(duration):
|
||||
"""Version of str(timedelta) which is not English specific."""
|
||||
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
|
||||
|
||||
string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
|
||||
if days:
|
||||
string = '{} '.format(days) + string
|
||||
if microseconds:
|
||||
string += '.{:06d}'.format(microseconds)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def duration_iso_string(duration):
|
||||
if duration < datetime.timedelta(0):
|
||||
sign = '-'
|
||||
duration *= -1
|
||||
else:
|
||||
sign = ''
|
||||
|
||||
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
|
||||
ms = '.{:06d}'.format(microseconds) if microseconds else ""
|
||||
return '{}P{}DT{:02d}H{:02d}M{:02d}{}S'.format(sign, days, hours, minutes, seconds, ms)
|
||||
|
||||
|
||||
def duration_microseconds(delta):
|
||||
return (24 * 60 * 60 * delta.days + delta.seconds) * 1000000 + delta.microseconds
|
||||
@@ -0,0 +1,433 @@
|
||||
import copy
|
||||
import itertools
|
||||
import operator
|
||||
from functools import total_ordering, wraps
|
||||
|
||||
from django.utils.version import PY36, get_docs_version
|
||||
|
||||
|
||||
# You can't trivially replace this with `functools.partial` because this binds
|
||||
# to classes and returns bound instances, whereas functools.partial (on
|
||||
# CPython) is a type and its instances don't bind.
|
||||
def curry(_curried_func, *args, **kwargs):
|
||||
def _curried(*moreargs, **morekwargs):
|
||||
return _curried_func(*args, *moreargs, **{**kwargs, **morekwargs})
|
||||
return _curried
|
||||
|
||||
|
||||
class cached_property:
|
||||
"""
|
||||
Decorator that converts a method with a single self argument into a
|
||||
property cached on the instance.
|
||||
|
||||
A cached property can be made out of an existing method:
|
||||
(e.g. ``url = cached_property(get_absolute_url)``).
|
||||
On Python < 3.6, the optional ``name`` argument must be provided, e.g.
|
||||
``url = cached_property(get_absolute_url, name='url')``.
|
||||
"""
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def func(instance):
|
||||
raise TypeError(
|
||||
'Cannot use cached_property instance without calling '
|
||||
'__set_name__() on it.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_mangled(name):
|
||||
return name.startswith('__') and not name.endswith('__')
|
||||
|
||||
def __init__(self, func, name=None):
|
||||
if PY36:
|
||||
self.real_func = func
|
||||
else:
|
||||
func_name = func.__name__
|
||||
name = name or func_name
|
||||
if not (isinstance(name, str) and name.isidentifier()):
|
||||
raise ValueError(
|
||||
"%r can't be used as the name of a cached_property." % name,
|
||||
)
|
||||
if self._is_mangled(name):
|
||||
raise ValueError(
|
||||
'cached_property does not work with mangled methods on '
|
||||
'Python < 3.6 without the appropriate `name` argument. See '
|
||||
'https://docs.djangoproject.com/en/%s/ref/utils/'
|
||||
'#cached-property-mangled-name' % get_docs_version(),
|
||||
)
|
||||
self.name = name
|
||||
self.func = func
|
||||
self.__doc__ = getattr(func, '__doc__')
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if self.name is None:
|
||||
self.name = name
|
||||
self.func = self.real_func
|
||||
elif name != self.name:
|
||||
raise TypeError(
|
||||
"Cannot assign the same cached_property to two different names "
|
||||
"(%r and %r)." % (self.name, name)
|
||||
)
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Call the function and put the return value in instance.__dict__ so that
|
||||
subsequent attribute access on the instance returns the cached value
|
||||
instead of calling cached_property.__get__().
|
||||
"""
|
||||
if instance is None:
|
||||
return self
|
||||
res = instance.__dict__[self.name] = self.func(instance)
|
||||
return res
|
||||
|
||||
|
||||
class Promise:
|
||||
"""
|
||||
Base class for the proxy class created in the closure of the lazy function.
|
||||
It's used to recognize promises in code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def lazy(func, *resultclasses):
|
||||
"""
|
||||
Turn any callable into a lazy evaluated callable. result classes or types
|
||||
is required -- at least one is needed so that the automatic forcing of
|
||||
the lazy evaluation code is triggered. Results are not memoized; the
|
||||
function is evaluated on every access.
|
||||
"""
|
||||
|
||||
@total_ordering
|
||||
class __proxy__(Promise):
|
||||
"""
|
||||
Encapsulate a function call and act as a proxy for methods that are
|
||||
called on the result of that function. The function is not evaluated
|
||||
until one of the methods on the result is called.
|
||||
"""
|
||||
__prepared = False
|
||||
|
||||
def __init__(self, args, kw):
|
||||
self.__args = args
|
||||
self.__kw = kw
|
||||
if not self.__prepared:
|
||||
self.__prepare_class__()
|
||||
self.__prepared = True
|
||||
|
||||
def __reduce__(self):
|
||||
return (
|
||||
_lazy_proxy_unpickle,
|
||||
(func, self.__args, self.__kw) + resultclasses
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__cast())
|
||||
|
||||
@classmethod
|
||||
def __prepare_class__(cls):
|
||||
for resultclass in resultclasses:
|
||||
for type_ in resultclass.mro():
|
||||
for method_name in type_.__dict__:
|
||||
# All __promise__ return the same wrapper method, they
|
||||
# look up the correct implementation when called.
|
||||
if hasattr(cls, method_name):
|
||||
continue
|
||||
meth = cls.__promise__(method_name)
|
||||
setattr(cls, method_name, meth)
|
||||
cls._delegate_bytes = bytes in resultclasses
|
||||
cls._delegate_text = str in resultclasses
|
||||
assert not (cls._delegate_bytes and cls._delegate_text), (
|
||||
"Cannot call lazy() with both bytes and text return types.")
|
||||
if cls._delegate_text:
|
||||
cls.__str__ = cls.__text_cast
|
||||
elif cls._delegate_bytes:
|
||||
cls.__bytes__ = cls.__bytes_cast
|
||||
|
||||
@classmethod
|
||||
def __promise__(cls, method_name):
|
||||
# Builds a wrapper around some magic method
|
||||
def __wrapper__(self, *args, **kw):
|
||||
# Automatically triggers the evaluation of a lazy value and
|
||||
# applies the given magic method of the result type.
|
||||
res = func(*self.__args, **self.__kw)
|
||||
return getattr(res, method_name)(*args, **kw)
|
||||
return __wrapper__
|
||||
|
||||
def __text_cast(self):
|
||||
return func(*self.__args, **self.__kw)
|
||||
|
||||
def __bytes_cast(self):
|
||||
return bytes(func(*self.__args, **self.__kw))
|
||||
|
||||
def __bytes_cast_encoded(self):
|
||||
return func(*self.__args, **self.__kw).encode()
|
||||
|
||||
def __cast(self):
|
||||
if self._delegate_bytes:
|
||||
return self.__bytes_cast()
|
||||
elif self._delegate_text:
|
||||
return self.__text_cast()
|
||||
else:
|
||||
return func(*self.__args, **self.__kw)
|
||||
|
||||
def __str__(self):
|
||||
# object defines __str__(), so __prepare_class__() won't overload
|
||||
# a __str__() method from the proxied class.
|
||||
return str(self.__cast())
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() == other
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() < other
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__cast())
|
||||
|
||||
def __mod__(self, rhs):
|
||||
if self._delegate_text:
|
||||
return str(self) % rhs
|
||||
return self.__cast() % rhs
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
# Instances of this class are effectively immutable. It's just a
|
||||
# collection of functions. So we don't need to do anything
|
||||
# complicated for copying.
|
||||
memo[id(self)] = self
|
||||
return self
|
||||
|
||||
@wraps(func)
|
||||
def __wrapper__(*args, **kw):
|
||||
# Creates the proxy object, instead of the actual value.
|
||||
return __proxy__(args, kw)
|
||||
|
||||
return __wrapper__
|
||||
|
||||
|
||||
def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
|
||||
return lazy(func, *resultclasses)(*args, **kwargs)
|
||||
|
||||
|
||||
def lazystr(text):
|
||||
"""
|
||||
Shortcut for the common case of a lazy callable that returns str.
|
||||
"""
|
||||
return lazy(str, str)(text)
|
||||
|
||||
|
||||
def keep_lazy(*resultclasses):
|
||||
"""
|
||||
A decorator that allows a function to be called with one or more lazy
|
||||
arguments. If none of the args are lazy, the function is evaluated
|
||||
immediately, otherwise a __proxy__ is returned that will evaluate the
|
||||
function when needed.
|
||||
"""
|
||||
if not resultclasses:
|
||||
raise TypeError("You must pass at least one argument to keep_lazy().")
|
||||
|
||||
def decorator(func):
|
||||
lazy_func = lazy(func, *resultclasses)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if any(isinstance(arg, Promise) for arg in itertools.chain(args, kwargs.values())):
|
||||
return lazy_func(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def keep_lazy_text(func):
|
||||
"""
|
||||
A decorator for functions that accept lazy arguments and return text.
|
||||
"""
|
||||
return keep_lazy(str)(func)
|
||||
|
||||
|
||||
empty = object()
|
||||
|
||||
|
||||
def new_method_proxy(func):
|
||||
def inner(self, *args):
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
return func(self._wrapped, *args)
|
||||
return inner
|
||||
|
||||
|
||||
class LazyObject:
|
||||
"""
|
||||
A wrapper for another class that can be used to delay instantiation of the
|
||||
wrapped class.
|
||||
|
||||
By subclassing, you have the opportunity to intercept and alter the
|
||||
instantiation. If you don't need to do that, use SimpleLazyObject.
|
||||
"""
|
||||
|
||||
# Avoid infinite recursion when tracing __init__ (#19456).
|
||||
_wrapped = None
|
||||
|
||||
def __init__(self):
|
||||
# Note: if a subclass overrides __init__(), it will likely need to
|
||||
# override __copy__() and __deepcopy__() as well.
|
||||
self._wrapped = empty
|
||||
|
||||
__getattr__ = new_method_proxy(getattr)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name == "_wrapped":
|
||||
# Assign to __dict__ to avoid infinite __setattr__ loops.
|
||||
self.__dict__["_wrapped"] = value
|
||||
else:
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
setattr(self._wrapped, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
if name == "_wrapped":
|
||||
raise TypeError("can't delete _wrapped.")
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
delattr(self._wrapped, name)
|
||||
|
||||
def _setup(self):
|
||||
"""
|
||||
Must be implemented by subclasses to initialize the wrapped object.
|
||||
"""
|
||||
raise NotImplementedError('subclasses of LazyObject must provide a _setup() method')
|
||||
|
||||
# Because we have messed with __class__ below, we confuse pickle as to what
|
||||
# class we are pickling. We're going to have to initialize the wrapped
|
||||
# object to successfully pickle it, so we might as well just pickle the
|
||||
# wrapped object since they're supposed to act the same way.
|
||||
#
|
||||
# Unfortunately, if we try to simply act like the wrapped object, the ruse
|
||||
# will break down when pickle gets our id(). Thus we end up with pickle
|
||||
# thinking, in effect, that we are a distinct object from the wrapped
|
||||
# object, but with the same __dict__. This can cause problems (see #25389).
|
||||
#
|
||||
# So instead, we define our own __reduce__ method and custom unpickler. We
|
||||
# pickle the wrapped object as the unpickler's argument, so that pickle
|
||||
# will pickle it normally, and then the unpickler simply returns its
|
||||
# argument.
|
||||
def __reduce__(self):
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
return (unpickle_lazyobject, (self._wrapped,))
|
||||
|
||||
def __copy__(self):
|
||||
if self._wrapped is empty:
|
||||
# If uninitialized, copy the wrapper. Use type(self), not
|
||||
# self.__class__, because the latter is proxied.
|
||||
return type(self)()
|
||||
else:
|
||||
# If initialized, return a copy of the wrapped object.
|
||||
return copy.copy(self._wrapped)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
if self._wrapped is empty:
|
||||
# We have to use type(self), not self.__class__, because the
|
||||
# latter is proxied.
|
||||
result = type(self)()
|
||||
memo[id(self)] = result
|
||||
return result
|
||||
return copy.deepcopy(self._wrapped, memo)
|
||||
|
||||
__bytes__ = new_method_proxy(bytes)
|
||||
__str__ = new_method_proxy(str)
|
||||
__bool__ = new_method_proxy(bool)
|
||||
|
||||
# Introspection support
|
||||
__dir__ = new_method_proxy(dir)
|
||||
|
||||
# Need to pretend to be the wrapped class, for the sake of objects that
|
||||
# care about this (especially in equality tests)
|
||||
__class__ = property(new_method_proxy(operator.attrgetter("__class__")))
|
||||
__eq__ = new_method_proxy(operator.eq)
|
||||
__lt__ = new_method_proxy(operator.lt)
|
||||
__gt__ = new_method_proxy(operator.gt)
|
||||
__ne__ = new_method_proxy(operator.ne)
|
||||
__hash__ = new_method_proxy(hash)
|
||||
|
||||
# List/Tuple/Dictionary methods support
|
||||
__getitem__ = new_method_proxy(operator.getitem)
|
||||
__setitem__ = new_method_proxy(operator.setitem)
|
||||
__delitem__ = new_method_proxy(operator.delitem)
|
||||
__iter__ = new_method_proxy(iter)
|
||||
__len__ = new_method_proxy(len)
|
||||
__contains__ = new_method_proxy(operator.contains)
|
||||
|
||||
|
||||
def unpickle_lazyobject(wrapped):
|
||||
"""
|
||||
Used to unpickle lazy objects. Just return its argument, which will be the
|
||||
wrapped object.
|
||||
"""
|
||||
return wrapped
|
||||
|
||||
|
||||
class SimpleLazyObject(LazyObject):
|
||||
"""
|
||||
A lazy object initialized from any function.
|
||||
|
||||
Designed for compound objects of unknown type. For builtins or objects of
|
||||
known type, use django.utils.functional.lazy.
|
||||
"""
|
||||
def __init__(self, func):
|
||||
"""
|
||||
Pass in a callable that returns the object to be wrapped.
|
||||
|
||||
If copies are made of the resulting SimpleLazyObject, which can happen
|
||||
in various circumstances within Django, then you must ensure that the
|
||||
callable can be safely run more than once and will return the same
|
||||
value.
|
||||
"""
|
||||
self.__dict__['_setupfunc'] = func
|
||||
super().__init__()
|
||||
|
||||
def _setup(self):
|
||||
self._wrapped = self._setupfunc()
|
||||
|
||||
# Return a meaningful representation of the lazy object for debugging
|
||||
# without evaluating the wrapped object.
|
||||
def __repr__(self):
|
||||
if self._wrapped is empty:
|
||||
repr_attr = self._setupfunc
|
||||
else:
|
||||
repr_attr = self._wrapped
|
||||
return '<%s: %r>' % (type(self).__name__, repr_attr)
|
||||
|
||||
def __copy__(self):
|
||||
if self._wrapped is empty:
|
||||
# If uninitialized, copy the wrapper. Use SimpleLazyObject, not
|
||||
# self.__class__, because the latter is proxied.
|
||||
return SimpleLazyObject(self._setupfunc)
|
||||
else:
|
||||
# If initialized, return a copy of the wrapped object.
|
||||
return copy.copy(self._wrapped)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
if self._wrapped is empty:
|
||||
# We have to use SimpleLazyObject, not self.__class__, because the
|
||||
# latter is proxied.
|
||||
result = SimpleLazyObject(self._setupfunc)
|
||||
memo[id(self)] = result
|
||||
return result
|
||||
return copy.deepcopy(self._wrapped, memo)
|
||||
|
||||
|
||||
def partition(predicate, values):
|
||||
"""
|
||||
Split the values into two sets, based on the return value of the function
|
||||
(True/False). e.g.:
|
||||
|
||||
>>> partition(lambda x: x > 3, range(5))
|
||||
[0, 1, 2, 3], [4]
|
||||
"""
|
||||
results = ([], [])
|
||||
for item in values:
|
||||
results[predicate(item)].append(item)
|
||||
return results
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Utility functions for generating "lorem ipsum" Latin text.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
COMMON_P = (
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod '
|
||||
'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim '
|
||||
'veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea '
|
||||
'commodo consequat. Duis aute irure dolor in reprehenderit in voluptate '
|
||||
'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint '
|
||||
'occaecat cupidatat non proident, sunt in culpa qui officia deserunt '
|
||||
'mollit anim id est laborum.'
|
||||
)
|
||||
|
||||
WORDS = (
|
||||
'exercitationem', 'perferendis', 'perspiciatis', 'laborum', 'eveniet',
|
||||
'sunt', 'iure', 'nam', 'nobis', 'eum', 'cum', 'officiis', 'excepturi',
|
||||
'odio', 'consectetur', 'quasi', 'aut', 'quisquam', 'vel', 'eligendi',
|
||||
'itaque', 'non', 'odit', 'tempore', 'quaerat', 'dignissimos',
|
||||
'facilis', 'neque', 'nihil', 'expedita', 'vitae', 'vero', 'ipsum',
|
||||
'nisi', 'animi', 'cumque', 'pariatur', 'velit', 'modi', 'natus',
|
||||
'iusto', 'eaque', 'sequi', 'illo', 'sed', 'ex', 'et', 'voluptatibus',
|
||||
'tempora', 'veritatis', 'ratione', 'assumenda', 'incidunt', 'nostrum',
|
||||
'placeat', 'aliquid', 'fuga', 'provident', 'praesentium', 'rem',
|
||||
'necessitatibus', 'suscipit', 'adipisci', 'quidem', 'possimus',
|
||||
'voluptas', 'debitis', 'sint', 'accusantium', 'unde', 'sapiente',
|
||||
'voluptate', 'qui', 'aspernatur', 'laudantium', 'soluta', 'amet',
|
||||
'quo', 'aliquam', 'saepe', 'culpa', 'libero', 'ipsa', 'dicta',
|
||||
'reiciendis', 'nesciunt', 'doloribus', 'autem', 'impedit', 'minima',
|
||||
'maiores', 'repudiandae', 'ipsam', 'obcaecati', 'ullam', 'enim',
|
||||
'totam', 'delectus', 'ducimus', 'quis', 'voluptates', 'dolores',
|
||||
'molestiae', 'harum', 'dolorem', 'quia', 'voluptatem', 'molestias',
|
||||
'magni', 'distinctio', 'omnis', 'illum', 'dolorum', 'voluptatum', 'ea',
|
||||
'quas', 'quam', 'corporis', 'quae', 'blanditiis', 'atque', 'deserunt',
|
||||
'laboriosam', 'earum', 'consequuntur', 'hic', 'cupiditate',
|
||||
'quibusdam', 'accusamus', 'ut', 'rerum', 'error', 'minus', 'eius',
|
||||
'ab', 'ad', 'nemo', 'fugit', 'officia', 'at', 'in', 'id', 'quos',
|
||||
'reprehenderit', 'numquam', 'iste', 'fugiat', 'sit', 'inventore',
|
||||
'beatae', 'repellendus', 'magnam', 'recusandae', 'quod', 'explicabo',
|
||||
'doloremque', 'aperiam', 'consequatur', 'asperiores', 'commodi',
|
||||
'optio', 'dolor', 'labore', 'temporibus', 'repellat', 'veniam',
|
||||
'architecto', 'est', 'esse', 'mollitia', 'nulla', 'a', 'similique',
|
||||
'eos', 'alias', 'dolore', 'tenetur', 'deleniti', 'porro', 'facere',
|
||||
'maxime', 'corrupti',
|
||||
)
|
||||
|
||||
COMMON_WORDS = (
|
||||
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur',
|
||||
'adipisicing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt',
|
||||
'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua',
|
||||
)
|
||||
|
||||
|
||||
def sentence():
|
||||
"""
|
||||
Return a randomly generated sentence of lorem ipsum text.
|
||||
|
||||
The first word is capitalized, and the sentence ends in either a period or
|
||||
question mark. Commas are added at random.
|
||||
"""
|
||||
# Determine the number of comma-separated sections and number of words in
|
||||
# each section for this sentence.
|
||||
sections = [' '.join(random.sample(WORDS, random.randint(3, 12))) for i in range(random.randint(1, 5))]
|
||||
s = ', '.join(sections)
|
||||
# Convert to sentence case and add end punctuation.
|
||||
return '%s%s%s' % (s[0].upper(), s[1:], random.choice('?.'))
|
||||
|
||||
|
||||
def paragraph():
|
||||
"""
|
||||
Return a randomly generated paragraph of lorem ipsum text.
|
||||
|
||||
The paragraph consists of between 1 and 4 sentences, inclusive.
|
||||
"""
|
||||
return ' '.join(sentence() for i in range(random.randint(1, 4)))
|
||||
|
||||
|
||||
def paragraphs(count, common=True):
|
||||
"""
|
||||
Return a list of paragraphs as returned by paragraph().
|
||||
|
||||
If `common` is True, then the first paragraph will be the standard
|
||||
'lorem ipsum' paragraph. Otherwise, the first paragraph will be random
|
||||
Latin text. Either way, subsequent paragraphs will be random Latin text.
|
||||
"""
|
||||
paras = []
|
||||
for i in range(count):
|
||||
if common and i == 0:
|
||||
paras.append(COMMON_P)
|
||||
else:
|
||||
paras.append(paragraph())
|
||||
return paras
|
||||
|
||||
|
||||
def words(count, common=True):
|
||||
"""
|
||||
Return a string of `count` lorem ipsum words separated by a single space.
|
||||
|
||||
If `common` is True, then the first 19 words will be the standard
|
||||
'lorem ipsum' words. Otherwise, all words will be selected randomly.
|
||||
"""
|
||||
word_list = list(COMMON_WORDS) if common else []
|
||||
c = len(word_list)
|
||||
if count > c:
|
||||
count -= c
|
||||
while count > 0:
|
||||
c = min(count, len(WORDS))
|
||||
count -= c
|
||||
word_list += random.sample(WORDS, c)
|
||||
else:
|
||||
word_list = word_list[:count]
|
||||
return ' '.join(word_list)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Functions for working with "safe strings": strings that can be displayed safely
|
||||
without further escaping in HTML. Marking something as a "safe string" means
|
||||
that the producer of the string has already turned characters that should not
|
||||
be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
|
||||
"""
|
||||
|
||||
from django.utils.functional import wraps
|
||||
|
||||
|
||||
class SafeData:
|
||||
def __html__(self):
|
||||
"""
|
||||
Return the html representation of a string for interoperability.
|
||||
|
||||
This allows other template engines to understand Django's SafeData.
|
||||
"""
|
||||
return self
|
||||
|
||||
|
||||
class SafeBytes(bytes, SafeData):
|
||||
"""
|
||||
A bytes subclass that has been specifically marked as "safe" (requires no
|
||||
further escaping) for HTML output purposes.
|
||||
|
||||
Kept in Django 2.0 for usage by apps supporting Python 2. Shouldn't be used
|
||||
in Django anymore.
|
||||
"""
|
||||
def __add__(self, rhs):
|
||||
"""
|
||||
Concatenating a safe byte string with another safe byte string or safe
|
||||
string is safe. Otherwise, the result is no longer safe.
|
||||
"""
|
||||
t = super().__add__(rhs)
|
||||
if isinstance(rhs, SafeText):
|
||||
return SafeText(t)
|
||||
elif isinstance(rhs, SafeBytes):
|
||||
return SafeBytes(t)
|
||||
return t
|
||||
|
||||
|
||||
class SafeText(str, SafeData):
|
||||
"""
|
||||
A str subclass that has been specifically marked as "safe" for HTML output
|
||||
purposes.
|
||||
"""
|
||||
def __add__(self, rhs):
|
||||
"""
|
||||
Concatenating a safe string with another safe bytestring or
|
||||
safe string is safe. Otherwise, the result is no longer safe.
|
||||
"""
|
||||
t = super().__add__(rhs)
|
||||
if isinstance(rhs, SafeData):
|
||||
return SafeText(t)
|
||||
return t
|
||||
|
||||
def __str__(self):
|
||||
return self
|
||||
|
||||
|
||||
SafeString = SafeText
|
||||
|
||||
|
||||
def _safety_decorator(safety_marker, func):
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return safety_marker(func(*args, **kwargs))
|
||||
return wrapped
|
||||
|
||||
|
||||
def mark_safe(s):
|
||||
"""
|
||||
Explicitly mark a string as safe for (HTML) output purposes. The returned
|
||||
object can be used everywhere a string is appropriate.
|
||||
|
||||
If used on a method as a decorator, mark the returned data as safe.
|
||||
|
||||
Can be called multiple times on a single string.
|
||||
"""
|
||||
if hasattr(s, '__html__'):
|
||||
return s
|
||||
if callable(s):
|
||||
return _safety_decorator(mark_safe, s)
|
||||
return SafeText(s)
|
||||
@@ -0,0 +1,227 @@
|
||||
import re
|
||||
import warnings
|
||||
from io import StringIO
|
||||
|
||||
from django.template.base import TRANSLATOR_COMMENT_MARK, Lexer, TokenType
|
||||
|
||||
from . import TranslatorCommentWarning, trim_whitespace
|
||||
|
||||
dot_re = re.compile(r'\S')
|
||||
|
||||
|
||||
def blankout(src, char):
|
||||
"""
|
||||
Change every non-whitespace character to the given char.
|
||||
Used in the templatize function.
|
||||
"""
|
||||
return dot_re.sub(char, src)
|
||||
|
||||
|
||||
context_re = re.compile(r"""^\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?'))\s*""")
|
||||
inline_re = re.compile(
|
||||
# Match the trans 'some text' part
|
||||
r"""^\s*trans\s+((?:"[^"]*?")|(?:'[^']*?'))"""
|
||||
# Match and ignore optional filters
|
||||
r"""(?:\s*\|\s*[^\s:]+(?::(?:[^\s'":]+|(?:"[^"]*?")|(?:'[^']*?')))?)*"""
|
||||
# Match the optional context part
|
||||
r"""(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?\s*"""
|
||||
)
|
||||
block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)""")
|
||||
endblock_re = re.compile(r"""^\s*endblocktrans$""")
|
||||
plural_re = re.compile(r"""^\s*plural$""")
|
||||
constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
|
||||
|
||||
|
||||
def templatize(src, origin=None):
|
||||
"""
|
||||
Turn a Django template into something that is understood by xgettext. It
|
||||
does so by translating the Django translation tags into standard gettext
|
||||
function invocations.
|
||||
"""
|
||||
out = StringIO('')
|
||||
message_context = None
|
||||
intrans = False
|
||||
inplural = False
|
||||
trimmed = False
|
||||
singular = []
|
||||
plural = []
|
||||
incomment = False
|
||||
comment = []
|
||||
lineno_comment_map = {}
|
||||
comment_lineno_cache = None
|
||||
# Adding the u prefix allows gettext to recognize the string (#26093).
|
||||
raw_prefix = 'u'
|
||||
|
||||
def join_tokens(tokens, trim=False):
|
||||
message = ''.join(tokens)
|
||||
if trim:
|
||||
message = trim_whitespace(message)
|
||||
return message
|
||||
|
||||
for t in Lexer(src).tokenize():
|
||||
if incomment:
|
||||
if t.token_type == TokenType.BLOCK and t.contents == 'endcomment':
|
||||
content = ''.join(comment)
|
||||
translators_comment_start = None
|
||||
for lineno, line in enumerate(content.splitlines(True)):
|
||||
if line.lstrip().startswith(TRANSLATOR_COMMENT_MARK):
|
||||
translators_comment_start = lineno
|
||||
for lineno, line in enumerate(content.splitlines(True)):
|
||||
if translators_comment_start is not None and lineno >= translators_comment_start:
|
||||
out.write(' # %s' % line)
|
||||
else:
|
||||
out.write(' #\n')
|
||||
incomment = False
|
||||
comment = []
|
||||
else:
|
||||
comment.append(t.contents)
|
||||
elif intrans:
|
||||
if t.token_type == TokenType.BLOCK:
|
||||
endbmatch = endblock_re.match(t.contents)
|
||||
pluralmatch = plural_re.match(t.contents)
|
||||
if endbmatch:
|
||||
if inplural:
|
||||
if message_context:
|
||||
out.write(' npgettext({p}{!r}, {p}{!r}, {p}{!r},count) '.format(
|
||||
message_context,
|
||||
join_tokens(singular, trimmed),
|
||||
join_tokens(plural, trimmed),
|
||||
p=raw_prefix,
|
||||
))
|
||||
else:
|
||||
out.write(' ngettext({p}{!r}, {p}{!r}, count) '.format(
|
||||
join_tokens(singular, trimmed),
|
||||
join_tokens(plural, trimmed),
|
||||
p=raw_prefix,
|
||||
))
|
||||
for part in singular:
|
||||
out.write(blankout(part, 'S'))
|
||||
for part in plural:
|
||||
out.write(blankout(part, 'P'))
|
||||
else:
|
||||
if message_context:
|
||||
out.write(' pgettext({p}{!r}, {p}{!r}) '.format(
|
||||
message_context,
|
||||
join_tokens(singular, trimmed),
|
||||
p=raw_prefix,
|
||||
))
|
||||
else:
|
||||
out.write(' gettext({p}{!r}) '.format(
|
||||
join_tokens(singular, trimmed),
|
||||
p=raw_prefix,
|
||||
))
|
||||
for part in singular:
|
||||
out.write(blankout(part, 'S'))
|
||||
message_context = None
|
||||
intrans = False
|
||||
inplural = False
|
||||
singular = []
|
||||
plural = []
|
||||
elif pluralmatch:
|
||||
inplural = True
|
||||
else:
|
||||
filemsg = ''
|
||||
if origin:
|
||||
filemsg = 'file %s, ' % origin
|
||||
raise SyntaxError(
|
||||
"Translation blocks must not include other block tags: "
|
||||
"%s (%sline %d)" % (t.contents, filemsg, t.lineno)
|
||||
)
|
||||
elif t.token_type == TokenType.VAR:
|
||||
if inplural:
|
||||
plural.append('%%(%s)s' % t.contents)
|
||||
else:
|
||||
singular.append('%%(%s)s' % t.contents)
|
||||
elif t.token_type == TokenType.TEXT:
|
||||
contents = t.contents.replace('%', '%%')
|
||||
if inplural:
|
||||
plural.append(contents)
|
||||
else:
|
||||
singular.append(contents)
|
||||
else:
|
||||
# Handle comment tokens (`{# ... #}`) plus other constructs on
|
||||
# the same line:
|
||||
if comment_lineno_cache is not None:
|
||||
cur_lineno = t.lineno + t.contents.count('\n')
|
||||
if comment_lineno_cache == cur_lineno:
|
||||
if t.token_type != TokenType.COMMENT:
|
||||
for c in lineno_comment_map[comment_lineno_cache]:
|
||||
filemsg = ''
|
||||
if origin:
|
||||
filemsg = 'file %s, ' % origin
|
||||
warn_msg = (
|
||||
"The translator-targeted comment '%s' "
|
||||
"(%sline %d) was ignored, because it wasn't "
|
||||
"the last item on the line."
|
||||
) % (c, filemsg, comment_lineno_cache)
|
||||
warnings.warn(warn_msg, TranslatorCommentWarning)
|
||||
lineno_comment_map[comment_lineno_cache] = []
|
||||
else:
|
||||
out.write('# %s' % ' | '.join(lineno_comment_map[comment_lineno_cache]))
|
||||
comment_lineno_cache = None
|
||||
|
||||
if t.token_type == TokenType.BLOCK:
|
||||
imatch = inline_re.match(t.contents)
|
||||
bmatch = block_re.match(t.contents)
|
||||
cmatches = constant_re.findall(t.contents)
|
||||
if imatch:
|
||||
g = imatch.group(1)
|
||||
if g[0] == '"':
|
||||
g = g.strip('"')
|
||||
elif g[0] == "'":
|
||||
g = g.strip("'")
|
||||
g = g.replace('%', '%%')
|
||||
if imatch.group(2):
|
||||
# A context is provided
|
||||
context_match = context_re.match(imatch.group(2))
|
||||
message_context = context_match.group(1)
|
||||
if message_context[0] == '"':
|
||||
message_context = message_context.strip('"')
|
||||
elif message_context[0] == "'":
|
||||
message_context = message_context.strip("'")
|
||||
out.write(' pgettext({p}{!r}, {p}{!r}) '.format(
|
||||
message_context, g, p=raw_prefix
|
||||
))
|
||||
message_context = None
|
||||
else:
|
||||
out.write(' gettext({p}{!r}) '.format(g, p=raw_prefix))
|
||||
elif bmatch:
|
||||
for fmatch in constant_re.findall(t.contents):
|
||||
out.write(' _(%s) ' % fmatch)
|
||||
if bmatch.group(1):
|
||||
# A context is provided
|
||||
context_match = context_re.match(bmatch.group(1))
|
||||
message_context = context_match.group(1)
|
||||
if message_context[0] == '"':
|
||||
message_context = message_context.strip('"')
|
||||
elif message_context[0] == "'":
|
||||
message_context = message_context.strip("'")
|
||||
intrans = True
|
||||
inplural = False
|
||||
trimmed = 'trimmed' in t.split_contents()
|
||||
singular = []
|
||||
plural = []
|
||||
elif cmatches:
|
||||
for cmatch in cmatches:
|
||||
out.write(' _(%s) ' % cmatch)
|
||||
elif t.contents == 'comment':
|
||||
incomment = True
|
||||
else:
|
||||
out.write(blankout(t.contents, 'B'))
|
||||
elif t.token_type == TokenType.VAR:
|
||||
parts = t.contents.split('|')
|
||||
cmatch = constant_re.match(parts[0])
|
||||
if cmatch:
|
||||
out.write(' _(%s) ' % cmatch.group(1))
|
||||
for p in parts[1:]:
|
||||
if p.find(':_(') >= 0:
|
||||
out.write(' %s ' % p.split(':', 1)[1])
|
||||
else:
|
||||
out.write(blankout(p, 'F'))
|
||||
elif t.token_type == TokenType.COMMENT:
|
||||
if t.contents.lstrip().startswith(TRANSLATOR_COMMENT_MARK):
|
||||
lineno_comment_map.setdefault(t.lineno, []).append(t.contents)
|
||||
comment_lineno_cache = t.lineno
|
||||
else:
|
||||
out.write(blankout(t.contents, 'X'))
|
||||
return out.getvalue()
|
||||
@@ -0,0 +1,515 @@
|
||||
"""Translation helper functions."""
|
||||
import functools
|
||||
import gettext as gettext_module
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from threading import local
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.conf.locale import LANG_INFO
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.core.signals import setting_changed
|
||||
from django.dispatch import receiver
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
|
||||
from . import LANGUAGE_SESSION_KEY, to_language, to_locale
|
||||
|
||||
# Translations are cached in a dictionary for every language.
|
||||
# The active translations are stored by threadid to make them thread local.
|
||||
_translations = {}
|
||||
_active = local()
|
||||
|
||||
# The default translation is based on the settings file.
|
||||
_default = None
|
||||
|
||||
# magic gettext number to separate context from message
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
|
||||
# and RFC 3066, section 2.1
|
||||
accept_language_re = re.compile(r'''
|
||||
([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z", "es-419", "*"
|
||||
(?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))? # Optional "q=1.00", "q=0.8"
|
||||
(?:\s*,\s*|$) # Multiple accepts per header.
|
||||
''', re.VERBOSE)
|
||||
|
||||
language_code_re = re.compile(
|
||||
r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
language_code_prefix_re = re.compile(r'^/(\w+([@-]\w+)?)(/|$)')
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
def reset_cache(**kwargs):
|
||||
"""
|
||||
Reset global state when LANGUAGES setting has been changed, as some
|
||||
languages should no longer be accepted.
|
||||
"""
|
||||
if kwargs['setting'] in ('LANGUAGES', 'LANGUAGE_CODE'):
|
||||
check_for_language.cache_clear()
|
||||
get_languages.cache_clear()
|
||||
get_supported_language_variant.cache_clear()
|
||||
|
||||
|
||||
class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
"""
|
||||
Set up the GNUTranslations context with regard to output charset.
|
||||
|
||||
This translation object will be constructed out of multiple GNUTranslations
|
||||
objects by merging their catalogs. It will construct an object for the
|
||||
requested language and add a fallback to the default language, if it's
|
||||
different from the requested language.
|
||||
"""
|
||||
domain = 'django'
|
||||
|
||||
def __init__(self, language, domain=None, localedirs=None):
|
||||
"""Create a GNUTranslations() using many locale directories"""
|
||||
gettext_module.GNUTranslations.__init__(self)
|
||||
if domain is not None:
|
||||
self.domain = domain
|
||||
|
||||
self.__language = language
|
||||
self.__to_language = to_language(language)
|
||||
self.__locale = to_locale(language)
|
||||
self._catalog = None
|
||||
# If a language doesn't have a catalog, use the Germanic default for
|
||||
# pluralization: anything except one is pluralized.
|
||||
self.plural = lambda n: int(n != 1)
|
||||
|
||||
if self.domain == 'django':
|
||||
if localedirs is not None:
|
||||
# A module-level cache is used for caching 'django' translations
|
||||
warnings.warn("localedirs is ignored when domain is 'django'.", RuntimeWarning)
|
||||
localedirs = None
|
||||
self._init_translation_catalog()
|
||||
|
||||
if localedirs:
|
||||
for localedir in localedirs:
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
else:
|
||||
self._add_installed_apps_translations()
|
||||
|
||||
self._add_local_translations()
|
||||
if self.__language == settings.LANGUAGE_CODE and self.domain == 'django' and self._catalog is None:
|
||||
# default lang should have at least one translation file available.
|
||||
raise IOError("No translation files found for default language %s." % settings.LANGUAGE_CODE)
|
||||
self._add_fallback(localedirs)
|
||||
if self._catalog is None:
|
||||
# No catalogs found for this language, set an empty catalog.
|
||||
self._catalog = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<DjangoTranslation lang:%s>" % self.__language
|
||||
|
||||
def _new_gnu_trans(self, localedir, use_null_fallback=True):
|
||||
"""
|
||||
Return a mergeable gettext.GNUTranslations instance.
|
||||
|
||||
A convenience wrapper. By default gettext uses 'fallback=False'.
|
||||
Using param `use_null_fallback` to avoid confusion with any other
|
||||
references to 'fallback'.
|
||||
"""
|
||||
return gettext_module.translation(
|
||||
domain=self.domain,
|
||||
localedir=localedir,
|
||||
languages=[self.__locale],
|
||||
fallback=use_null_fallback,
|
||||
)
|
||||
|
||||
def _init_translation_catalog(self):
|
||||
"""Create a base catalog using global django translations."""
|
||||
settingsfile = sys.modules[settings.__module__].__file__
|
||||
localedir = os.path.join(os.path.dirname(settingsfile), 'locale')
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
def _add_installed_apps_translations(self):
|
||||
"""Merge translations from each installed app."""
|
||||
try:
|
||||
app_configs = reversed(list(apps.get_app_configs()))
|
||||
except AppRegistryNotReady:
|
||||
raise AppRegistryNotReady(
|
||||
"The translation infrastructure cannot be initialized before the "
|
||||
"apps registry is ready. Check that you don't make non-lazy "
|
||||
"gettext calls at import time.")
|
||||
for app_config in app_configs:
|
||||
localedir = os.path.join(app_config.path, 'locale')
|
||||
if os.path.exists(localedir):
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
def _add_local_translations(self):
|
||||
"""Merge translations defined in LOCALE_PATHS."""
|
||||
for localedir in reversed(settings.LOCALE_PATHS):
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
def _add_fallback(self, localedirs=None):
|
||||
"""Set the GNUTranslations() fallback with the default language."""
|
||||
# Don't set a fallback for the default language or any English variant
|
||||
# (as it's empty, so it'll ALWAYS fall back to the default language)
|
||||
if self.__language == settings.LANGUAGE_CODE or self.__language.startswith('en'):
|
||||
return
|
||||
if self.domain == 'django':
|
||||
# Get from cache
|
||||
default_translation = translation(settings.LANGUAGE_CODE)
|
||||
else:
|
||||
default_translation = DjangoTranslation(
|
||||
settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedirs
|
||||
)
|
||||
self.add_fallback(default_translation)
|
||||
|
||||
def merge(self, other):
|
||||
"""Merge another translation into this catalog."""
|
||||
if not getattr(other, '_catalog', None):
|
||||
return # NullTranslations() has no _catalog
|
||||
if self._catalog is None:
|
||||
# Take plural and _info from first catalog found (generally Django's).
|
||||
self.plural = other.plural
|
||||
self._info = other._info.copy()
|
||||
self._catalog = other._catalog.copy()
|
||||
else:
|
||||
self._catalog.update(other._catalog)
|
||||
if other._fallback:
|
||||
self.add_fallback(other._fallback)
|
||||
|
||||
def language(self):
|
||||
"""Return the translation language."""
|
||||
return self.__language
|
||||
|
||||
def to_language(self):
|
||||
"""Return the translation language name."""
|
||||
return self.__to_language
|
||||
|
||||
|
||||
def translation(language):
|
||||
"""
|
||||
Return a translation object in the default 'django' domain.
|
||||
"""
|
||||
global _translations
|
||||
if language not in _translations:
|
||||
_translations[language] = DjangoTranslation(language)
|
||||
return _translations[language]
|
||||
|
||||
|
||||
def activate(language):
|
||||
"""
|
||||
Fetch the translation object for a given language and install it as the
|
||||
current translation object for the current thread.
|
||||
"""
|
||||
if not language:
|
||||
return
|
||||
_active.value = translation(language)
|
||||
|
||||
|
||||
def deactivate():
|
||||
"""
|
||||
Uninstall the active translation object so that further _() calls resolve
|
||||
to the default translation object.
|
||||
"""
|
||||
if hasattr(_active, "value"):
|
||||
del _active.value
|
||||
|
||||
|
||||
def deactivate_all():
|
||||
"""
|
||||
Make the active translation object a NullTranslations() instance. This is
|
||||
useful when we want delayed translations to appear as the original string
|
||||
for some reason.
|
||||
"""
|
||||
_active.value = gettext_module.NullTranslations()
|
||||
_active.value.to_language = lambda *args: None
|
||||
|
||||
|
||||
def get_language():
|
||||
"""Return the currently selected language."""
|
||||
t = getattr(_active, "value", None)
|
||||
if t is not None:
|
||||
try:
|
||||
return t.to_language()
|
||||
except AttributeError:
|
||||
pass
|
||||
# If we don't have a real translation object, assume it's the default language.
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
def get_language_bidi():
|
||||
"""
|
||||
Return selected language's BiDi layout.
|
||||
|
||||
* False = left-to-right layout
|
||||
* True = right-to-left layout
|
||||
"""
|
||||
lang = get_language()
|
||||
if lang is None:
|
||||
return False
|
||||
else:
|
||||
base_lang = get_language().split('-')[0]
|
||||
return base_lang in settings.LANGUAGES_BIDI
|
||||
|
||||
|
||||
def catalog():
|
||||
"""
|
||||
Return the current active catalog for further processing.
|
||||
This can be used if you need to modify the catalog or want to access the
|
||||
whole message catalog instead of just translating one string.
|
||||
"""
|
||||
global _default
|
||||
|
||||
t = getattr(_active, "value", None)
|
||||
if t is not None:
|
||||
return t
|
||||
if _default is None:
|
||||
_default = translation(settings.LANGUAGE_CODE)
|
||||
return _default
|
||||
|
||||
|
||||
def gettext(message):
|
||||
"""
|
||||
Translate the 'message' string. It uses the current thread to find the
|
||||
translation object to use. If no current translation is activated, the
|
||||
message will be run through the default translation object.
|
||||
"""
|
||||
global _default
|
||||
|
||||
eol_message = message.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
if eol_message:
|
||||
_default = _default or translation(settings.LANGUAGE_CODE)
|
||||
translation_object = getattr(_active, "value", _default)
|
||||
|
||||
result = translation_object.gettext(eol_message)
|
||||
else:
|
||||
# Return an empty value of the corresponding type if an empty message
|
||||
# is given, instead of metadata, which is the default gettext behavior.
|
||||
result = type(message)('')
|
||||
|
||||
if isinstance(message, SafeData):
|
||||
return mark_safe(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def pgettext(context, message):
|
||||
msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message)
|
||||
result = gettext(msg_with_ctxt)
|
||||
if CONTEXT_SEPARATOR in result:
|
||||
# Translation not found
|
||||
result = message
|
||||
elif isinstance(message, SafeData):
|
||||
result = mark_safe(result)
|
||||
return result
|
||||
|
||||
|
||||
def gettext_noop(message):
|
||||
"""
|
||||
Mark strings for translation but don't translate them now. This can be
|
||||
used to store strings in global variables that should stay in the base
|
||||
language (because they might be used externally) and will be translated
|
||||
later.
|
||||
"""
|
||||
return message
|
||||
|
||||
|
||||
def do_ntranslate(singular, plural, number, translation_function):
|
||||
global _default
|
||||
|
||||
t = getattr(_active, "value", None)
|
||||
if t is not None:
|
||||
return getattr(t, translation_function)(singular, plural, number)
|
||||
if _default is None:
|
||||
_default = translation(settings.LANGUAGE_CODE)
|
||||
return getattr(_default, translation_function)(singular, plural, number)
|
||||
|
||||
|
||||
def ngettext(singular, plural, number):
|
||||
"""
|
||||
Return a string of the translation of either the singular or plural,
|
||||
based on the number.
|
||||
"""
|
||||
return do_ntranslate(singular, plural, number, 'ngettext')
|
||||
|
||||
|
||||
def npgettext(context, singular, plural, number):
|
||||
msgs_with_ctxt = ("%s%s%s" % (context, CONTEXT_SEPARATOR, singular),
|
||||
"%s%s%s" % (context, CONTEXT_SEPARATOR, plural),
|
||||
number)
|
||||
result = ngettext(*msgs_with_ctxt)
|
||||
if CONTEXT_SEPARATOR in result:
|
||||
# Translation not found
|
||||
result = ngettext(singular, plural, number)
|
||||
return result
|
||||
|
||||
|
||||
def all_locale_paths():
|
||||
"""
|
||||
Return a list of paths to user-provides languages files.
|
||||
"""
|
||||
globalpath = os.path.join(
|
||||
os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
|
||||
app_paths = []
|
||||
for app_config in apps.get_app_configs():
|
||||
locale_path = os.path.join(app_config.path, 'locale')
|
||||
if os.path.exists(locale_path):
|
||||
app_paths.append(locale_path)
|
||||
return [globalpath, *settings.LOCALE_PATHS, *app_paths]
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def check_for_language(lang_code):
|
||||
"""
|
||||
Check whether there is a global language file for the given language
|
||||
code. This is used to decide whether a user-provided language is
|
||||
available.
|
||||
|
||||
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
|
||||
as the provided language codes are taken from the HTTP request. See also
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
# First, a quick check to make sure lang_code is well-formed (#21458)
|
||||
if lang_code is None or not language_code_re.search(lang_code):
|
||||
return False
|
||||
return any(
|
||||
gettext_module.find('django', path, [to_locale(lang_code)]) is not None
|
||||
for path in all_locale_paths()
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_languages():
|
||||
"""
|
||||
Cache of settings.LANGUAGES in an OrderedDict for easy lookups by key.
|
||||
"""
|
||||
return OrderedDict(settings.LANGUAGES)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def get_supported_language_variant(lang_code, strict=False):
|
||||
"""
|
||||
Return the language code that's listed in supported languages, possibly
|
||||
selecting a more generic variant. Raise LookupError if nothing is found.
|
||||
|
||||
If `strict` is False (the default), look for a country-specific variant
|
||||
when neither the language code nor its generic variant is found.
|
||||
|
||||
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
|
||||
as the provided language codes are taken from the HTTP request. See also
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
if lang_code:
|
||||
# If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
|
||||
possible_lang_codes = [lang_code]
|
||||
try:
|
||||
possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
|
||||
except KeyError:
|
||||
pass
|
||||
generic_lang_code = lang_code.split('-')[0]
|
||||
possible_lang_codes.append(generic_lang_code)
|
||||
supported_lang_codes = get_languages()
|
||||
|
||||
for code in possible_lang_codes:
|
||||
if code in supported_lang_codes and check_for_language(code):
|
||||
return code
|
||||
if not strict:
|
||||
# if fr-fr is not supported, try fr-ca.
|
||||
for supported_code in supported_lang_codes:
|
||||
if supported_code.startswith(generic_lang_code + '-'):
|
||||
return supported_code
|
||||
raise LookupError(lang_code)
|
||||
|
||||
|
||||
def get_language_from_path(path, strict=False):
|
||||
"""
|
||||
Return the language code if there's a valid language code found in `path`.
|
||||
|
||||
If `strict` is False (the default), look for a country-specific variant
|
||||
when neither the language code nor its generic variant is found.
|
||||
"""
|
||||
regex_match = language_code_prefix_re.match(path)
|
||||
if not regex_match:
|
||||
return None
|
||||
lang_code = regex_match.group(1)
|
||||
try:
|
||||
return get_supported_language_variant(lang_code, strict=strict)
|
||||
except LookupError:
|
||||
return None
|
||||
|
||||
|
||||
def get_language_from_request(request, check_path=False):
|
||||
"""
|
||||
Analyze the request to find what language the user wants the system to
|
||||
show. Only languages listed in settings.LANGUAGES are taken into account.
|
||||
If the user requests a sublanguage where we have a main language, we send
|
||||
out the main language.
|
||||
|
||||
If check_path is True, the URL path prefix will be checked for a language
|
||||
code, otherwise this is skipped for backwards compatibility.
|
||||
"""
|
||||
if check_path:
|
||||
lang_code = get_language_from_path(request.path_info)
|
||||
if lang_code is not None:
|
||||
return lang_code
|
||||
|
||||
supported_lang_codes = get_languages()
|
||||
|
||||
if hasattr(request, 'session'):
|
||||
lang_code = request.session.get(LANGUAGE_SESSION_KEY)
|
||||
if lang_code in supported_lang_codes and lang_code is not None and check_for_language(lang_code):
|
||||
return lang_code
|
||||
|
||||
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
try:
|
||||
return get_supported_language_variant(lang_code)
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
|
||||
for accept_lang, unused in parse_accept_lang_header(accept):
|
||||
if accept_lang == '*':
|
||||
break
|
||||
|
||||
if not language_code_re.search(accept_lang):
|
||||
continue
|
||||
|
||||
try:
|
||||
return get_supported_language_variant(accept_lang)
|
||||
except LookupError:
|
||||
continue
|
||||
|
||||
try:
|
||||
return get_supported_language_variant(settings.LANGUAGE_CODE)
|
||||
except LookupError:
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def parse_accept_lang_header(lang_string):
|
||||
"""
|
||||
Parse the lang_string, which is the body of an HTTP Accept-Language
|
||||
header, and return a tuple of (lang, q-value), ordered by 'q' values.
|
||||
|
||||
Return an empty tuple if there are any format errors in lang_string.
|
||||
"""
|
||||
result = []
|
||||
pieces = accept_language_re.split(lang_string.lower())
|
||||
if pieces[-1]:
|
||||
return ()
|
||||
for i in range(0, len(pieces) - 1, 3):
|
||||
first, lang, priority = pieces[i:i + 3]
|
||||
if first:
|
||||
return ()
|
||||
if priority:
|
||||
priority = float(priority)
|
||||
else:
|
||||
priority = 1.0
|
||||
result.append((lang, priority))
|
||||
result.sort(key=lambda k: k[1], reverse=True)
|
||||
return tuple(result)
|
||||
@@ -0,0 +1,104 @@
|
||||
import datetime
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
# Private, stable API for detecting the Python version. PYXY means "Python X.Y
|
||||
# or later". So that third-party apps can use these values, each constant
|
||||
# should remain as long as the oldest supported Django version supports that
|
||||
# Python version.
|
||||
PY36 = sys.version_info >= (3, 6)
|
||||
PY37 = sys.version_info >= (3, 7)
|
||||
PY38 = sys.version_info >= (3, 8)
|
||||
PY39 = sys.version_info >= (3, 9)
|
||||
|
||||
|
||||
def get_version(version=None):
|
||||
"""Return a PEP 440-compliant version number from VERSION."""
|
||||
version = get_complete_version(version)
|
||||
|
||||
# Now build the two parts of the version number:
|
||||
# main = X.Y[.Z]
|
||||
# sub = .devN - for pre-alpha releases
|
||||
# | {a|b|rc}N - for alpha, beta, and rc releases
|
||||
|
||||
main = get_main_version(version)
|
||||
|
||||
sub = ''
|
||||
if version[3] == 'alpha' and version[4] == 0:
|
||||
git_changeset = get_git_changeset()
|
||||
if git_changeset:
|
||||
sub = '.dev%s' % git_changeset
|
||||
|
||||
elif version[3] != 'final':
|
||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
|
||||
sub = mapping[version[3]] + str(version[4])
|
||||
|
||||
return main + sub
|
||||
|
||||
|
||||
def get_main_version(version=None):
|
||||
"""Return main version (X.Y[.Z]) from VERSION."""
|
||||
version = get_complete_version(version)
|
||||
parts = 2 if version[2] == 0 else 3
|
||||
return '.'.join(str(x) for x in version[:parts])
|
||||
|
||||
|
||||
def get_complete_version(version=None):
|
||||
"""
|
||||
Return a tuple of the django version. If version argument is non-empty,
|
||||
check for correctness of the tuple provided.
|
||||
"""
|
||||
if version is None:
|
||||
from django import VERSION as version
|
||||
else:
|
||||
assert len(version) == 5
|
||||
assert version[3] in ('alpha', 'beta', 'rc', 'final')
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def get_docs_version(version=None):
|
||||
version = get_complete_version(version)
|
||||
if version[3] != 'final':
|
||||
return 'dev'
|
||||
else:
|
||||
return '%d.%d' % version[:2]
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_git_changeset():
|
||||
"""Return a numeric identifier of the latest git changeset.
|
||||
|
||||
The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format.
|
||||
This value isn't guaranteed to be unique, but collisions are very unlikely,
|
||||
so it's sufficient for generating the development version numbers.
|
||||
"""
|
||||
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
git_log = subprocess.Popen(
|
||||
'git log --pretty=format:%ct --quiet -1 HEAD',
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
shell=True, cwd=repo_dir, universal_newlines=True,
|
||||
)
|
||||
timestamp = git_log.communicate()[0]
|
||||
try:
|
||||
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
|
||||
except ValueError:
|
||||
return None
|
||||
return timestamp.strftime('%Y%m%d%H%M%S')
|
||||
|
||||
|
||||
def get_version_tuple(version):
|
||||
"""
|
||||
Return a tuple of version numbers (e.g. (1, 2, 3)) from the version
|
||||
string (e.g. '1.2.3').
|
||||
"""
|
||||
loose_version = LooseVersion(version)
|
||||
version_numbers = []
|
||||
for item in loose_version.version:
|
||||
if not isinstance(item, int):
|
||||
break
|
||||
version_numbers.append(item)
|
||||
return tuple(version_numbers)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Utilities for XML generation/parsing.
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from xml.sax.saxutils import XMLGenerator
|
||||
|
||||
|
||||
class UnserializableContentError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class SimplerXMLGenerator(XMLGenerator):
|
||||
def addQuickElement(self, name, contents=None, attrs=None):
|
||||
"Convenience method for adding an element with no children"
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
self.startElement(name, attrs)
|
||||
if contents is not None:
|
||||
self.characters(contents)
|
||||
self.endElement(name)
|
||||
|
||||
def characters(self, content):
|
||||
if content and re.search(r'[\x00-\x08\x0B-\x0C\x0E-\x1F]', content):
|
||||
# Fail loudly when content has control chars (unsupported in XML 1.0)
|
||||
# See https://www.w3.org/International/questions/qa-controls
|
||||
raise UnserializableContentError("Control characters are not supported in XML 1.0")
|
||||
XMLGenerator.characters(self, content)
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
# Sort attrs for a deterministic output.
|
||||
sorted_attrs = OrderedDict(sorted(attrs.items())) if attrs else attrs
|
||||
super().startElement(name, sorted_attrs)
|
||||
Reference in New Issue
Block a user