测试gitnore
This commit is contained in:
@@ -23,15 +23,12 @@ def safe_join(base, *paths):
|
||||
# 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)
|
||||
):
|
||||
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)
|
||||
)
|
||||
'The joined path ({}) is located outside of the base path '
|
||||
'component ({})'.format(final_path, base_path))
|
||||
return final_path
|
||||
|
||||
|
||||
@@ -42,8 +39,8 @@ def symlinks_supported():
|
||||
permissions).
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
original_path = os.path.join(temp_dir, "original")
|
||||
symlink_path = os.path.join(temp_dir, "symlink")
|
||||
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)
|
||||
@@ -58,5 +55,5 @@ def to_path(value):
|
||||
if isinstance(value, Path):
|
||||
return value
|
||||
elif not isinstance(value, str):
|
||||
raise TypeError("Invalid path type: %s" % type(value).__name__)
|
||||
raise TypeError('Invalid path type: %s' % type(value).__name__)
|
||||
return Path(value)
|
||||
|
||||
@@ -55,7 +55,6 @@ class Archive:
|
||||
"""
|
||||
The external API class that encapsulates an archive implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, file):
|
||||
self._archive = self._archive_cls(file)(file)
|
||||
|
||||
@@ -69,8 +68,7 @@ class Archive:
|
||||
filename = file.name
|
||||
except AttributeError:
|
||||
raise UnrecognizedArchiveFormat(
|
||||
"File object not a recognized archive format."
|
||||
)
|
||||
"File object not a recognized archive format.")
|
||||
base, tail_ext = os.path.splitext(filename.lower())
|
||||
cls = extension_map.get(tail_ext)
|
||||
if not cls:
|
||||
@@ -78,8 +76,7 @@ class Archive:
|
||||
cls = extension_map.get(ext)
|
||||
if not cls:
|
||||
raise UnrecognizedArchiveFormat(
|
||||
"Path not a recognized archive format: %s" % filename
|
||||
)
|
||||
"Path not a recognized archive format: %s" % filename)
|
||||
return cls
|
||||
|
||||
def __enter__(self):
|
||||
@@ -102,7 +99,6 @@ class BaseArchive:
|
||||
"""
|
||||
Base Archive class. Implementations should inherit this class.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _copy_permissions(mode, filename):
|
||||
"""
|
||||
@@ -115,15 +111,13 @@ class BaseArchive:
|
||||
|
||||
def split_leading_dir(self, path):
|
||||
path = str(path)
|
||||
path = path.lstrip("/").lstrip("\\")
|
||||
if "/" in path and (
|
||||
("\\" in path and path.find("/") < path.find("\\")) or "\\" not in path
|
||||
):
|
||||
return path.split("/", 1)
|
||||
elif "\\" in path:
|
||||
return path.split("\\", 1)
|
||||
path = path.lstrip('/').lstrip('\\')
|
||||
if '/' in path and (('\\' in path and path.find('/') < path.find('\\')) or '\\' not in path):
|
||||
return path.split('/', 1)
|
||||
elif '\\' in path:
|
||||
return path.split('\\', 1)
|
||||
else:
|
||||
return path, ""
|
||||
return path, ''
|
||||
|
||||
def has_leading_dir(self, paths):
|
||||
"""
|
||||
@@ -149,17 +143,14 @@ class BaseArchive:
|
||||
return filename
|
||||
|
||||
def extract(self):
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseArchive must provide an extract() method"
|
||||
)
|
||||
raise NotImplementedError('subclasses of BaseArchive must provide an extract() method')
|
||||
|
||||
def list(self):
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseArchive must provide a list() method"
|
||||
)
|
||||
raise NotImplementedError('subclasses of BaseArchive must provide a list() method')
|
||||
|
||||
|
||||
class TarArchive(BaseArchive):
|
||||
|
||||
def __init__(self, file):
|
||||
self._archive = tarfile.open(file)
|
||||
|
||||
@@ -183,15 +174,13 @@ class TarArchive(BaseArchive):
|
||||
except (KeyError, AttributeError) as exc:
|
||||
# Some corrupt tar files seem to produce this
|
||||
# (specifically bad symlinks)
|
||||
print(
|
||||
"In the tar file %s the member %s is invalid: %s"
|
||||
% (name, member.name, exc)
|
||||
)
|
||||
print("In the tar file %s the member %s is invalid: %s" %
|
||||
(name, member.name, exc))
|
||||
else:
|
||||
dirname = os.path.dirname(filename)
|
||||
if dirname:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
with open(filename, "wb") as outfile:
|
||||
with open(filename, 'wb') as outfile:
|
||||
shutil.copyfileobj(extracted, outfile)
|
||||
self._copy_permissions(member.mode, filename)
|
||||
finally:
|
||||
@@ -203,6 +192,7 @@ class TarArchive(BaseArchive):
|
||||
|
||||
|
||||
class ZipArchive(BaseArchive):
|
||||
|
||||
def __init__(self, file):
|
||||
self._archive = zipfile.ZipFile(file)
|
||||
|
||||
@@ -220,14 +210,14 @@ class ZipArchive(BaseArchive):
|
||||
if not name:
|
||||
continue
|
||||
filename = self.target_filename(to_path, name)
|
||||
if name.endswith(("/", "\\")):
|
||||
if name.endswith(('/', '\\')):
|
||||
# A directory
|
||||
os.makedirs(filename, exist_ok=True)
|
||||
else:
|
||||
dirname = os.path.dirname(filename)
|
||||
if dirname:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
with open(filename, "wb") as outfile:
|
||||
with open(filename, 'wb') as outfile:
|
||||
outfile.write(data)
|
||||
# Convert ZipInfo.external_attr to mode
|
||||
mode = info.external_attr >> 16
|
||||
@@ -237,21 +227,11 @@ class ZipArchive(BaseArchive):
|
||||
self._archive.close()
|
||||
|
||||
|
||||
extension_map = dict.fromkeys(
|
||||
(
|
||||
".tar",
|
||||
".tar.bz2",
|
||||
".tbz2",
|
||||
".tbz",
|
||||
".tz2",
|
||||
".tar.gz",
|
||||
".tgz",
|
||||
".taz",
|
||||
".tar.lzma",
|
||||
".tlz",
|
||||
".tar.xz",
|
||||
".txz",
|
||||
),
|
||||
TarArchive,
|
||||
)
|
||||
extension_map[".zip"] = ZipArchive
|
||||
extension_map = dict.fromkeys((
|
||||
'.tar',
|
||||
'.tar.bz2', '.tbz2', '.tbz', '.tz2',
|
||||
'.tar.gz', '.tgz', '.taz',
|
||||
'.tar.lzma', '.tlz',
|
||||
'.tar.xz', '.txz',
|
||||
), TarArchive)
|
||||
extension_map['.zip'] = ZipArchive
|
||||
|
||||
@@ -10,30 +10,25 @@ def async_unsafe(message):
|
||||
Decorator to mark functions as async-unsafe. Someone trying to access
|
||||
the function while in an async context will get an error message.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
if not os.environ.get("DJANGO_ALLOW_ASYNC_UNSAFE"):
|
||||
if not os.environ.get('DJANGO_ALLOW_ASYNC_UNSAFE'):
|
||||
# Detect a running event loop in this thread.
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
event_loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
raise SynchronousOnlyOperation(message)
|
||||
# Pass onward.
|
||||
if event_loop.is_running():
|
||||
raise SynchronousOnlyOperation(message)
|
||||
# Pass onwards.
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
# If the message is actually a function, then be a no-arguments decorator.
|
||||
if callable(message):
|
||||
func = message
|
||||
message = (
|
||||
"You cannot call this from an async context - use a thread or "
|
||||
"sync_to_async."
|
||||
)
|
||||
message = 'You cannot call this from an async context - use a thread or sync_to_async.'
|
||||
return decorator(func)
|
||||
else:
|
||||
return decorator
|
||||
|
||||
@@ -24,9 +24,9 @@ from django.utils.version import get_version_tuple
|
||||
autoreload_started = Signal()
|
||||
file_changed = Signal()
|
||||
|
||||
DJANGO_AUTORELOAD_ENV = "RUN_MAIN"
|
||||
DJANGO_AUTORELOAD_ENV = 'RUN_MAIN'
|
||||
|
||||
logger = logging.getLogger("django.utils.autoreload")
|
||||
logger = logging.getLogger('django.utils.autoreload')
|
||||
|
||||
# If an error is raised while importing a file, it's not placed in sys.modules.
|
||||
# This means that any future modifications aren't caught. Keep a list of these
|
||||
@@ -48,7 +48,7 @@ except ImportError:
|
||||
|
||||
def is_django_module(module):
|
||||
"""Return True if the given module is nested under Django."""
|
||||
return module.__name__.startswith("django.")
|
||||
return module.__name__.startswith('django.')
|
||||
|
||||
|
||||
def is_django_path(path):
|
||||
@@ -67,7 +67,7 @@ def check_errors(fn):
|
||||
|
||||
et, ev, tb = _exception
|
||||
|
||||
if getattr(ev, "filename", None) is None:
|
||||
if getattr(ev, 'filename', None) is None:
|
||||
# get the filename from the last item in the stack
|
||||
filename = traceback.extract_tb(tb)[-1][0]
|
||||
else:
|
||||
@@ -97,7 +97,7 @@ def ensure_echo_on():
|
||||
attr_list = termios.tcgetattr(sys.stdin)
|
||||
if not attr_list[3] & termios.ECHO:
|
||||
attr_list[3] |= termios.ECHO
|
||||
if hasattr(signal, "SIGTTOU"):
|
||||
if hasattr(signal, 'SIGTTOU'):
|
||||
old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
||||
else:
|
||||
old_handler = None
|
||||
@@ -112,11 +112,7 @@ def iter_all_python_module_files():
|
||||
# This ensures cached results are returned in the usual case that modules
|
||||
# aren't loaded on the fly.
|
||||
keys = sorted(sys.modules)
|
||||
modules = tuple(
|
||||
m
|
||||
for m in map(sys.modules.__getitem__, keys)
|
||||
if not isinstance(m, weakref.ProxyTypes)
|
||||
)
|
||||
modules = tuple(m for m in map(sys.modules.__getitem__, keys) if not isinstance(m, weakref.ProxyTypes))
|
||||
return iter_modules_and_files(modules, frozenset(_error_files))
|
||||
|
||||
|
||||
@@ -130,25 +126,21 @@ def iter_modules_and_files(modules, extra_files):
|
||||
# cause issues here.
|
||||
if not isinstance(module, ModuleType):
|
||||
continue
|
||||
if module.__name__ == "__main__":
|
||||
if module.__name__ == '__main__':
|
||||
# __main__ (usually manage.py) doesn't always have a __spec__ set.
|
||||
# Handle this by falling back to using __file__, resolved below.
|
||||
# See https://docs.python.org/reference/import.html#main-spec
|
||||
# __file__ may not exists, e.g. when running ipdb debugger.
|
||||
if hasattr(module, "__file__"):
|
||||
if hasattr(module, '__file__'):
|
||||
sys_file_paths.append(module.__file__)
|
||||
continue
|
||||
if getattr(module, "__spec__", None) is None:
|
||||
if getattr(module, '__spec__', None) is None:
|
||||
continue
|
||||
spec = module.__spec__
|
||||
# Modules could be loaded from places without a concrete location. If
|
||||
# this is the case, skip them.
|
||||
if spec.has_location:
|
||||
origin = (
|
||||
spec.loader.archive
|
||||
if isinstance(spec.loader, zipimporter)
|
||||
else spec.origin
|
||||
)
|
||||
origin = spec.loader.archive if isinstance(spec.loader, zipimporter) else spec.origin
|
||||
sys_file_paths.append(origin)
|
||||
|
||||
results = set()
|
||||
@@ -225,50 +217,43 @@ def get_child_arguments():
|
||||
on reloading.
|
||||
"""
|
||||
import __main__
|
||||
|
||||
py_script = Path(sys.argv[0])
|
||||
|
||||
args = [sys.executable] + ["-W%s" % o for o in sys.warnoptions]
|
||||
if sys.implementation.name == "cpython":
|
||||
args.extend(
|
||||
f"-X{key}" if value is True else f"-X{key}={value}"
|
||||
for key, value in sys._xoptions.items()
|
||||
)
|
||||
args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions]
|
||||
# __spec__ is set when the server was started with the `-m` option,
|
||||
# see https://docs.python.org/3/reference/import.html#main-spec
|
||||
# __spec__ may not exist, e.g. when running in a Conda env.
|
||||
if getattr(__main__, "__spec__", None) is not None:
|
||||
spec = __main__.__spec__
|
||||
if (spec.name == "__main__" or spec.name.endswith(".__main__")) and spec.parent:
|
||||
name = spec.parent
|
||||
else:
|
||||
name = spec.name
|
||||
args += ["-m", name]
|
||||
if getattr(__main__, '__spec__', None) is not None and __main__.__spec__.parent:
|
||||
args += ['-m', __main__.__spec__.parent]
|
||||
args += sys.argv[1:]
|
||||
elif not py_script.exists():
|
||||
# sys.argv[0] may not exist for several reasons on Windows.
|
||||
# It may exist with a .exe extension or have a -script.py suffix.
|
||||
exe_entrypoint = py_script.with_suffix(".exe")
|
||||
exe_entrypoint = py_script.with_suffix('.exe')
|
||||
if exe_entrypoint.exists():
|
||||
# Should be executed directly, ignoring sys.executable.
|
||||
return [exe_entrypoint, *sys.argv[1:]]
|
||||
script_entrypoint = py_script.with_name("%s-script.py" % py_script.name)
|
||||
# TODO: Remove str() when dropping support for PY37.
|
||||
# args parameter accepts path-like on Windows from Python 3.8.
|
||||
return [str(exe_entrypoint), *sys.argv[1:]]
|
||||
script_entrypoint = py_script.with_name('%s-script.py' % py_script.name)
|
||||
if script_entrypoint.exists():
|
||||
# Should be executed as usual.
|
||||
return [*args, script_entrypoint, *sys.argv[1:]]
|
||||
raise RuntimeError("Script %s does not exist." % py_script)
|
||||
# TODO: Remove str() when dropping support for PY37.
|
||||
# args parameter accepts path-like on Windows from Python 3.8.
|
||||
return [*args, str(script_entrypoint), *sys.argv[1:]]
|
||||
raise RuntimeError('Script %s does not exist.' % py_script)
|
||||
else:
|
||||
args += sys.argv
|
||||
return args
|
||||
|
||||
|
||||
def trigger_reload(filename):
|
||||
logger.info("%s changed, reloading.", filename)
|
||||
logger.info('%s changed, reloading.', filename)
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
|
||||
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: 'true'}
|
||||
args = get_child_arguments()
|
||||
while True:
|
||||
p = subprocess.run(args, env=new_environ, close_fds=False)
|
||||
@@ -288,12 +273,12 @@ class BaseReloader:
|
||||
path = path.absolute()
|
||||
except FileNotFoundError:
|
||||
logger.debug(
|
||||
"Unable to watch directory %s as it cannot be resolved.",
|
||||
'Unable to watch directory %s as it cannot be resolved.',
|
||||
path,
|
||||
exc_info=True,
|
||||
)
|
||||
return
|
||||
logger.debug("Watching dir %s with glob %s.", path, glob)
|
||||
logger.debug('Watching dir %s with glob %s.', path, glob)
|
||||
self.directory_globs[path].add(glob)
|
||||
|
||||
def watched_files(self, include_globs=True):
|
||||
@@ -323,11 +308,11 @@ class BaseReloader:
|
||||
if app_reg.ready_event.wait(timeout=0.1):
|
||||
return True
|
||||
else:
|
||||
logger.debug("Main Django thread has terminated before apps are ready.")
|
||||
logger.debug('Main Django thread has terminated before apps are ready.')
|
||||
return False
|
||||
|
||||
def run(self, django_main_thread):
|
||||
logger.debug("Waiting for apps ready_event.")
|
||||
logger.debug('Waiting for apps ready_event.')
|
||||
self.wait_for_apps_ready(apps, django_main_thread)
|
||||
from django.urls import get_resolver
|
||||
|
||||
@@ -339,7 +324,7 @@ class BaseReloader:
|
||||
# Loading the urlconf can result in errors during development.
|
||||
# If this occurs then swallow the error and continue.
|
||||
pass
|
||||
logger.debug("Apps ready_event triggered. Sending autoreload_started signal.")
|
||||
logger.debug('Apps ready_event triggered. Sending autoreload_started signal.')
|
||||
autoreload_started.send(sender=self)
|
||||
self.run_loop()
|
||||
|
||||
@@ -360,15 +345,15 @@ class BaseReloader:
|
||||
testability of the reloader implementations by decoupling the work they
|
||||
do from the loop.
|
||||
"""
|
||||
raise NotImplementedError("subclasses must implement tick().")
|
||||
raise NotImplementedError('subclasses must implement tick().')
|
||||
|
||||
@classmethod
|
||||
def check_availability(cls):
|
||||
raise NotImplementedError("subclasses must implement check_availability().")
|
||||
raise NotImplementedError('subclasses must implement check_availability().')
|
||||
|
||||
def notify_file_changed(self, path):
|
||||
results = file_changed.send(sender=self, file_path=path)
|
||||
logger.debug("%s notified as changed. Signal results: %s.", path, results)
|
||||
logger.debug('%s notified as changed. Signal results: %s.', path, results)
|
||||
if not any(res[1] for res in results):
|
||||
trigger_reload(path)
|
||||
|
||||
@@ -391,15 +376,10 @@ class StatReloader(BaseReloader):
|
||||
old_time = mtimes.get(filepath)
|
||||
mtimes[filepath] = mtime
|
||||
if old_time is None:
|
||||
logger.debug("File %s first seen with mtime %s", filepath, mtime)
|
||||
logger.debug('File %s first seen with mtime %s', filepath, mtime)
|
||||
continue
|
||||
elif mtime > old_time:
|
||||
logger.debug(
|
||||
"File %s previous mtime: %s, current mtime: %s",
|
||||
filepath,
|
||||
old_time,
|
||||
mtime,
|
||||
)
|
||||
logger.debug('File %s previous mtime: %s, current mtime: %s', filepath, old_time, mtime)
|
||||
self.notify_file_changed(filepath)
|
||||
|
||||
time.sleep(self.SLEEP_TIME)
|
||||
@@ -432,7 +412,7 @@ class WatchmanReloader(BaseReloader):
|
||||
def __init__(self):
|
||||
self.roots = defaultdict(set)
|
||||
self.processed_request = threading.Event()
|
||||
self.client_timeout = int(os.environ.get("DJANGO_WATCHMAN_TIMEOUT", 5))
|
||||
self.client_timeout = int(os.environ.get('DJANGO_WATCHMAN_TIMEOUT', 5))
|
||||
super().__init__()
|
||||
|
||||
@cached_property
|
||||
@@ -451,63 +431,52 @@ class WatchmanReloader(BaseReloader):
|
||||
# now, watching its parent, if possible, is sufficient.
|
||||
if not root.exists():
|
||||
if not root.parent.exists():
|
||||
logger.warning(
|
||||
"Unable to watch root dir %s as neither it or its parent exist.",
|
||||
root,
|
||||
)
|
||||
logger.warning('Unable to watch root dir %s as neither it or its parent exist.', root)
|
||||
return
|
||||
root = root.parent
|
||||
result = self.client.query("watch-project", str(root.absolute()))
|
||||
if "warning" in result:
|
||||
logger.warning("Watchman warning: %s", result["warning"])
|
||||
logger.debug("Watchman watch-project result: %s", result)
|
||||
return result["watch"], result.get("relative_path")
|
||||
result = self.client.query('watch-project', str(root.absolute()))
|
||||
if 'warning' in result:
|
||||
logger.warning('Watchman warning: %s', result['warning'])
|
||||
logger.debug('Watchman watch-project result: %s', result)
|
||||
return result['watch'], result.get('relative_path')
|
||||
|
||||
@functools.lru_cache()
|
||||
def _get_clock(self, root):
|
||||
return self.client.query("clock", root)["clock"]
|
||||
return self.client.query('clock', root)['clock']
|
||||
|
||||
def _subscribe(self, directory, name, expression):
|
||||
root, rel_path = self._watch_root(directory)
|
||||
# Only receive notifications of files changing, filtering out other types
|
||||
# like special files: https://facebook.github.io/watchman/docs/type
|
||||
only_files_expression = [
|
||||
"allof",
|
||||
["anyof", ["type", "f"], ["type", "l"]],
|
||||
expression,
|
||||
'allof',
|
||||
['anyof', ['type', 'f'], ['type', 'l']],
|
||||
expression
|
||||
]
|
||||
query = {
|
||||
"expression": only_files_expression,
|
||||
"fields": ["name"],
|
||||
"since": self._get_clock(root),
|
||||
"dedup_results": True,
|
||||
'expression': only_files_expression,
|
||||
'fields': ['name'],
|
||||
'since': self._get_clock(root),
|
||||
'dedup_results': True,
|
||||
}
|
||||
if rel_path:
|
||||
query["relative_root"] = rel_path
|
||||
logger.debug(
|
||||
"Issuing watchman subscription %s, for root %s. Query: %s",
|
||||
name,
|
||||
root,
|
||||
query,
|
||||
)
|
||||
self.client.query("subscribe", root, name, query)
|
||||
query['relative_root'] = rel_path
|
||||
logger.debug('Issuing watchman subscription %s, for root %s. Query: %s', name, root, query)
|
||||
self.client.query('subscribe', root, name, query)
|
||||
|
||||
def _subscribe_dir(self, directory, filenames):
|
||||
if not directory.exists():
|
||||
if not directory.parent.exists():
|
||||
logger.warning(
|
||||
"Unable to watch directory %s as neither it or its parent exist.",
|
||||
directory,
|
||||
)
|
||||
logger.warning('Unable to watch directory %s as neither it or its parent exist.', directory)
|
||||
return
|
||||
prefix = "files-parent-%s" % directory.name
|
||||
filenames = ["%s/%s" % (directory.name, filename) for filename in filenames]
|
||||
prefix = 'files-parent-%s' % directory.name
|
||||
filenames = ['%s/%s' % (directory.name, filename) for filename in filenames]
|
||||
directory = directory.parent
|
||||
expression = ["name", filenames, "wholename"]
|
||||
expression = ['name', filenames, 'wholename']
|
||||
else:
|
||||
prefix = "files"
|
||||
expression = ["name", filenames]
|
||||
self._subscribe(directory, "%s:%s" % (prefix, directory), expression)
|
||||
prefix = 'files'
|
||||
expression = ['name', filenames]
|
||||
self._subscribe(directory, '%s:%s' % (prefix, directory), expression)
|
||||
|
||||
def _watch_glob(self, directory, patterns):
|
||||
"""
|
||||
@@ -518,22 +487,19 @@ class WatchmanReloader(BaseReloader):
|
||||
overwrite the named subscription, so it must include all possible glob
|
||||
expressions.
|
||||
"""
|
||||
prefix = "glob"
|
||||
prefix = 'glob'
|
||||
if not directory.exists():
|
||||
if not directory.parent.exists():
|
||||
logger.warning(
|
||||
"Unable to watch directory %s as neither it or its parent exist.",
|
||||
directory,
|
||||
)
|
||||
logger.warning('Unable to watch directory %s as neither it or its parent exist.', directory)
|
||||
return
|
||||
prefix = "glob-parent-%s" % directory.name
|
||||
patterns = ["%s/%s" % (directory.name, pattern) for pattern in patterns]
|
||||
prefix = 'glob-parent-%s' % directory.name
|
||||
patterns = ['%s/%s' % (directory.name, pattern) for pattern in patterns]
|
||||
directory = directory.parent
|
||||
|
||||
expression = ["anyof"]
|
||||
expression = ['anyof']
|
||||
for pattern in patterns:
|
||||
expression.append(["match", pattern, "wholename"])
|
||||
self._subscribe(directory, "%s:%s" % (prefix, directory), expression)
|
||||
expression.append(['match', pattern, 'wholename'])
|
||||
self._subscribe(directory, '%s:%s' % (prefix, directory), expression)
|
||||
|
||||
def watched_roots(self, watched_files):
|
||||
extra_directories = self.directory_globs.keys()
|
||||
@@ -544,8 +510,8 @@ class WatchmanReloader(BaseReloader):
|
||||
def _update_watches(self):
|
||||
watched_files = list(self.watched_files(include_globs=False))
|
||||
found_roots = common_roots(self.watched_roots(watched_files))
|
||||
logger.debug("Watching %s files", len(watched_files))
|
||||
logger.debug("Found common roots: %s", found_roots)
|
||||
logger.debug('Watching %s files', len(watched_files))
|
||||
logger.debug('Found common roots: %s', found_roots)
|
||||
# Setup initial roots for performance, shortest roots first.
|
||||
for root in sorted(found_roots):
|
||||
self._watch_root(root)
|
||||
@@ -555,9 +521,7 @@ class WatchmanReloader(BaseReloader):
|
||||
sorted_files = sorted(watched_files, key=lambda p: p.parent)
|
||||
for directory, group in itertools.groupby(sorted_files, key=lambda p: p.parent):
|
||||
# These paths need to be relative to the parent directory.
|
||||
self._subscribe_dir(
|
||||
directory, [str(p.relative_to(directory)) for p in group]
|
||||
)
|
||||
self._subscribe_dir(directory, [str(p.relative_to(directory)) for p in group])
|
||||
|
||||
def update_watches(self):
|
||||
try:
|
||||
@@ -571,19 +535,19 @@ class WatchmanReloader(BaseReloader):
|
||||
subscription = self.client.getSubscription(sub)
|
||||
if not subscription:
|
||||
return
|
||||
logger.debug("Watchman subscription %s has results.", sub)
|
||||
logger.debug('Watchman subscription %s has results.', sub)
|
||||
for result in subscription:
|
||||
# When using watch-project, it's not simple to get the relative
|
||||
# directory without storing some specific state. Store the full
|
||||
# path to the directory in the subscription name, prefixed by its
|
||||
# type (glob, files).
|
||||
root_directory = Path(result["subscription"].split(":", 1)[1])
|
||||
logger.debug("Found root directory %s", root_directory)
|
||||
for file in result.get("files", []):
|
||||
root_directory = Path(result['subscription'].split(':', 1)[1])
|
||||
logger.debug('Found root directory %s', root_directory)
|
||||
for file in result.get('files', []):
|
||||
self.notify_file_changed(root_directory / file)
|
||||
|
||||
def request_processed(self, **kwargs):
|
||||
logger.debug("Request processed. Setting update_watches event.")
|
||||
logger.debug('Request processed. Setting update_watches event.')
|
||||
self.processed_request.set()
|
||||
|
||||
def tick(self):
|
||||
@@ -598,7 +562,7 @@ class WatchmanReloader(BaseReloader):
|
||||
except pywatchman.SocketTimeout:
|
||||
pass
|
||||
except pywatchman.WatchmanError as ex:
|
||||
logger.debug("Watchman error: %s, checking server status.", ex)
|
||||
logger.debug('Watchman error: %s, checking server status.', ex)
|
||||
self.check_server_status(ex)
|
||||
else:
|
||||
for sub in list(self.client.subs.keys()):
|
||||
@@ -614,7 +578,7 @@ class WatchmanReloader(BaseReloader):
|
||||
def check_server_status(self, inner_ex=None):
|
||||
"""Return True if the server is available."""
|
||||
try:
|
||||
self.client.query("version")
|
||||
self.client.query('version')
|
||||
except Exception:
|
||||
raise WatchmanUnavailable(str(inner_ex)) from inner_ex
|
||||
return True
|
||||
@@ -622,19 +586,19 @@ class WatchmanReloader(BaseReloader):
|
||||
@classmethod
|
||||
def check_availability(cls):
|
||||
if not pywatchman:
|
||||
raise WatchmanUnavailable("pywatchman not installed.")
|
||||
raise WatchmanUnavailable('pywatchman not installed.')
|
||||
client = pywatchman.client(timeout=0.1)
|
||||
try:
|
||||
result = client.capabilityCheck()
|
||||
except Exception:
|
||||
# The service is down?
|
||||
raise WatchmanUnavailable("Cannot connect to the watchman service.")
|
||||
version = get_version_tuple(result["version"])
|
||||
raise WatchmanUnavailable('Cannot connect to the watchman service.')
|
||||
version = get_version_tuple(result['version'])
|
||||
# Watchman 4.9 includes multiple improvements to watching project
|
||||
# directories as well as case insensitive filesystems.
|
||||
logger.debug("Watchman version %s", version)
|
||||
logger.debug('Watchman version %s', version)
|
||||
if version < (4, 9):
|
||||
raise WatchmanUnavailable("Watchman 4.9 or later is required.")
|
||||
raise WatchmanUnavailable('Watchman 4.9 or later is required.')
|
||||
|
||||
|
||||
def get_reloader():
|
||||
@@ -650,10 +614,8 @@ def start_django(reloader, main_func, *args, **kwargs):
|
||||
ensure_echo_on()
|
||||
|
||||
main_func = check_errors(main_func)
|
||||
django_main_thread = threading.Thread(
|
||||
target=main_func, args=args, kwargs=kwargs, name="django-main-thread"
|
||||
)
|
||||
django_main_thread.daemon = True
|
||||
django_main_thread = threading.Thread(target=main_func, args=args, kwargs=kwargs, name='django-main-thread')
|
||||
django_main_thread.setDaemon(True)
|
||||
django_main_thread.start()
|
||||
|
||||
while not reloader.should_stop:
|
||||
@@ -663,20 +625,16 @@ def start_django(reloader, main_func, *args, **kwargs):
|
||||
# It's possible that the watchman service shuts down or otherwise
|
||||
# becomes unavailable. In that case, use the StatReloader.
|
||||
reloader = StatReloader()
|
||||
logger.error("Error connecting to Watchman: %s", ex)
|
||||
logger.info(
|
||||
"Watching for file changes with %s", reloader.__class__.__name__
|
||||
)
|
||||
logger.error('Error connecting to Watchman: %s', ex)
|
||||
logger.info('Watching for file changes with %s', reloader.__class__.__name__)
|
||||
|
||||
|
||||
def run_with_reloader(main_func, *args, **kwargs):
|
||||
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
||||
try:
|
||||
if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":
|
||||
if os.environ.get(DJANGO_AUTORELOAD_ENV) == 'true':
|
||||
reloader = get_reloader()
|
||||
logger.info(
|
||||
"Watching for file changes with %s", reloader.__class__.__name__
|
||||
)
|
||||
logger.info('Watching for file changes with %s', reloader.__class__.__name__)
|
||||
start_django(reloader, main_func, *args, **kwargs)
|
||||
else:
|
||||
exit_code = restart_with_reloader()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# RemovedInDjango50Warning
|
||||
# Copyright (c) 2010 Guilherme Gondim. All rights reserved.
|
||||
# Copyright (c) 2009 Simon Willison. All rights reserved.
|
||||
# Copyright (c) 2002 Drew Perttula. All rights reserved.
|
||||
@@ -31,48 +30,35 @@ Sample usage::
|
||||
>>> base20.decode('-31e')
|
||||
-1234
|
||||
>>> base11 = BaseConverter('0123456789-', sign='$')
|
||||
>>> base11.encode(-1234)
|
||||
>>> base11.encode('$1234')
|
||||
'$-22'
|
||||
>>> base11.decode('$-22')
|
||||
-1234
|
||||
'$1234'
|
||||
|
||||
"""
|
||||
import warnings
|
||||
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
|
||||
warnings.warn(
|
||||
"The django.utils.baseconv module is deprecated.",
|
||||
category=RemovedInDjango50Warning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
BASE2_ALPHABET = "01"
|
||||
BASE16_ALPHABET = "0123456789ABCDEF"
|
||||
BASE56_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
|
||||
BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
BASE64_ALPHABET = BASE62_ALPHABET + "-_"
|
||||
BASE2_ALPHABET = '01'
|
||||
BASE16_ALPHABET = '0123456789ABCDEF'
|
||||
BASE56_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz'
|
||||
BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
BASE64_ALPHABET = BASE62_ALPHABET + '-_'
|
||||
|
||||
|
||||
class BaseConverter:
|
||||
decimal_digits = "0123456789"
|
||||
decimal_digits = '0123456789'
|
||||
|
||||
def __init__(self, digits, sign="-"):
|
||||
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.")
|
||||
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,
|
||||
)
|
||||
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, "-")
|
||||
neg, value = self.convert(i, self.decimal_digits, self.digits, '-')
|
||||
if neg:
|
||||
return self.sign + value
|
||||
return value
|
||||
@@ -80,7 +66,7 @@ class BaseConverter:
|
||||
def decode(self, s):
|
||||
neg, value = self.convert(s, self.digits, self.decimal_digits, self.sign)
|
||||
if neg:
|
||||
value = "-" + value
|
||||
value = '-' + value
|
||||
return int(value)
|
||||
|
||||
def convert(self, number, from_digits, to_digits, sign):
|
||||
@@ -99,7 +85,7 @@ class BaseConverter:
|
||||
if x == 0:
|
||||
res = to_digits[0]
|
||||
else:
|
||||
res = ""
|
||||
res = ''
|
||||
while x > 0:
|
||||
digit = x % len(to_digits)
|
||||
res = to_digits[digit] + res
|
||||
@@ -112,4 +98,4 @@ base16 = BaseConverter(BASE16_ALPHABET)
|
||||
base36 = BaseConverter(BASE36_ALPHABET)
|
||||
base56 = BaseConverter(BASE56_ALPHABET)
|
||||
base62 = BaseConverter(BASE62_ALPHABET)
|
||||
base64 = BaseConverter(BASE64_ALPHABET, sign="$")
|
||||
base64 = BaseConverter(BASE64_ALPHABET, sign='$')
|
||||
|
||||
@@ -23,13 +23,15 @@ from collections import defaultdict
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.http import HttpResponse, HttpResponseNotModified
|
||||
from django.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag
|
||||
from django.utils.http import (
|
||||
http_date, parse_etags, parse_http_date_safe, quote_etag,
|
||||
)
|
||||
from django.utils.log import log_response
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import get_language
|
||||
|
||||
cc_delim_re = _lazy_re_compile(r"\s*,\s*")
|
||||
cc_delim_re = _lazy_re_compile(r'\s*,\s*')
|
||||
|
||||
|
||||
def patch_cache_control(response, **kwargs):
|
||||
@@ -44,9 +46,8 @@ def patch_cache_control(response, **kwargs):
|
||||
* All other parameters are added with their value, after applying
|
||||
str() to it.
|
||||
"""
|
||||
|
||||
def dictitem(s):
|
||||
t = s.split("=", 1)
|
||||
t = s.split('=', 1)
|
||||
if len(t) > 1:
|
||||
return (t[0].lower(), t[1])
|
||||
else:
|
||||
@@ -56,13 +57,13 @@ def patch_cache_control(response, **kwargs):
|
||||
if t[1] is True:
|
||||
return t[0]
|
||||
else:
|
||||
return "%s=%s" % (t[0], t[1])
|
||||
return '%s=%s' % (t[0], t[1])
|
||||
|
||||
cc = defaultdict(set)
|
||||
if response.get("Cache-Control"):
|
||||
for field in cc_delim_re.split(response.headers["Cache-Control"]):
|
||||
if response.get('Cache-Control'):
|
||||
for field in cc_delim_re.split(response.headers['Cache-Control']):
|
||||
directive, value = dictitem(field)
|
||||
if directive == "no-cache":
|
||||
if directive == 'no-cache':
|
||||
# no-cache supports multiple field names.
|
||||
cc[directive].add(value)
|
||||
else:
|
||||
@@ -71,18 +72,18 @@ def patch_cache_control(response, **kwargs):
|
||||
# If there's already a max-age header but we're being asked to set a new
|
||||
# max-age, use the minimum of the two ages. In practice this happens when
|
||||
# a decorator and a piece of middleware both operate on a given view.
|
||||
if "max-age" in cc and "max_age" in kwargs:
|
||||
kwargs["max_age"] = min(int(cc["max-age"]), kwargs["max_age"])
|
||||
if 'max-age' in cc and 'max_age' in kwargs:
|
||||
kwargs['max_age'] = min(int(cc['max-age']), kwargs['max_age'])
|
||||
|
||||
# Allow overriding private caching and vice versa
|
||||
if "private" in cc and "public" in kwargs:
|
||||
del cc["private"]
|
||||
elif "public" in cc and "private" in kwargs:
|
||||
del cc["public"]
|
||||
if 'private' in cc and 'public' in kwargs:
|
||||
del cc['private']
|
||||
elif 'public' in cc and 'private' in kwargs:
|
||||
del cc['public']
|
||||
|
||||
for (k, v) in kwargs.items():
|
||||
directive = k.replace("_", "-")
|
||||
if directive == "no-cache":
|
||||
directive = k.replace('_', '-')
|
||||
if directive == 'no-cache':
|
||||
# no-cache supports multiple field names.
|
||||
cc[directive].add(v)
|
||||
else:
|
||||
@@ -97,8 +98,8 @@ def patch_cache_control(response, **kwargs):
|
||||
directives.extend([dictvalue(directive, value) for value in values])
|
||||
else:
|
||||
directives.append(dictvalue(directive, values))
|
||||
cc = ", ".join(directives)
|
||||
response.headers["Cache-Control"] = cc
|
||||
cc = ', '.join(directives)
|
||||
response.headers['Cache-Control'] = cc
|
||||
|
||||
|
||||
def get_max_age(response):
|
||||
@@ -106,28 +107,25 @@ def get_max_age(response):
|
||||
Return the max-age from the response Cache-Control header as an integer,
|
||||
or None if it wasn't found or wasn't an integer.
|
||||
"""
|
||||
if not response.has_header("Cache-Control"):
|
||||
if not response.has_header('Cache-Control'):
|
||||
return
|
||||
cc = dict(
|
||||
_to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
|
||||
)
|
||||
cc = dict(_to_tuple(el) for el in cc_delim_re.split(response.headers['Cache-Control']))
|
||||
try:
|
||||
return int(cc["max-age"])
|
||||
return int(cc['max-age'])
|
||||
except (ValueError, TypeError, KeyError):
|
||||
pass
|
||||
|
||||
|
||||
def set_response_etag(response):
|
||||
if not response.streaming and response.content:
|
||||
response.headers["ETag"] = quote_etag(hashlib.md5(response.content).hexdigest())
|
||||
response.headers['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest())
|
||||
return response
|
||||
|
||||
|
||||
def _precondition_failed(request):
|
||||
response = HttpResponse(status=412)
|
||||
log_response(
|
||||
"Precondition Failed: %s",
|
||||
request.path,
|
||||
'Precondition Failed: %s', request.path,
|
||||
response=response,
|
||||
request=request,
|
||||
)
|
||||
@@ -139,15 +137,7 @@ def _not_modified(request, response=None):
|
||||
if response:
|
||||
# Preserve the headers required by Section 4.1 of RFC 7232, as well as
|
||||
# Last-Modified.
|
||||
for header in (
|
||||
"Cache-Control",
|
||||
"Content-Location",
|
||||
"Date",
|
||||
"ETag",
|
||||
"Expires",
|
||||
"Last-Modified",
|
||||
"Vary",
|
||||
):
|
||||
for header in ('Cache-Control', 'Content-Location', 'Date', 'ETag', 'Expires', 'Last-Modified', 'Vary'):
|
||||
if header in response:
|
||||
new_response.headers[header] = response.headers[header]
|
||||
|
||||
@@ -166,13 +156,11 @@ def get_conditional_response(request, etag=None, last_modified=None, response=No
|
||||
return response
|
||||
|
||||
# Get HTTP request headers.
|
||||
if_match_etags = parse_etags(request.META.get("HTTP_IF_MATCH", ""))
|
||||
if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
|
||||
if_unmodified_since = if_unmodified_since and parse_http_date_safe(
|
||||
if_unmodified_since
|
||||
)
|
||||
if_none_match_etags = parse_etags(request.META.get("HTTP_IF_NONE_MATCH", ""))
|
||||
if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
|
||||
if_match_etags = parse_etags(request.META.get('HTTP_IF_MATCH', ''))
|
||||
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
|
||||
if_unmodified_since = if_unmodified_since and parse_http_date_safe(if_unmodified_since)
|
||||
if_none_match_etags = parse_etags(request.META.get('HTTP_IF_NONE_MATCH', ''))
|
||||
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
|
||||
if_modified_since = if_modified_since and parse_http_date_safe(if_modified_since)
|
||||
|
||||
# Step 1 of section 6 of RFC 7232: Test the If-Match precondition.
|
||||
@@ -180,26 +168,23 @@ def get_conditional_response(request, etag=None, last_modified=None, response=No
|
||||
return _precondition_failed(request)
|
||||
|
||||
# Step 2: Test the If-Unmodified-Since precondition.
|
||||
if (
|
||||
not if_match_etags
|
||||
and if_unmodified_since
|
||||
and not _if_unmodified_since_passes(last_modified, if_unmodified_since)
|
||||
):
|
||||
if (not if_match_etags and if_unmodified_since and
|
||||
not _if_unmodified_since_passes(last_modified, if_unmodified_since)):
|
||||
return _precondition_failed(request)
|
||||
|
||||
# Step 3: Test the If-None-Match precondition.
|
||||
if if_none_match_etags and not _if_none_match_passes(etag, if_none_match_etags):
|
||||
if request.method in ("GET", "HEAD"):
|
||||
if request.method in ('GET', 'HEAD'):
|
||||
return _not_modified(request, response)
|
||||
else:
|
||||
return _precondition_failed(request)
|
||||
|
||||
# Step 4: Test the If-Modified-Since precondition.
|
||||
if (
|
||||
not if_none_match_etags
|
||||
and if_modified_since
|
||||
and not _if_modified_since_passes(last_modified, if_modified_since)
|
||||
and request.method in ("GET", "HEAD")
|
||||
not if_none_match_etags and
|
||||
if_modified_since and
|
||||
not _if_modified_since_passes(last_modified, if_modified_since) and
|
||||
request.method in ('GET', 'HEAD')
|
||||
):
|
||||
return _not_modified(request, response)
|
||||
|
||||
@@ -215,12 +200,12 @@ def _if_match_passes(target_etag, etags):
|
||||
if not target_etag:
|
||||
# If there isn't an ETag, then there can't be a match.
|
||||
return False
|
||||
elif etags == ["*"]:
|
||||
elif etags == ['*']:
|
||||
# The existence of an ETag means that there is "a current
|
||||
# representation for the target resource", even if the ETag is weak,
|
||||
# so there is a match to '*'.
|
||||
return True
|
||||
elif target_etag.startswith("W/"):
|
||||
elif target_etag.startswith('W/'):
|
||||
# A weak ETag can never strongly match another ETag.
|
||||
return False
|
||||
else:
|
||||
@@ -244,15 +229,15 @@ def _if_none_match_passes(target_etag, etags):
|
||||
if not target_etag:
|
||||
# If there isn't an ETag, then there isn't a match.
|
||||
return True
|
||||
elif etags == ["*"]:
|
||||
elif etags == ['*']:
|
||||
# The existence of an ETag means that there is "a current
|
||||
# representation for the target resource", so there is a match to '*'.
|
||||
return False
|
||||
else:
|
||||
# The comparison should be weak, so look for a match after stripping
|
||||
# off any weak indicators.
|
||||
target_etag = target_etag.strip("W/")
|
||||
etags = (etag.strip("W/") for etag in etags)
|
||||
target_etag = target_etag.strip('W/')
|
||||
etags = (etag.strip('W/') for etag in etags)
|
||||
return target_etag not in etags
|
||||
|
||||
|
||||
@@ -277,8 +262,8 @@ def patch_response_headers(response, cache_timeout=None):
|
||||
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
|
||||
if cache_timeout < 0:
|
||||
cache_timeout = 0 # Can't have max-age negative
|
||||
if not response.has_header("Expires"):
|
||||
response.headers["Expires"] = http_date(time.time() + cache_timeout)
|
||||
if not response.has_header('Expires'):
|
||||
response.headers['Expires'] = http_date(time.time() + cache_timeout)
|
||||
patch_cache_control(response, max_age=cache_timeout)
|
||||
|
||||
|
||||
@@ -287,9 +272,7 @@ def add_never_cache_headers(response):
|
||||
Add headers to a response to indicate that a page should never be cached.
|
||||
"""
|
||||
patch_response_headers(response, cache_timeout=-1)
|
||||
patch_cache_control(
|
||||
response, no_cache=True, no_store=True, must_revalidate=True, private=True
|
||||
)
|
||||
patch_cache_control(response, no_cache=True, no_store=True, must_revalidate=True, private=True)
|
||||
|
||||
|
||||
def patch_vary_headers(response, newheaders):
|
||||
@@ -302,31 +285,28 @@ def patch_vary_headers(response, newheaders):
|
||||
# Note that we need to keep the original order intact, because cache
|
||||
# implementations may rely on the order of the Vary contents in, say,
|
||||
# computing an MD5 hash.
|
||||
if response.has_header("Vary"):
|
||||
vary_headers = cc_delim_re.split(response.headers["Vary"])
|
||||
if response.has_header('Vary'):
|
||||
vary_headers = cc_delim_re.split(response.headers['Vary'])
|
||||
else:
|
||||
vary_headers = []
|
||||
# Use .lower() here so we treat headers as case-insensitive.
|
||||
existing_headers = {header.lower() for header in vary_headers}
|
||||
additional_headers = [
|
||||
newheader
|
||||
for newheader in newheaders
|
||||
if newheader.lower() not in existing_headers
|
||||
]
|
||||
additional_headers = [newheader for newheader in newheaders
|
||||
if newheader.lower() not in existing_headers]
|
||||
vary_headers += additional_headers
|
||||
if "*" in vary_headers:
|
||||
response.headers["Vary"] = "*"
|
||||
if '*' in vary_headers:
|
||||
response.headers['Vary'] = '*'
|
||||
else:
|
||||
response.headers["Vary"] = ", ".join(vary_headers)
|
||||
response.headers['Vary'] = ', '.join(vary_headers)
|
||||
|
||||
|
||||
def has_vary_header(response, header_query):
|
||||
"""
|
||||
Check to see if the response has a given header name in its Vary header.
|
||||
"""
|
||||
if not response.has_header("Vary"):
|
||||
if not response.has_header('Vary'):
|
||||
return False
|
||||
vary_headers = cc_delim_re.split(response.headers["Vary"])
|
||||
vary_headers = cc_delim_re.split(response.headers['Vary'])
|
||||
existing_headers = {header.lower() for header in vary_headers}
|
||||
return header_query.lower() in existing_headers
|
||||
|
||||
@@ -337,9 +317,9 @@ def _i18n_cache_key_suffix(request, cache_key):
|
||||
# first check if LocaleMiddleware or another middleware added
|
||||
# LANGUAGE_CODE to request, then fall back to the active language
|
||||
# which in turn can also fall back to settings.LANGUAGE_CODE
|
||||
cache_key += ".%s" % getattr(request, "LANGUAGE_CODE", get_language())
|
||||
cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
|
||||
if settings.USE_TZ:
|
||||
cache_key += ".%s" % get_current_timezone_name()
|
||||
cache_key += '.%s' % get_current_timezone_name()
|
||||
return cache_key
|
||||
|
||||
|
||||
@@ -350,27 +330,21 @@ def _generate_cache_key(request, method, headerlist, key_prefix):
|
||||
value = request.META.get(header)
|
||||
if value is not None:
|
||||
ctx.update(value.encode())
|
||||
url = hashlib.md5(request.build_absolute_uri().encode("ascii"))
|
||||
cache_key = "views.decorators.cache.cache_page.%s.%s.%s.%s" % (
|
||||
key_prefix,
|
||||
method,
|
||||
url.hexdigest(),
|
||||
ctx.hexdigest(),
|
||||
)
|
||||
url = hashlib.md5(request.build_absolute_uri().encode('ascii'))
|
||||
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
|
||||
key_prefix, method, url.hexdigest(), ctx.hexdigest())
|
||||
return _i18n_cache_key_suffix(request, cache_key)
|
||||
|
||||
|
||||
def _generate_cache_header_key(key_prefix, request):
|
||||
"""Return a cache key for the header cache."""
|
||||
url = hashlib.md5(request.build_absolute_uri().encode("ascii"))
|
||||
cache_key = "views.decorators.cache.cache_header.%s.%s" % (
|
||||
key_prefix,
|
||||
url.hexdigest(),
|
||||
)
|
||||
url = hashlib.md5(request.build_absolute_uri().encode('ascii'))
|
||||
cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
|
||||
key_prefix, url.hexdigest())
|
||||
return _i18n_cache_key_suffix(request, cache_key)
|
||||
|
||||
|
||||
def get_cache_key(request, key_prefix=None, method="GET", cache=None):
|
||||
def get_cache_key(request, key_prefix=None, method='GET', cache=None):
|
||||
"""
|
||||
Return a cache key based on the request URL and query. It can be used
|
||||
in the request phase because it pulls the list of headers to take into
|
||||
@@ -412,17 +386,17 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
|
||||
cache_key = _generate_cache_header_key(key_prefix, request)
|
||||
if cache is None:
|
||||
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
|
||||
if response.has_header("Vary"):
|
||||
if response.has_header('Vary'):
|
||||
is_accept_language_redundant = settings.USE_I18N
|
||||
# If i18n is used, the generated cache key will be suffixed with the
|
||||
# current locale. Adding the raw value of Accept-Language is redundant
|
||||
# in that case and would result in storing the same content under
|
||||
# multiple keys in the cache. See #18191 for details.
|
||||
headerlist = []
|
||||
for header in cc_delim_re.split(response.headers["Vary"]):
|
||||
header = header.upper().replace("-", "_")
|
||||
if header != "ACCEPT_LANGUAGE" or not is_accept_language_redundant:
|
||||
headerlist.append("HTTP_" + header)
|
||||
for header in cc_delim_re.split(response.headers['Vary']):
|
||||
header = header.upper().replace('-', '_')
|
||||
if header != 'ACCEPT_LANGUAGE' or not is_accept_language_redundant:
|
||||
headerlist.append('HTTP_' + header)
|
||||
headerlist.sort()
|
||||
cache.set(cache_key, headerlist, cache_timeout)
|
||||
return _generate_cache_key(request, request.method, headerlist, key_prefix)
|
||||
@@ -434,7 +408,7 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
|
||||
|
||||
|
||||
def _to_tuple(s):
|
||||
t = s.split("=", 1)
|
||||
t = s.split('=', 1)
|
||||
if len(t) == 2:
|
||||
return t[0].lower(), t[1]
|
||||
return t[0].lower(), True
|
||||
|
||||
@@ -8,8 +8,8 @@ class ConnectionProxy:
|
||||
"""Proxy for accessing a connection object's attributes."""
|
||||
|
||||
def __init__(self, connections, alias):
|
||||
self.__dict__["_connections"] = connections
|
||||
self.__dict__["_alias"] = alias
|
||||
self.__dict__['_connections'] = connections
|
||||
self.__dict__['_alias'] = alias
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._connections[self._alias], item)
|
||||
@@ -51,7 +51,7 @@ class BaseConnectionHandler:
|
||||
return settings
|
||||
|
||||
def create_connection(self, alias):
|
||||
raise NotImplementedError("Subclasses must implement create_connection().")
|
||||
raise NotImplementedError('Subclasses must implement create_connection().')
|
||||
|
||||
def __getitem__(self, alias):
|
||||
try:
|
||||
|
||||
@@ -4,18 +4,19 @@ Django's standard crypto functions and utilities.
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.deprecation import RemovedInDjango40Warning
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
|
||||
class InvalidAlgorithm(ValueError):
|
||||
"""Algorithm is not supported by hashlib."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
|
||||
def salted_hmac(key_salt, value, secret=None, *, algorithm='sha1'):
|
||||
"""
|
||||
Return the HMAC of 'value', using a key generated from key_salt and a
|
||||
secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1,
|
||||
@@ -32,7 +33,8 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
|
||||
hasher = getattr(hashlib, algorithm)
|
||||
except AttributeError as e:
|
||||
raise InvalidAlgorithm(
|
||||
"%r is not an algorithm accepted by the hashlib module." % algorithm
|
||||
'%r is not an algorithm accepted by the hashlib module.'
|
||||
% algorithm
|
||||
) from e
|
||||
# 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.
|
||||
@@ -44,10 +46,13 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
|
||||
return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
|
||||
|
||||
|
||||
RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
NOT_PROVIDED = object() # RemovedInDjango40Warning.
|
||||
RANDOM_STRING_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
||||
|
||||
def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
||||
# RemovedInDjango40Warning: when the deprecation ends, replace with:
|
||||
# def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
||||
def get_random_string(length=NOT_PROVIDED, allowed_chars=RANDOM_STRING_CHARS):
|
||||
"""
|
||||
Return a securely generated random string.
|
||||
|
||||
@@ -58,7 +63,13 @@ def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
||||
* length: 12, bit length =~ 71 bits
|
||||
* length: 22, bit length =~ 131 bits
|
||||
"""
|
||||
return "".join(secrets.choice(allowed_chars) for i in range(length))
|
||||
if length is NOT_PROVIDED:
|
||||
warnings.warn(
|
||||
'Not providing a length argument is deprecated.',
|
||||
RemovedInDjango40Warning,
|
||||
)
|
||||
length = 12
|
||||
return ''.join(secrets.choice(allowed_chars) for i in range(length))
|
||||
|
||||
|
||||
def constant_time_compare(val1, val2):
|
||||
|
||||
@@ -25,9 +25,6 @@ class OrderedSet:
|
||||
def __iter__(self):
|
||||
return iter(self.dict)
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self.dict)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.dict
|
||||
|
||||
@@ -37,10 +34,6 @@ class OrderedSet:
|
||||
def __len__(self):
|
||||
return len(self.dict)
|
||||
|
||||
def __repr__(self):
|
||||
data = repr(list(self.dict)) if self.dict else ""
|
||||
return f"{self.__class__.__qualname__}({data})"
|
||||
|
||||
|
||||
class MultiValueDictKeyError(KeyError):
|
||||
pass
|
||||
@@ -65,10 +58,9 @@ class MultiValueDict(dict):
|
||||
>>> d.setlist('lastname', ['Holovaty', 'Willison'])
|
||||
|
||||
This class exists to solve the irritating problem raised by cgi.parse_qs,
|
||||
which returns a list for every key, even though most web forms submit
|
||||
which returns a list for every key, even though most Web forms submit
|
||||
single name-value pairs.
|
||||
"""
|
||||
|
||||
def __init__(self, key_to_list_mapping=()):
|
||||
super().__init__(key_to_list_mapping)
|
||||
|
||||
@@ -93,22 +85,24 @@ class MultiValueDict(dict):
|
||||
super().__setitem__(key, [value])
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__([(k, v[:]) for k, v in self.lists()])
|
||||
return self.__class__([
|
||||
(k, v[:])
|
||||
for k, v in self.lists()
|
||||
])
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
result = self.__class__()
|
||||
memo[id(self)] = result
|
||||
for key, value in dict.items(self):
|
||||
dict.__setitem__(
|
||||
result, copy.deepcopy(key, memo), copy.deepcopy(value, memo)
|
||||
)
|
||||
dict.__setitem__(result, copy.deepcopy(key, memo),
|
||||
copy.deepcopy(value, memo))
|
||||
return result
|
||||
|
||||
def __getstate__(self):
|
||||
return {**self.__dict__, "_data": {k: self._getlist(k) for k in self}}
|
||||
return {**self.__dict__, '_data': {k: self._getlist(k) for k in self}}
|
||||
|
||||
def __setstate__(self, obj_dict):
|
||||
data = obj_dict.pop("_data", {})
|
||||
data = obj_dict.pop('_data', {})
|
||||
for k, v in data.items():
|
||||
self.setlist(k, v)
|
||||
self.__dict__.update(obj_dict)
|
||||
@@ -230,7 +224,7 @@ class ImmutableList(tuple):
|
||||
AttributeError: You cannot mutate this.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, warning="ImmutableList object is immutable.", **kwargs):
|
||||
def __new__(cls, *args, warning='ImmutableList object is immutable.', **kwargs):
|
||||
self = tuple.__new__(cls, *args, **kwargs)
|
||||
self.warning = warning
|
||||
return self
|
||||
@@ -263,7 +257,6 @@ class DictWrapper(dict):
|
||||
Used by the SQL construction code to ensure that values are correctly
|
||||
quoted before being used.
|
||||
"""
|
||||
|
||||
def __init__(self, data, func, prefix):
|
||||
super().__init__(data)
|
||||
self.func = func
|
||||
@@ -277,7 +270,7 @@ class DictWrapper(dict):
|
||||
"""
|
||||
use_func = key.startswith(self.prefix)
|
||||
if use_func:
|
||||
key = key[len(self.prefix) :]
|
||||
key = key[len(self.prefix):]
|
||||
value = super().__getitem__(key)
|
||||
if use_func:
|
||||
return self.func(value)
|
||||
@@ -288,13 +281,11 @@ def _destruct_iterable_mapping_values(data):
|
||||
for i, elem in enumerate(data):
|
||||
if len(elem) != 2:
|
||||
raise ValueError(
|
||||
"dictionary update sequence element #{} has "
|
||||
"length {}; 2 is required.".format(i, len(elem))
|
||||
'dictionary update sequence element #{} has '
|
||||
'length {}; 2 is required.'.format(i, len(elem))
|
||||
)
|
||||
if not isinstance(elem[0], str):
|
||||
raise ValueError(
|
||||
"Element key %r invalid, only strings are allowed" % elem[0]
|
||||
)
|
||||
raise ValueError('Element key %r invalid, only strings are allowed' % elem[0])
|
||||
yield tuple(elem)
|
||||
|
||||
|
||||
@@ -330,7 +321,9 @@ class CaseInsensitiveMapping(Mapping):
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Mapping) and {
|
||||
k.lower(): v for k, v in self.items()
|
||||
} == {k.lower(): v for k, v in other.items()}
|
||||
} == {
|
||||
k.lower(): v for k, v in other.items()
|
||||
}
|
||||
|
||||
def __iter__(self):
|
||||
return (original_key for original_key, value in self._store.values())
|
||||
|
||||
@@ -12,27 +12,21 @@ Usage:
|
||||
"""
|
||||
import calendar
|
||||
import datetime
|
||||
import time
|
||||
from email.utils import format_datetime as format_datetime_rfc5322
|
||||
|
||||
from django.utils.dates import (
|
||||
MONTHS,
|
||||
MONTHS_3,
|
||||
MONTHS_ALT,
|
||||
MONTHS_AP,
|
||||
WEEKDAYS,
|
||||
WEEKDAYS_ABBR,
|
||||
MONTHS, MONTHS_3, MONTHS_ALT, MONTHS_AP, WEEKDAYS, WEEKDAYS_ABBR,
|
||||
)
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.timezone import (
|
||||
_datetime_ambiguous_or_imaginary,
|
||||
get_default_timezone,
|
||||
is_naive,
|
||||
_datetime_ambiguous_or_imaginary, get_default_timezone, is_aware, is_naive,
|
||||
make_aware,
|
||||
)
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
re_formatchars = _lazy_re_compile(r"(?<!\\)([aAbcdDeEfFgGhHiIjlLmMnNoOPrsStTUuwWyYzZ])")
|
||||
re_escaped = _lazy_re_compile(r"\\(.)")
|
||||
re_formatchars = _lazy_re_compile(r'(?<!\\)([aAbcdDeEfFgGhHiIjlLmMnNoOPrsStTUuwWyYzZ])')
|
||||
re_escaped = _lazy_re_compile(r'\\(.)')
|
||||
|
||||
|
||||
class Formatter:
|
||||
@@ -47,11 +41,12 @@ class Formatter:
|
||||
)
|
||||
pieces.append(str(getattr(self, piece)()))
|
||||
elif piece:
|
||||
pieces.append(re_escaped.sub(r"\1", piece))
|
||||
return "".join(pieces)
|
||||
pieces.append(re_escaped.sub(r'\1', piece))
|
||||
return ''.join(pieces)
|
||||
|
||||
|
||||
class TimeFormat(Formatter):
|
||||
|
||||
def __init__(self, obj):
|
||||
self.data = obj
|
||||
self.timezone = None
|
||||
@@ -65,23 +60,17 @@ class TimeFormat(Formatter):
|
||||
else:
|
||||
self.timezone = obj.tzinfo
|
||||
|
||||
@property
|
||||
def _no_timezone_or_datetime_is_ambiguous_or_imaginary(self):
|
||||
return not self.timezone or _datetime_ambiguous_or_imaginary(
|
||||
self.data, self.timezone
|
||||
)
|
||||
|
||||
def a(self):
|
||||
"'a.m.' or 'p.m.'"
|
||||
if self.data.hour > 11:
|
||||
return _("p.m.")
|
||||
return _("a.m.")
|
||||
return _('p.m.')
|
||||
return _('a.m.')
|
||||
|
||||
def A(self):
|
||||
"'AM' or 'PM'"
|
||||
if self.data.hour > 11:
|
||||
return _("PM")
|
||||
return _("AM")
|
||||
return _('PM')
|
||||
return _('AM')
|
||||
|
||||
def e(self):
|
||||
"""
|
||||
@@ -93,8 +82,8 @@ class TimeFormat(Formatter):
|
||||
return ""
|
||||
|
||||
try:
|
||||
if hasattr(self.data, "tzinfo") and self.data.tzinfo:
|
||||
return self.data.tzname() or ""
|
||||
if hasattr(self.data, 'tzinfo') and self.data.tzinfo:
|
||||
return self.data.tzname() or ''
|
||||
except NotImplementedError:
|
||||
pass
|
||||
return ""
|
||||
@@ -106,9 +95,9 @@ class TimeFormat(Formatter):
|
||||
Examples: '1', '1:30', '2:05', '2'
|
||||
Proprietary extension.
|
||||
"""
|
||||
hour = self.data.hour % 12 or 12
|
||||
minute = self.data.minute
|
||||
return "%d:%02d" % (hour, minute) if minute else hour
|
||||
if self.data.minute == 0:
|
||||
return self.g()
|
||||
return '%s:%s' % (self.g(), self.i())
|
||||
|
||||
def g(self):
|
||||
"Hour, 12-hour format without leading zeros; i.e. '1' to '12'"
|
||||
@@ -120,15 +109,15 @@ class TimeFormat(Formatter):
|
||||
|
||||
def h(self):
|
||||
"Hour, 12-hour format; i.e. '01' to '12'"
|
||||
return "%02d" % (self.data.hour % 12 or 12)
|
||||
return '%02d' % self.g()
|
||||
|
||||
def H(self):
|
||||
"Hour, 24-hour format; i.e. '00' to '23'"
|
||||
return "%02d" % self.data.hour
|
||||
return '%02d' % self.G()
|
||||
|
||||
def i(self):
|
||||
"Minutes; i.e. '00' to '59'"
|
||||
return "%02d" % self.data.minute
|
||||
return '%02d' % self.data.minute
|
||||
|
||||
def O(self): # NOQA: E743, E741
|
||||
"""
|
||||
@@ -136,11 +125,13 @@ class TimeFormat(Formatter):
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if self._no_timezone_or_datetime_is_ambiguous_or_imaginary:
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
seconds = self.Z()
|
||||
sign = "-" if seconds < 0 else "+"
|
||||
if seconds == "":
|
||||
return ""
|
||||
sign = '-' if seconds < 0 else '+'
|
||||
seconds = abs(seconds)
|
||||
return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60)
|
||||
|
||||
@@ -152,14 +143,14 @@ class TimeFormat(Formatter):
|
||||
Proprietary extension.
|
||||
"""
|
||||
if self.data.minute == 0 and self.data.hour == 0:
|
||||
return _("midnight")
|
||||
return _('midnight')
|
||||
if self.data.minute == 0 and self.data.hour == 12:
|
||||
return _("noon")
|
||||
return "%s %s" % (self.f(), self.a())
|
||||
return _('noon')
|
||||
return '%s %s' % (self.f(), self.a())
|
||||
|
||||
def s(self):
|
||||
"Seconds; i.e. '00' to '59'"
|
||||
return "%02d" % self.data.second
|
||||
return '%02d' % self.data.second
|
||||
|
||||
def T(self):
|
||||
"""
|
||||
@@ -167,14 +158,18 @@ class TimeFormat(Formatter):
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if self._no_timezone_or_datetime_is_ambiguous_or_imaginary:
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
return str(self.timezone.tzname(self.data))
|
||||
if not _datetime_ambiguous_or_imaginary(self.data, self.timezone):
|
||||
name = self.timezone.tzname(self.data)
|
||||
else:
|
||||
name = self.format('O')
|
||||
return str(name)
|
||||
|
||||
def u(self):
|
||||
"Microseconds; i.e. '000000' to '999999'"
|
||||
return "%06d" % self.data.microsecond
|
||||
return '%06d' % self.data.microsecond
|
||||
|
||||
def Z(self):
|
||||
"""
|
||||
@@ -184,7 +179,10 @@ class TimeFormat(Formatter):
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if self._no_timezone_or_datetime_is_ambiguous_or_imaginary:
|
||||
if (
|
||||
not self.timezone or
|
||||
_datetime_ambiguous_or_imaginary(self.data, self.timezone)
|
||||
):
|
||||
return ""
|
||||
|
||||
offset = self.timezone.utcoffset(self.data)
|
||||
@@ -210,7 +208,7 @@ class DateFormat(TimeFormat):
|
||||
|
||||
def d(self):
|
||||
"Day of the month, 2 digits with leading zeros; i.e. '01' to '31'"
|
||||
return "%02d" % self.data.day
|
||||
return '%02d' % self.data.day
|
||||
|
||||
def D(self):
|
||||
"Day of the week, textual, 3 letters; e.g. 'Fri'"
|
||||
@@ -225,10 +223,13 @@ class DateFormat(TimeFormat):
|
||||
return MONTHS[self.data.month]
|
||||
|
||||
def I(self): # NOQA: E743, E741
|
||||
"'1' if daylight saving time, '0' otherwise."
|
||||
if self._no_timezone_or_datetime_is_ambiguous_or_imaginary:
|
||||
return ""
|
||||
return "1" if self.timezone.dst(self.data) else "0"
|
||||
"'1' if Daylight Savings Time, '0' otherwise."
|
||||
if (
|
||||
not self.timezone or
|
||||
_datetime_ambiguous_or_imaginary(self.data, self.timezone)
|
||||
):
|
||||
return ''
|
||||
return '1' if self.timezone.dst(self.data) else '0'
|
||||
|
||||
def j(self):
|
||||
"Day of the month without leading zeros; i.e. '1' to '31'"
|
||||
@@ -244,7 +245,7 @@ class DateFormat(TimeFormat):
|
||||
|
||||
def m(self):
|
||||
"Month; i.e. '01' to '12'"
|
||||
return "%02d" % self.data.month
|
||||
return '%02d' % self.data.month
|
||||
|
||||
def M(self):
|
||||
"Month, textual, 3 letters; e.g. 'Jan'"
|
||||
@@ -276,31 +277,28 @@ class DateFormat(TimeFormat):
|
||||
return format_datetime_rfc5322(dt)
|
||||
|
||||
def S(self):
|
||||
"""
|
||||
English ordinal suffix for the day of the month, 2 characters; i.e.
|
||||
'st', 'nd', 'rd' or 'th'.
|
||||
"""
|
||||
"English ordinal suffix for the day of the month, 2 characters; i.e. 'st', 'nd', 'rd' or 'th'"
|
||||
if self.data.day in (11, 12, 13): # Special case
|
||||
return "th"
|
||||
return 'th'
|
||||
last = self.data.day % 10
|
||||
if last == 1:
|
||||
return "st"
|
||||
return 'st'
|
||||
if last == 2:
|
||||
return "nd"
|
||||
return 'nd'
|
||||
if last == 3:
|
||||
return "rd"
|
||||
return "th"
|
||||
return 'rd'
|
||||
return 'th'
|
||||
|
||||
def t(self):
|
||||
"Number of days in the given month; i.e. '28' to '31'"
|
||||
return "%02d" % calendar.monthrange(self.data.year, self.data.month)[1]
|
||||
return '%02d' % calendar.monthrange(self.data.year, self.data.month)[1]
|
||||
|
||||
def U(self):
|
||||
"Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)"
|
||||
value = self.data
|
||||
if not isinstance(value, datetime.datetime):
|
||||
value = datetime.datetime.combine(value, datetime.time.min)
|
||||
return int(value.timestamp())
|
||||
if isinstance(self.data, datetime.datetime) and is_aware(self.data):
|
||||
return int(calendar.timegm(self.data.utctimetuple()))
|
||||
else:
|
||||
return int(time.mktime(self.data.timetuple()))
|
||||
|
||||
def w(self):
|
||||
"Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)"
|
||||
@@ -312,11 +310,11 @@ class DateFormat(TimeFormat):
|
||||
|
||||
def y(self):
|
||||
"""Year, 2 digits with leading zeros; e.g. '99'."""
|
||||
return "%02d" % (self.data.year % 100)
|
||||
return '%02d' % (self.data.year % 100)
|
||||
|
||||
def Y(self):
|
||||
"""Year, 4 digits with leading zeros; e.g. '1999'."""
|
||||
return "%04d" % self.data.year
|
||||
"Year, 4 digits; e.g. '1999'"
|
||||
return self.data.year
|
||||
|
||||
def z(self):
|
||||
"""Day of the year, i.e. 1 to 366."""
|
||||
|
||||
@@ -10,57 +10,59 @@ import datetime
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.timezone import get_fixed_timezone, utc
|
||||
|
||||
date_re = _lazy_re_compile(r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$")
|
||||
date_re = _lazy_re_compile(
|
||||
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$'
|
||||
)
|
||||
|
||||
time_re = _lazy_re_compile(
|
||||
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
||||
r"(?::(?P<second>\d{1,2})(?:[\.,](?P<microsecond>\d{1,6})\d{0,6})?)?$"
|
||||
r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
|
||||
r'(?::(?P<second>\d{1,2})(?:[\.,](?P<microsecond>\d{1,6})\d{0,6})?)?'
|
||||
)
|
||||
|
||||
datetime_re = _lazy_re_compile(
|
||||
r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
||||
r"[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
||||
r"(?::(?P<second>\d{1,2})(?:[\.,](?P<microsecond>\d{1,6})\d{0,6})?)?"
|
||||
r"\s*(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
||||
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
|
||||
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
|
||||
r'(?::(?P<second>\d{1,2})(?:[\.,](?P<microsecond>\d{1,6})\d{0,6})?)?'
|
||||
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
|
||||
)
|
||||
|
||||
standard_duration_re = _lazy_re_compile(
|
||||
r"^"
|
||||
r"(?:(?P<days>-?\d+) (days?, )?)?"
|
||||
r"(?P<sign>-?)"
|
||||
r"((?:(?P<hours>\d+):)(?=\d+:\d+))?"
|
||||
r"(?:(?P<minutes>\d+):)?"
|
||||
r"(?P<seconds>\d+)"
|
||||
r"(?:[\.,](?P<microseconds>\d{1,6})\d{0,6})?"
|
||||
r"$"
|
||||
r'^'
|
||||
r'(?:(?P<days>-?\d+) (days?, )?)?'
|
||||
r'(?P<sign>-?)'
|
||||
r'((?:(?P<hours>\d+):)(?=\d+:\d+))?'
|
||||
r'(?:(?P<minutes>\d+):)?'
|
||||
r'(?P<seconds>\d+)'
|
||||
r'(?:[\.,](?P<microseconds>\d{1,6})\d{0,6})?'
|
||||
r'$'
|
||||
)
|
||||
|
||||
# Support the sections of ISO 8601 date representation that are accepted by
|
||||
# timedelta
|
||||
iso8601_duration_re = _lazy_re_compile(
|
||||
r"^(?P<sign>[-+]?)"
|
||||
r"P"
|
||||
r"(?:(?P<days>\d+(.\d+)?)D)?"
|
||||
r"(?:T"
|
||||
r"(?:(?P<hours>\d+(.\d+)?)H)?"
|
||||
r"(?:(?P<minutes>\d+(.\d+)?)M)?"
|
||||
r"(?:(?P<seconds>\d+(.\d+)?)S)?"
|
||||
r")?"
|
||||
r"$"
|
||||
r'^(?P<sign>[-+]?)'
|
||||
r'P'
|
||||
r'(?:(?P<days>\d+(.\d+)?)D)?'
|
||||
r'(?:T'
|
||||
r'(?:(?P<hours>\d+(.\d+)?)H)?'
|
||||
r'(?:(?P<minutes>\d+(.\d+)?)M)?'
|
||||
r'(?:(?P<seconds>\d+(.\d+)?)S)?'
|
||||
r')?'
|
||||
r'$'
|
||||
)
|
||||
|
||||
# Support PostgreSQL's day-time interval format, e.g. "3 days 04:05:06". The
|
||||
# year-month and mixed intervals cannot be converted to a timedelta and thus
|
||||
# aren't accepted.
|
||||
postgres_interval_re = _lazy_re_compile(
|
||||
r"^"
|
||||
r"(?:(?P<days>-?\d+) (days? ?))?"
|
||||
r"(?:(?P<sign>[-+])?"
|
||||
r"(?P<hours>\d+):"
|
||||
r"(?P<minutes>\d\d):"
|
||||
r"(?P<seconds>\d\d)"
|
||||
r"(?:\.(?P<microseconds>\d{1,6}))?"
|
||||
r")?$"
|
||||
r'^'
|
||||
r'(?:(?P<days>-?\d+) (days? ?))?'
|
||||
r'(?:(?P<sign>[-+])?'
|
||||
r'(?P<hours>\d+):'
|
||||
r'(?P<minutes>\d\d):'
|
||||
r'(?P<seconds>\d\d)'
|
||||
r'(?:\.(?P<microseconds>\d{1,6}))?'
|
||||
r')?$'
|
||||
)
|
||||
|
||||
|
||||
@@ -70,12 +72,10 @@ def parse_date(value):
|
||||
Raise ValueError if the input is well formatted but not a valid date.
|
||||
Return None if the input isn't well formatted.
|
||||
"""
|
||||
try:
|
||||
return datetime.date.fromisoformat(value)
|
||||
except ValueError:
|
||||
if match := date_re.match(value):
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
return datetime.date(**kw)
|
||||
match = date_re.match(value)
|
||||
if match:
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
return datetime.date(**kw)
|
||||
|
||||
|
||||
def parse_time(value):
|
||||
@@ -87,18 +87,12 @@ def parse_time(value):
|
||||
Return None if the input isn't well formatted, in particular if it
|
||||
contains an offset.
|
||||
"""
|
||||
try:
|
||||
# The fromisoformat() method takes time zone info into account and
|
||||
# returns a time with a tzinfo component, if possible. However, there
|
||||
# are no circumstances where aware datetime.time objects make sense, so
|
||||
# remove the time zone offset.
|
||||
return datetime.time.fromisoformat(value).replace(tzinfo=None)
|
||||
except ValueError:
|
||||
if match := time_re.match(value):
|
||||
kw = match.groupdict()
|
||||
kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
|
||||
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
return datetime.time(**kw)
|
||||
match = time_re.match(value)
|
||||
if match:
|
||||
kw = match.groupdict()
|
||||
kw['microsecond'] = kw['microsecond'] and kw['microsecond'].ljust(6, '0')
|
||||
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
return datetime.time(**kw)
|
||||
|
||||
|
||||
def parse_datetime(value):
|
||||
@@ -110,23 +104,22 @@ def parse_datetime(value):
|
||||
Raise ValueError if the input is well formatted but not a valid datetime.
|
||||
Return None if the input isn't well formatted.
|
||||
"""
|
||||
try:
|
||||
return datetime.datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
if match := datetime_re.match(value):
|
||||
kw = match.groupdict()
|
||||
kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
|
||||
tzinfo = kw.pop("tzinfo")
|
||||
if tzinfo == "Z":
|
||||
tzinfo = utc
|
||||
elif tzinfo is not None:
|
||||
offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
|
||||
offset = 60 * int(tzinfo[1:3]) + offset_mins
|
||||
if tzinfo[0] == "-":
|
||||
offset = -offset
|
||||
tzinfo = get_fixed_timezone(offset)
|
||||
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
return datetime.datetime(**kw, tzinfo=tzinfo)
|
||||
match = datetime_re.match(value)
|
||||
if match:
|
||||
kw = match.groupdict()
|
||||
kw['microsecond'] = kw['microsecond'] and kw['microsecond'].ljust(6, '0')
|
||||
tzinfo = kw.pop('tzinfo')
|
||||
if tzinfo == 'Z':
|
||||
tzinfo = utc
|
||||
elif tzinfo is not None:
|
||||
offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
|
||||
offset = 60 * int(tzinfo[1:3]) + offset_mins
|
||||
if tzinfo[0] == '-':
|
||||
offset = -offset
|
||||
tzinfo = get_fixed_timezone(offset)
|
||||
kw = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw['tzinfo'] = tzinfo
|
||||
return datetime.datetime(**kw)
|
||||
|
||||
|
||||
def parse_duration(value):
|
||||
@@ -138,23 +131,19 @@ def parse_duration(value):
|
||||
format.
|
||||
"""
|
||||
match = (
|
||||
standard_duration_re.match(value)
|
||||
or iso8601_duration_re.match(value)
|
||||
or postgres_interval_re.match(value)
|
||||
standard_duration_re.match(value) or
|
||||
iso8601_duration_re.match(value) or
|
||||
postgres_interval_re.match(value)
|
||||
)
|
||||
if match:
|
||||
kw = match.groupdict()
|
||||
sign = -1 if kw.pop("sign", "+") == "-" else 1
|
||||
if kw.get("microseconds"):
|
||||
kw["microseconds"] = kw["microseconds"].ljust(6, "0")
|
||||
if (
|
||||
kw.get("seconds")
|
||||
and kw.get("microseconds")
|
||||
and kw["seconds"].startswith("-")
|
||||
):
|
||||
kw["microseconds"] = "-" + kw["microseconds"]
|
||||
kw = {k: float(v.replace(",", ".")) for k, v in kw.items() if v is not None}
|
||||
days = datetime.timedelta(kw.pop("days", 0.0) or 0.0)
|
||||
sign = -1 if kw.pop('sign', '+') == '-' else 1
|
||||
if kw.get('microseconds'):
|
||||
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
|
||||
if kw.get('seconds') and kw.get('microseconds') and kw['seconds'].startswith('-'):
|
||||
kw['microseconds'] = '-' + kw['microseconds']
|
||||
kw = {k: float(v.replace(',', '.')) for k, v in kw.items() if v is not None}
|
||||
days = datetime.timedelta(kw.pop('days', .0) or .0)
|
||||
if match.re == iso8601_duration_re:
|
||||
days *= sign
|
||||
return days + sign * datetime.timedelta(**kw)
|
||||
|
||||
@@ -1,79 +1,49 @@
|
||||
"Commonly-used date structures"
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
WEEKDAYS = {
|
||||
0: _("Monday"),
|
||||
1: _("Tuesday"),
|
||||
2: _("Wednesday"),
|
||||
3: _("Thursday"),
|
||||
4: _("Friday"),
|
||||
5: _("Saturday"),
|
||||
6: _("Sunday"),
|
||||
0: _('Monday'), 1: _('Tuesday'), 2: _('Wednesday'), 3: _('Thursday'), 4: _('Friday'),
|
||||
5: _('Saturday'), 6: _('Sunday')
|
||||
}
|
||||
WEEKDAYS_ABBR = {
|
||||
0: _("Mon"),
|
||||
1: _("Tue"),
|
||||
2: _("Wed"),
|
||||
3: _("Thu"),
|
||||
4: _("Fri"),
|
||||
5: _("Sat"),
|
||||
6: _("Sun"),
|
||||
0: _('Mon'), 1: _('Tue'), 2: _('Wed'), 3: _('Thu'), 4: _('Fri'),
|
||||
5: _('Sat'), 6: _('Sun')
|
||||
}
|
||||
MONTHS = {
|
||||
1: _("January"),
|
||||
2: _("February"),
|
||||
3: _("March"),
|
||||
4: _("April"),
|
||||
5: _("May"),
|
||||
6: _("June"),
|
||||
7: _("July"),
|
||||
8: _("August"),
|
||||
9: _("September"),
|
||||
10: _("October"),
|
||||
11: _("November"),
|
||||
12: _("December"),
|
||||
1: _('January'), 2: _('February'), 3: _('March'), 4: _('April'), 5: _('May'), 6: _('June'),
|
||||
7: _('July'), 8: _('August'), 9: _('September'), 10: _('October'), 11: _('November'),
|
||||
12: _('December')
|
||||
}
|
||||
MONTHS_3 = {
|
||||
1: _("jan"),
|
||||
2: _("feb"),
|
||||
3: _("mar"),
|
||||
4: _("apr"),
|
||||
5: _("may"),
|
||||
6: _("jun"),
|
||||
7: _("jul"),
|
||||
8: _("aug"),
|
||||
9: _("sep"),
|
||||
10: _("oct"),
|
||||
11: _("nov"),
|
||||
12: _("dec"),
|
||||
1: _('jan'), 2: _('feb'), 3: _('mar'), 4: _('apr'), 5: _('may'), 6: _('jun'),
|
||||
7: _('jul'), 8: _('aug'), 9: _('sep'), 10: _('oct'), 11: _('nov'), 12: _('dec')
|
||||
}
|
||||
MONTHS_AP = { # month names in Associated Press style
|
||||
1: pgettext_lazy("abbrev. month", "Jan."),
|
||||
2: pgettext_lazy("abbrev. month", "Feb."),
|
||||
3: pgettext_lazy("abbrev. month", "March"),
|
||||
4: pgettext_lazy("abbrev. month", "April"),
|
||||
5: pgettext_lazy("abbrev. month", "May"),
|
||||
6: pgettext_lazy("abbrev. month", "June"),
|
||||
7: pgettext_lazy("abbrev. month", "July"),
|
||||
8: pgettext_lazy("abbrev. month", "Aug."),
|
||||
9: pgettext_lazy("abbrev. month", "Sept."),
|
||||
10: pgettext_lazy("abbrev. month", "Oct."),
|
||||
11: pgettext_lazy("abbrev. month", "Nov."),
|
||||
12: pgettext_lazy("abbrev. month", "Dec."),
|
||||
1: pgettext_lazy('abbrev. month', 'Jan.'),
|
||||
2: pgettext_lazy('abbrev. month', 'Feb.'),
|
||||
3: pgettext_lazy('abbrev. month', 'March'),
|
||||
4: pgettext_lazy('abbrev. month', 'April'),
|
||||
5: pgettext_lazy('abbrev. month', 'May'),
|
||||
6: pgettext_lazy('abbrev. month', 'June'),
|
||||
7: pgettext_lazy('abbrev. month', 'July'),
|
||||
8: pgettext_lazy('abbrev. month', 'Aug.'),
|
||||
9: pgettext_lazy('abbrev. month', 'Sept.'),
|
||||
10: pgettext_lazy('abbrev. month', 'Oct.'),
|
||||
11: pgettext_lazy('abbrev. month', 'Nov.'),
|
||||
12: pgettext_lazy('abbrev. month', 'Dec.')
|
||||
}
|
||||
MONTHS_ALT = { # required for long date representation by some locales
|
||||
1: pgettext_lazy("alt. month", "January"),
|
||||
2: pgettext_lazy("alt. month", "February"),
|
||||
3: pgettext_lazy("alt. month", "March"),
|
||||
4: pgettext_lazy("alt. month", "April"),
|
||||
5: pgettext_lazy("alt. month", "May"),
|
||||
6: pgettext_lazy("alt. month", "June"),
|
||||
7: pgettext_lazy("alt. month", "July"),
|
||||
8: pgettext_lazy("alt. month", "August"),
|
||||
9: pgettext_lazy("alt. month", "September"),
|
||||
10: pgettext_lazy("alt. month", "October"),
|
||||
11: pgettext_lazy("alt. month", "November"),
|
||||
12: pgettext_lazy("alt. month", "December"),
|
||||
1: pgettext_lazy('alt. month', 'January'),
|
||||
2: pgettext_lazy('alt. month', 'February'),
|
||||
3: pgettext_lazy('alt. month', 'March'),
|
||||
4: pgettext_lazy('alt. month', 'April'),
|
||||
5: pgettext_lazy('alt. month', 'May'),
|
||||
6: pgettext_lazy('alt. month', 'June'),
|
||||
7: pgettext_lazy('alt. month', 'July'),
|
||||
8: pgettext_lazy('alt. month', 'August'),
|
||||
9: pgettext_lazy('alt. month', 'September'),
|
||||
10: pgettext_lazy('alt. month', 'October'),
|
||||
11: pgettext_lazy('alt. month', 'November'),
|
||||
12: pgettext_lazy('alt. month', 'December')
|
||||
}
|
||||
|
||||
@@ -7,20 +7,13 @@
|
||||
# >>> datetime_safe.date(10, 8, 2).strftime("%Y/%m/%d was a %A")
|
||||
# '0010/08/02 was a Monday'
|
||||
|
||||
import time
|
||||
import warnings
|
||||
from datetime import date as real_date
|
||||
from datetime import datetime as real_datetime
|
||||
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
warnings.warn(
|
||||
"The django.utils.datetime_safe module is deprecated.",
|
||||
category=RemovedInDjango50Warning,
|
||||
stacklevel=2,
|
||||
import time as ttime
|
||||
from datetime import (
|
||||
date as real_date, datetime as real_datetime, time as real_time,
|
||||
)
|
||||
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
|
||||
class date(real_date):
|
||||
def strftime(self, fmt):
|
||||
@@ -33,21 +26,18 @@ class datetime(real_datetime):
|
||||
|
||||
@classmethod
|
||||
def combine(cls, date, time):
|
||||
return cls(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
time.second,
|
||||
time.microsecond,
|
||||
time.tzinfo,
|
||||
)
|
||||
return cls(date.year, date.month, date.day,
|
||||
time.hour, time.minute, time.second,
|
||||
time.microsecond, time.tzinfo)
|
||||
|
||||
def date(self):
|
||||
return date(self.year, self.month, self.day)
|
||||
|
||||
|
||||
class time(real_time):
|
||||
pass
|
||||
|
||||
|
||||
def new_date(d):
|
||||
"Generate a safe date from a datetime.date object."
|
||||
return date(d.year, d.month, d.day)
|
||||
@@ -86,9 +76,7 @@ def strftime(dt, fmt):
|
||||
return super(type(dt), dt).strftime(fmt)
|
||||
illegal_formatting = _illegal_formatting.search(fmt)
|
||||
if illegal_formatting:
|
||||
raise TypeError(
|
||||
"strftime of dates before 1000 does not handle " + illegal_formatting[0]
|
||||
)
|
||||
raise TypeError('strftime of dates before 1000 does not handle ' + illegal_formatting[0])
|
||||
|
||||
year = dt.year
|
||||
# For every non-leap year century, advance by
|
||||
@@ -100,10 +88,10 @@ def strftime(dt, fmt):
|
||||
# Move to around the year 2000
|
||||
year = year + ((2000 - year) // 28) * 28
|
||||
timetuple = dt.timetuple()
|
||||
s1 = time.strftime(fmt, (year,) + timetuple[1:])
|
||||
s1 = ttime.strftime(fmt, (year,) + timetuple[1:])
|
||||
sites1 = _findall(s1, str(year))
|
||||
|
||||
s2 = time.strftime(fmt, (year + 28,) + timetuple[1:])
|
||||
s2 = ttime.strftime(fmt, (year + 28,) + timetuple[1:])
|
||||
sites2 = _findall(s2, str(year + 28))
|
||||
|
||||
sites = []
|
||||
@@ -114,5 +102,5 @@ def strftime(dt, fmt):
|
||||
s = s1
|
||||
syear = "%04d" % dt.year
|
||||
for site in sites:
|
||||
s = s[:site] + syear + s[site + 4 :]
|
||||
s = s[:site] + syear + s[site + 4:]
|
||||
return s
|
||||
|
||||
@@ -10,7 +10,6 @@ def deconstructible(*args, path=None):
|
||||
|
||||
The `path` kwarg specifies the import path.
|
||||
"""
|
||||
|
||||
def decorator(klass):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We capture the arguments to make returning them trivial
|
||||
@@ -25,7 +24,7 @@ def deconstructible(*args, path=None):
|
||||
"""
|
||||
# Fallback version
|
||||
if path:
|
||||
module_name, _, name = path.rpartition(".")
|
||||
module_name, _, name = path.rpartition('.')
|
||||
else:
|
||||
module_name = obj.__module__
|
||||
name = obj.__class__.__name__
|
||||
@@ -38,11 +37,10 @@ def deconstructible(*args, path=None):
|
||||
"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())
|
||||
)
|
||||
"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),
|
||||
path or '%s.%s' % (obj.__class__.__module__, name),
|
||||
obj._constructor_args[0],
|
||||
obj._constructor_args[1],
|
||||
)
|
||||
|
||||
@@ -6,9 +6,7 @@ from functools import partial, update_wrapper, wraps
|
||||
class classonlymethod(classmethod):
|
||||
def __get__(self, instance, cls=None):
|
||||
if instance is not None:
|
||||
raise AttributeError(
|
||||
"This method is available only on the class, not on instances."
|
||||
)
|
||||
raise AttributeError("This method is available only on the class, not on instances.")
|
||||
return super().__get__(instance, cls)
|
||||
|
||||
|
||||
@@ -18,7 +16,6 @@ def _update_method_wrapper(_wrapper, decorator):
|
||||
@decorator
|
||||
def dummy(*args, **kwargs):
|
||||
pass
|
||||
|
||||
update_wrapper(_wrapper, dummy)
|
||||
|
||||
|
||||
@@ -27,7 +24,7 @@ def _multi_decorate(decorators, method):
|
||||
Decorate `method` with one or more function decorators. `decorators` can be
|
||||
a single decorator or an iterable of decorators.
|
||||
"""
|
||||
if hasattr(decorators, "__iter__"):
|
||||
if hasattr(decorators, '__iter__'):
|
||||
# Apply a list/tuple of decorators if 'decorators' is one. Decorator
|
||||
# functions are applied so that the call order is the same as the
|
||||
# order in which they appear in the iterable.
|
||||
@@ -40,7 +37,7 @@ def _multi_decorate(decorators, method):
|
||||
# 'self' argument, but it's a closure over self so it can call
|
||||
# 'func'. Also, wrap method.__get__() in a function because new
|
||||
# attributes can't be set on bound method objects, only on functions.
|
||||
bound_method = wraps(method)(partial(method.__get__(self, type(self))))
|
||||
bound_method = partial(method.__get__(self, type(self)))
|
||||
for dec in decorators:
|
||||
bound_method = dec(bound_method)
|
||||
return bound_method(*args, **kwargs)
|
||||
@@ -53,7 +50,7 @@ def _multi_decorate(decorators, method):
|
||||
return _wrapper
|
||||
|
||||
|
||||
def method_decorator(decorator, name=""):
|
||||
def method_decorator(decorator, name=''):
|
||||
"""
|
||||
Convert a function decorator into a method decorator
|
||||
"""
|
||||
@@ -81,11 +78,11 @@ def method_decorator(decorator, name=""):
|
||||
|
||||
# Don't worry about making _dec look similar to a list/tuple as it's rather
|
||||
# meaningless.
|
||||
if not hasattr(decorator, "__iter__"):
|
||||
if not hasattr(decorator, '__iter__'):
|
||||
update_wrapper(_dec, decorator)
|
||||
# Change the name to aid debugging.
|
||||
obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
|
||||
_dec.__name__ = "method_decorator(%s)" % obj.__name__
|
||||
obj = decorator if hasattr(decorator, '__name__') else decorator.__class__
|
||||
_dec.__name__ = 'method_decorator(%s)' % obj.__name__
|
||||
return _dec
|
||||
|
||||
|
||||
@@ -121,44 +118,37 @@ def make_middleware_decorator(middleware_class):
|
||||
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if hasattr(middleware, "process_request"):
|
||||
if hasattr(middleware, 'process_request'):
|
||||
result = middleware.process_request(request)
|
||||
if result is not None:
|
||||
return result
|
||||
if hasattr(middleware, "process_view"):
|
||||
if hasattr(middleware, 'process_view'):
|
||||
result = middleware.process_view(request, view_func, args, kwargs)
|
||||
if result is not None:
|
||||
return result
|
||||
try:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
except Exception as e:
|
||||
if hasattr(middleware, "process_exception"):
|
||||
if hasattr(middleware, 'process_exception'):
|
||||
result = middleware.process_exception(request, e)
|
||||
if result is not None:
|
||||
return result
|
||||
raise
|
||||
if hasattr(response, "render") and callable(response.render):
|
||||
if hasattr(middleware, "process_template_response"):
|
||||
response = middleware.process_template_response(
|
||||
request, response
|
||||
)
|
||||
if hasattr(response, 'render') and callable(response.render):
|
||||
if hasattr(middleware, 'process_template_response'):
|
||||
response = middleware.process_template_response(request, response)
|
||||
# Defer running of process_response until after the template
|
||||
# has been rendered:
|
||||
if hasattr(middleware, "process_response"):
|
||||
|
||||
if hasattr(middleware, 'process_response'):
|
||||
def callback(response):
|
||||
return middleware.process_response(request, response)
|
||||
|
||||
response.add_post_render_callback(callback)
|
||||
else:
|
||||
if hasattr(middleware, "process_response"):
|
||||
if hasattr(middleware, 'process_response'):
|
||||
return middleware.process_response(request, response)
|
||||
return response
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
return _decorator
|
||||
|
||||
return _make_decorator
|
||||
|
||||
|
||||
|
||||
@@ -5,21 +5,19 @@ import warnings
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
class RemovedInDjango41Warning(DeprecationWarning):
|
||||
class RemovedInDjango40Warning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class RemovedInDjango50Warning(PendingDeprecationWarning):
|
||||
class RemovedInDjango41Warning(PendingDeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
RemovedInNextVersionWarning = RemovedInDjango41Warning
|
||||
RemovedInNextVersionWarning = RemovedInDjango40Warning
|
||||
|
||||
|
||||
class warn_about_renamed_method:
|
||||
def __init__(
|
||||
self, class_name, old_method_name, new_method_name, deprecation_warning
|
||||
):
|
||||
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
|
||||
@@ -28,13 +26,10 @@ class warn_about_renamed_method:
|
||||
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,
|
||||
)
|
||||
"`%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
|
||||
|
||||
|
||||
@@ -68,11 +63,9 @@ class RenameMethodsBase(type):
|
||||
# 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,
|
||||
)
|
||||
"`%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))
|
||||
|
||||
@@ -87,8 +80,7 @@ class DeprecationInstanceCheck(type):
|
||||
def __instancecheck__(self, instance):
|
||||
warnings.warn(
|
||||
"`%s` is deprecated, use `%s` instead." % (self.__name__, self.alternative),
|
||||
self.deprecation_warning,
|
||||
2,
|
||||
self.deprecation_warning, 2
|
||||
)
|
||||
return super().__instancecheck__(instance)
|
||||
|
||||
@@ -97,23 +89,14 @@ class MiddlewareMixin:
|
||||
sync_capable = True
|
||||
async_capable = True
|
||||
|
||||
def __init__(self, get_response):
|
||||
if get_response is None:
|
||||
raise ValueError("get_response must be provided.")
|
||||
# RemovedInDjango40Warning: when the deprecation ends, replace with:
|
||||
# def __init__(self, get_response):
|
||||
def __init__(self, get_response=None):
|
||||
self._get_response_none_deprecation(get_response)
|
||||
self.get_response = get_response
|
||||
self._async_check()
|
||||
super().__init__()
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s get_response=%s>" % (
|
||||
self.__class__.__qualname__,
|
||||
getattr(
|
||||
self.get_response,
|
||||
"__qualname__",
|
||||
self.get_response.__class__.__name__,
|
||||
),
|
||||
)
|
||||
|
||||
def _async_check(self):
|
||||
"""
|
||||
If get_response is a coroutine function, turns us into async mode so
|
||||
@@ -129,10 +112,10 @@ class MiddlewareMixin:
|
||||
if asyncio.iscoroutinefunction(self.get_response):
|
||||
return self.__acall__(request)
|
||||
response = None
|
||||
if hasattr(self, "process_request"):
|
||||
if hasattr(self, 'process_request'):
|
||||
response = self.process_request(request)
|
||||
response = response or self.get_response(request)
|
||||
if hasattr(self, "process_response"):
|
||||
if hasattr(self, 'process_response'):
|
||||
response = self.process_response(request, response)
|
||||
return response
|
||||
|
||||
@@ -142,15 +125,23 @@ class MiddlewareMixin:
|
||||
is running.
|
||||
"""
|
||||
response = None
|
||||
if hasattr(self, "process_request"):
|
||||
if hasattr(self, 'process_request'):
|
||||
response = await sync_to_async(
|
||||
self.process_request,
|
||||
thread_sensitive=True,
|
||||
)(request)
|
||||
response = response or await self.get_response(request)
|
||||
if hasattr(self, "process_response"):
|
||||
if hasattr(self, 'process_response'):
|
||||
response = await sync_to_async(
|
||||
self.process_response,
|
||||
thread_sensitive=True,
|
||||
)(request, response)
|
||||
return response
|
||||
|
||||
def _get_response_none_deprecation(self, get_response):
|
||||
if get_response is None:
|
||||
warnings.warn(
|
||||
'Passing None for the middleware get_response argument is '
|
||||
'deprecated.',
|
||||
RemovedInDjango40Warning, stacklevel=3,
|
||||
)
|
||||
|
||||
@@ -19,27 +19,25 @@ 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)
|
||||
string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
|
||||
if days:
|
||||
string = "{} ".format(days) + string
|
||||
string = '{} '.format(days) + string
|
||||
if microseconds:
|
||||
string += ".{:06d}".format(microseconds)
|
||||
string += '.{:06d}'.format(microseconds)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def duration_iso_string(duration):
|
||||
if duration < datetime.timedelta(0):
|
||||
sign = "-"
|
||||
sign = '-'
|
||||
duration *= -1
|
||||
else:
|
||||
sign = ""
|
||||
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
|
||||
)
|
||||
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):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import codecs
|
||||
import datetime
|
||||
import locale
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.utils.deprecation import RemovedInDjango40Warning
|
||||
from django.utils.functional import Promise
|
||||
|
||||
|
||||
@@ -13,14 +15,10 @@ class DjangoUnicodeDecodeError(UnicodeDecodeError):
|
||||
super().__init__(*args)
|
||||
|
||||
def __str__(self):
|
||||
return "%s. You passed in %r (%s)" % (
|
||||
super().__str__(),
|
||||
self.obj,
|
||||
type(self.obj),
|
||||
)
|
||||
return '%s. You passed in %r (%s)' % (super().__str__(), self.obj, type(self.obj))
|
||||
|
||||
|
||||
def smart_str(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
"""
|
||||
Return a string representing 's'. Treat bytestrings using the 'encoding'
|
||||
codec.
|
||||
@@ -34,13 +32,7 @@ def smart_str(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
|
||||
|
||||
_PROTECTED_TYPES = (
|
||||
type(None),
|
||||
int,
|
||||
float,
|
||||
Decimal,
|
||||
datetime.datetime,
|
||||
datetime.date,
|
||||
datetime.time,
|
||||
type(None), int, float, Decimal, datetime.datetime, datetime.date, datetime.time,
|
||||
)
|
||||
|
||||
|
||||
@@ -53,7 +45,7 @@ def is_protected_type(obj):
|
||||
return isinstance(obj, _PROTECTED_TYPES)
|
||||
|
||||
|
||||
def force_str(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
def force_str(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
"""
|
||||
Similar to smart_str(), except that lazy instances are resolved to
|
||||
strings, rather than kept as lazy objects.
|
||||
@@ -75,7 +67,7 @@ def force_str(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
return s
|
||||
|
||||
|
||||
def smart_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
"""
|
||||
Return a bytestring version of 's', encoded as specified in 'encoding'.
|
||||
|
||||
@@ -87,7 +79,7 @@ def smart_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
return force_bytes(s, encoding, strings_only, errors)
|
||||
|
||||
|
||||
def force_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
"""
|
||||
Similar to smart_bytes, except that lazy instances are resolved to
|
||||
strings, rather than kept as lazy objects.
|
||||
@@ -96,10 +88,10 @@ def force_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
"""
|
||||
# Handle the common case first for performance reasons.
|
||||
if isinstance(s, bytes):
|
||||
if encoding == "utf-8":
|
||||
if encoding == 'utf-8':
|
||||
return s
|
||||
else:
|
||||
return s.decode("utf-8", errors).encode(encoding, errors)
|
||||
return s.decode('utf-8', errors).encode(encoding, errors)
|
||||
if strings_only and is_protected_type(s):
|
||||
return s
|
||||
if isinstance(s, memoryview):
|
||||
@@ -107,6 +99,22 @@ def force_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
return str(s).encode(encoding, errors)
|
||||
|
||||
|
||||
def smart_text(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
warnings.warn(
|
||||
'smart_text() is deprecated in favor of smart_str().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return smart_str(s, encoding, strings_only, errors)
|
||||
|
||||
|
||||
def force_text(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
warnings.warn(
|
||||
'force_text() is deprecated in favor of force_str().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return force_str(s, encoding, strings_only, errors)
|
||||
|
||||
|
||||
def iri_to_uri(iri):
|
||||
"""
|
||||
Convert an Internationalized Resource Identifier (IRI) portion to a URI
|
||||
@@ -146,14 +154,15 @@ _hextobyte = {
|
||||
(fmt % char).encode(): bytes((char,))
|
||||
for ascii_range in _ascii_ranges
|
||||
for char in ascii_range
|
||||
for fmt in ["%02x", "%02X"]
|
||||
for fmt in ['%02x', '%02X']
|
||||
}
|
||||
# And then everything above 128, because bytes ≥ 128 are part of multibyte
|
||||
# Unicode characters.
|
||||
_hexdig = "0123456789ABCDEFabcdef"
|
||||
_hextobyte.update(
|
||||
{(a + b).encode(): bytes.fromhex(a + b) for a in _hexdig[8:] for b in _hexdig}
|
||||
)
|
||||
_hexdig = '0123456789ABCDEFabcdef'
|
||||
_hextobyte.update({
|
||||
(a + b).encode(): bytes.fromhex(a + b)
|
||||
for a in _hexdig[8:] for b in _hexdig
|
||||
})
|
||||
|
||||
|
||||
def uri_to_iri(uri):
|
||||
@@ -169,11 +178,11 @@ def uri_to_iri(uri):
|
||||
if uri is None:
|
||||
return uri
|
||||
uri = force_bytes(uri)
|
||||
# Fast selective unquote: First, split on '%' and then starting with the
|
||||
# Fast selective unqote: First, split on '%' and then starting with the
|
||||
# second block, decode the first 2 bytes if they represent a hex code to
|
||||
# decode. The rest of the block is the part after '%AB', not containing
|
||||
# any '%'. Add that to the output without further processing.
|
||||
bits = uri.split(b"%")
|
||||
bits = uri.split(b'%')
|
||||
if len(bits) == 1:
|
||||
iri = uri
|
||||
else:
|
||||
@@ -186,9 +195,9 @@ def uri_to_iri(uri):
|
||||
append(hextobyte[item[:2]])
|
||||
append(item[2:])
|
||||
else:
|
||||
append(b"%")
|
||||
append(b'%')
|
||||
append(item)
|
||||
iri = b"".join(parts)
|
||||
iri = b''.join(parts)
|
||||
return repercent_broken_unicode(iri).decode()
|
||||
|
||||
|
||||
@@ -211,7 +220,7 @@ def escape_uri_path(path):
|
||||
|
||||
def punycode(domain):
|
||||
"""Return the Punycode of the given domain if it's non-ASCII."""
|
||||
return domain.encode("idna").decode("ascii")
|
||||
return domain.encode('idna').decode('ascii')
|
||||
|
||||
|
||||
def repercent_broken_unicode(path):
|
||||
@@ -226,8 +235,8 @@ def repercent_broken_unicode(path):
|
||||
except UnicodeDecodeError as e:
|
||||
# CVE-2019-14235: A recursion shouldn't be used since the exception
|
||||
# handling uses massive amounts of memory
|
||||
repercent = quote(path[e.start : e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
|
||||
path = path[: e.start] + repercent.encode() + path[e.end :]
|
||||
repercent = quote(path[e.start:e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
|
||||
path = path[:e.start] + repercent.encode() + path[e.end:]
|
||||
else:
|
||||
return path
|
||||
|
||||
@@ -254,10 +263,10 @@ def get_system_encoding():
|
||||
#10335 and #5846.
|
||||
"""
|
||||
try:
|
||||
encoding = locale.getdefaultlocale()[1] or "ascii"
|
||||
encoding = locale.getdefaultlocale()[1] or 'ascii'
|
||||
codecs.lookup(encoding)
|
||||
except Exception:
|
||||
encoding = "ascii"
|
||||
encoding = 'ascii'
|
||||
return encoding
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Sample usage:
|
||||
>>> feed = feedgenerator.Rss201rev2Feed(
|
||||
... title="Poynter E-Media Tidbits",
|
||||
... link="http://www.poynter.org/column.asp?id=31",
|
||||
... description="A group blog by the sharpest minds in online journalism.",
|
||||
... description="A group Weblog by the sharpest minds in online media/journalism/publishing.",
|
||||
... language="en",
|
||||
... )
|
||||
>>> feed.add_item(
|
||||
@@ -40,114 +40,78 @@ def rfc2822_date(date):
|
||||
def rfc3339_date(date):
|
||||
if not isinstance(date, datetime.datetime):
|
||||
date = datetime.datetime.combine(date, datetime.time())
|
||||
return date.isoformat() + ("Z" if date.utcoffset() is None else "")
|
||||
return date.isoformat() + ('Z' if date.utcoffset() is None else '')
|
||||
|
||||
|
||||
def get_tag_uri(url, date):
|
||||
"""
|
||||
Create a TagURI.
|
||||
|
||||
See
|
||||
https://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
|
||||
See https://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
|
||||
"""
|
||||
bits = urlparse(url)
|
||||
d = ""
|
||||
d = ''
|
||||
if date is not None:
|
||||
d = ",%s" % date.strftime("%Y-%m-%d")
|
||||
return "tag:%s%s:%s/%s" % (bits.hostname, d, bits.path, bits.fragment)
|
||||
d = ',%s' % date.strftime('%Y-%m-%d')
|
||||
return 'tag:%s%s:%s/%s' % (bits.hostname, d, bits.path, bits.fragment)
|
||||
|
||||
|
||||
class SyndicationFeed:
|
||||
"Base class for all syndication feeds. Subclasses should provide write()"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
language=None,
|
||||
author_email=None,
|
||||
author_name=None,
|
||||
author_link=None,
|
||||
subtitle=None,
|
||||
categories=None,
|
||||
feed_url=None,
|
||||
feed_copyright=None,
|
||||
feed_guid=None,
|
||||
ttl=None,
|
||||
**kwargs,
|
||||
):
|
||||
def __init__(self, title, link, description, language=None, author_email=None,
|
||||
author_name=None, author_link=None, subtitle=None, categories=None,
|
||||
feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
|
||||
def to_str(s):
|
||||
return str(s) if s is not None else s
|
||||
|
||||
categories = categories and [str(c) for c in categories]
|
||||
self.feed = {
|
||||
"title": to_str(title),
|
||||
"link": iri_to_uri(link),
|
||||
"description": to_str(description),
|
||||
"language": to_str(language),
|
||||
"author_email": to_str(author_email),
|
||||
"author_name": to_str(author_name),
|
||||
"author_link": iri_to_uri(author_link),
|
||||
"subtitle": to_str(subtitle),
|
||||
"categories": categories or (),
|
||||
"feed_url": iri_to_uri(feed_url),
|
||||
"feed_copyright": to_str(feed_copyright),
|
||||
"id": feed_guid or link,
|
||||
"ttl": to_str(ttl),
|
||||
'title': to_str(title),
|
||||
'link': iri_to_uri(link),
|
||||
'description': to_str(description),
|
||||
'language': to_str(language),
|
||||
'author_email': to_str(author_email),
|
||||
'author_name': to_str(author_name),
|
||||
'author_link': iri_to_uri(author_link),
|
||||
'subtitle': to_str(subtitle),
|
||||
'categories': categories or (),
|
||||
'feed_url': iri_to_uri(feed_url),
|
||||
'feed_copyright': to_str(feed_copyright),
|
||||
'id': feed_guid or link,
|
||||
'ttl': to_str(ttl),
|
||||
**kwargs,
|
||||
}
|
||||
self.items = []
|
||||
|
||||
def add_item(
|
||||
self,
|
||||
title,
|
||||
link,
|
||||
description,
|
||||
author_email=None,
|
||||
author_name=None,
|
||||
author_link=None,
|
||||
pubdate=None,
|
||||
comments=None,
|
||||
unique_id=None,
|
||||
unique_id_is_permalink=None,
|
||||
categories=(),
|
||||
item_copyright=None,
|
||||
ttl=None,
|
||||
updateddate=None,
|
||||
enclosures=None,
|
||||
**kwargs,
|
||||
):
|
||||
def add_item(self, title, link, description, author_email=None,
|
||||
author_name=None, author_link=None, pubdate=None, comments=None,
|
||||
unique_id=None, unique_id_is_permalink=None, categories=(),
|
||||
item_copyright=None, ttl=None, updateddate=None, enclosures=None, **kwargs):
|
||||
"""
|
||||
Add an item to the feed. All args are expected to be strings except
|
||||
pubdate and updateddate, which are datetime.datetime objects, and
|
||||
enclosures, which is an iterable of instances of the Enclosure class.
|
||||
"""
|
||||
|
||||
def to_str(s):
|
||||
return str(s) if s is not None else s
|
||||
|
||||
categories = categories and [to_str(c) for c in categories]
|
||||
self.items.append(
|
||||
{
|
||||
"title": to_str(title),
|
||||
"link": iri_to_uri(link),
|
||||
"description": to_str(description),
|
||||
"author_email": to_str(author_email),
|
||||
"author_name": to_str(author_name),
|
||||
"author_link": iri_to_uri(author_link),
|
||||
"pubdate": pubdate,
|
||||
"updateddate": updateddate,
|
||||
"comments": to_str(comments),
|
||||
"unique_id": to_str(unique_id),
|
||||
"unique_id_is_permalink": unique_id_is_permalink,
|
||||
"enclosures": enclosures or (),
|
||||
"categories": categories or (),
|
||||
"item_copyright": to_str(item_copyright),
|
||||
"ttl": to_str(ttl),
|
||||
**kwargs,
|
||||
}
|
||||
)
|
||||
self.items.append({
|
||||
'title': to_str(title),
|
||||
'link': iri_to_uri(link),
|
||||
'description': to_str(description),
|
||||
'author_email': to_str(author_email),
|
||||
'author_name': to_str(author_name),
|
||||
'author_link': iri_to_uri(author_link),
|
||||
'pubdate': pubdate,
|
||||
'updateddate': updateddate,
|
||||
'comments': to_str(comments),
|
||||
'unique_id': to_str(unique_id),
|
||||
'unique_id_is_permalink': unique_id_is_permalink,
|
||||
'enclosures': enclosures or (),
|
||||
'categories': categories or (),
|
||||
'item_copyright': to_str(item_copyright),
|
||||
'ttl': to_str(ttl),
|
||||
**kwargs,
|
||||
})
|
||||
|
||||
def num_items(self):
|
||||
return len(self.items)
|
||||
@@ -183,9 +147,7 @@ class SyndicationFeed:
|
||||
Output the feed in the given encoding to outfile, which is a file-like
|
||||
object. Subclasses should override this.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of SyndicationFeed must provide a write() method"
|
||||
)
|
||||
raise NotImplementedError('subclasses of SyndicationFeed must provide a write() method')
|
||||
|
||||
def writeString(self, encoding):
|
||||
"""
|
||||
@@ -201,7 +163,7 @@ class SyndicationFeed:
|
||||
have either of these attributes this return the current UTC date/time.
|
||||
"""
|
||||
latest_date = None
|
||||
date_keys = ("updateddate", "pubdate")
|
||||
date_keys = ('updateddate', 'pubdate')
|
||||
|
||||
for item in self.items:
|
||||
for date_key in date_keys:
|
||||
@@ -210,12 +172,12 @@ class SyndicationFeed:
|
||||
if latest_date is None or item_date > latest_date:
|
||||
latest_date = item_date
|
||||
|
||||
return latest_date or datetime.datetime.now(tz=utc)
|
||||
# datetime.now(tz=utc) is slower, as documented in django.utils.timezone.now
|
||||
return latest_date or datetime.datetime.utcnow().replace(tzinfo=utc)
|
||||
|
||||
|
||||
class Enclosure:
|
||||
"""An RSS enclosure"""
|
||||
|
||||
def __init__(self, url, length, mime_type):
|
||||
"All args are expected to be strings"
|
||||
self.length, self.mime_type = length, mime_type
|
||||
@@ -223,10 +185,10 @@ class Enclosure:
|
||||
|
||||
|
||||
class RssFeed(SyndicationFeed):
|
||||
content_type = "application/rss+xml; charset=utf-8"
|
||||
content_type = 'application/rss+xml; charset=utf-8'
|
||||
|
||||
def write(self, outfile, encoding):
|
||||
handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
|
||||
handler = SimplerXMLGenerator(outfile, encoding)
|
||||
handler.startDocument()
|
||||
handler.startElement("rss", self.rss_attributes())
|
||||
handler.startElement("channel", self.root_attributes())
|
||||
@@ -237,33 +199,31 @@ class RssFeed(SyndicationFeed):
|
||||
|
||||
def rss_attributes(self):
|
||||
return {
|
||||
"version": self._version,
|
||||
"xmlns:atom": "http://www.w3.org/2005/Atom",
|
||||
'version': self._version,
|
||||
'xmlns:atom': 'http://www.w3.org/2005/Atom',
|
||||
}
|
||||
|
||||
def write_items(self, handler):
|
||||
for item in self.items:
|
||||
handler.startElement("item", self.item_attributes(item))
|
||||
handler.startElement('item', self.item_attributes(item))
|
||||
self.add_item_elements(handler, item)
|
||||
handler.endElement("item")
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
handler.addQuickElement("title", self.feed["title"])
|
||||
handler.addQuickElement("link", self.feed["link"])
|
||||
handler.addQuickElement("description", self.feed["description"])
|
||||
if self.feed["feed_url"] is not None:
|
||||
handler.addQuickElement(
|
||||
"atom:link", None, {"rel": "self", "href": self.feed["feed_url"]}
|
||||
)
|
||||
if self.feed["language"] is not None:
|
||||
handler.addQuickElement("language", self.feed["language"])
|
||||
for cat in self.feed["categories"]:
|
||||
handler.addQuickElement("title", self.feed['title'])
|
||||
handler.addQuickElement("link", self.feed['link'])
|
||||
handler.addQuickElement("description", self.feed['description'])
|
||||
if self.feed['feed_url'] is not None:
|
||||
handler.addQuickElement("atom:link", None, {"rel": "self", "href": self.feed['feed_url']})
|
||||
if self.feed['language'] is not None:
|
||||
handler.addQuickElement("language", self.feed['language'])
|
||||
for cat in self.feed['categories']:
|
||||
handler.addQuickElement("category", cat)
|
||||
if self.feed["feed_copyright"] is not None:
|
||||
handler.addQuickElement("copyright", self.feed["feed_copyright"])
|
||||
if self.feed['feed_copyright'] is not None:
|
||||
handler.addQuickElement("copyright", self.feed['feed_copyright'])
|
||||
handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
|
||||
if self.feed["ttl"] is not None:
|
||||
handler.addQuickElement("ttl", self.feed["ttl"])
|
||||
if self.feed['ttl'] is not None:
|
||||
handler.addQuickElement("ttl", self.feed['ttl'])
|
||||
|
||||
def endChannelElement(self, handler):
|
||||
handler.endElement("channel")
|
||||
@@ -273,10 +233,10 @@ class RssUserland091Feed(RssFeed):
|
||||
_version = "0.91"
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
handler.addQuickElement("title", item["title"])
|
||||
handler.addQuickElement("link", item["link"])
|
||||
if item["description"] is not None:
|
||||
handler.addQuickElement("description", item["description"])
|
||||
handler.addQuickElement("title", item['title'])
|
||||
handler.addQuickElement("link", item['link'])
|
||||
if item['description'] is not None:
|
||||
handler.addQuickElement("description", item['description'])
|
||||
|
||||
|
||||
class Rss201rev2Feed(RssFeed):
|
||||
@@ -284,105 +244,93 @@ class Rss201rev2Feed(RssFeed):
|
||||
_version = "2.0"
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
handler.addQuickElement("title", item["title"])
|
||||
handler.addQuickElement("link", item["link"])
|
||||
if item["description"] is not None:
|
||||
handler.addQuickElement("description", item["description"])
|
||||
handler.addQuickElement("title", item['title'])
|
||||
handler.addQuickElement("link", item['link'])
|
||||
if item['description'] is not None:
|
||||
handler.addQuickElement("description", item['description'])
|
||||
|
||||
# Author information.
|
||||
if item["author_name"] and item["author_email"]:
|
||||
handler.addQuickElement(
|
||||
"author", "%s (%s)" % (item["author_email"], item["author_name"])
|
||||
)
|
||||
handler.addQuickElement("author", "%s (%s)" % (item['author_email'], item['author_name']))
|
||||
elif item["author_email"]:
|
||||
handler.addQuickElement("author", item["author_email"])
|
||||
elif item["author_name"]:
|
||||
handler.addQuickElement(
|
||||
"dc:creator",
|
||||
item["author_name"],
|
||||
{"xmlns:dc": "http://purl.org/dc/elements/1.1/"},
|
||||
"dc:creator", item["author_name"], {"xmlns:dc": "http://purl.org/dc/elements/1.1/"}
|
||||
)
|
||||
|
||||
if item["pubdate"] is not None:
|
||||
handler.addQuickElement("pubDate", rfc2822_date(item["pubdate"]))
|
||||
if item["comments"] is not None:
|
||||
handler.addQuickElement("comments", item["comments"])
|
||||
if item["unique_id"] is not None:
|
||||
if item['pubdate'] is not None:
|
||||
handler.addQuickElement("pubDate", rfc2822_date(item['pubdate']))
|
||||
if item['comments'] is not None:
|
||||
handler.addQuickElement("comments", item['comments'])
|
||||
if item['unique_id'] is not None:
|
||||
guid_attrs = {}
|
||||
if isinstance(item.get("unique_id_is_permalink"), bool):
|
||||
guid_attrs["isPermaLink"] = str(item["unique_id_is_permalink"]).lower()
|
||||
handler.addQuickElement("guid", item["unique_id"], guid_attrs)
|
||||
if item["ttl"] is not None:
|
||||
handler.addQuickElement("ttl", item["ttl"])
|
||||
if isinstance(item.get('unique_id_is_permalink'), bool):
|
||||
guid_attrs['isPermaLink'] = str(item['unique_id_is_permalink']).lower()
|
||||
handler.addQuickElement("guid", item['unique_id'], guid_attrs)
|
||||
if item['ttl'] is not None:
|
||||
handler.addQuickElement("ttl", item['ttl'])
|
||||
|
||||
# Enclosure.
|
||||
if item["enclosures"]:
|
||||
enclosures = list(item["enclosures"])
|
||||
if item['enclosures']:
|
||||
enclosures = list(item['enclosures'])
|
||||
if len(enclosures) > 1:
|
||||
raise ValueError(
|
||||
"RSS feed items may only have one enclosure, see "
|
||||
"http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
|
||||
)
|
||||
enclosure = enclosures[0]
|
||||
handler.addQuickElement(
|
||||
"enclosure",
|
||||
"",
|
||||
{
|
||||
"url": enclosure.url,
|
||||
"length": enclosure.length,
|
||||
"type": enclosure.mime_type,
|
||||
},
|
||||
)
|
||||
handler.addQuickElement('enclosure', '', {
|
||||
'url': enclosure.url,
|
||||
'length': enclosure.length,
|
||||
'type': enclosure.mime_type,
|
||||
})
|
||||
|
||||
# Categories.
|
||||
for cat in item["categories"]:
|
||||
for cat in item['categories']:
|
||||
handler.addQuickElement("category", cat)
|
||||
|
||||
|
||||
class Atom1Feed(SyndicationFeed):
|
||||
# Spec: https://tools.ietf.org/html/rfc4287
|
||||
content_type = "application/atom+xml; charset=utf-8"
|
||||
content_type = 'application/atom+xml; charset=utf-8'
|
||||
ns = "http://www.w3.org/2005/Atom"
|
||||
|
||||
def write(self, outfile, encoding):
|
||||
handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
|
||||
handler = SimplerXMLGenerator(outfile, encoding)
|
||||
handler.startDocument()
|
||||
handler.startElement("feed", self.root_attributes())
|
||||
handler.startElement('feed', self.root_attributes())
|
||||
self.add_root_elements(handler)
|
||||
self.write_items(handler)
|
||||
handler.endElement("feed")
|
||||
|
||||
def root_attributes(self):
|
||||
if self.feed["language"] is not None:
|
||||
return {"xmlns": self.ns, "xml:lang": self.feed["language"]}
|
||||
if self.feed['language'] is not None:
|
||||
return {"xmlns": self.ns, "xml:lang": self.feed['language']}
|
||||
else:
|
||||
return {"xmlns": self.ns}
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
handler.addQuickElement("title", self.feed["title"])
|
||||
handler.addQuickElement(
|
||||
"link", "", {"rel": "alternate", "href": self.feed["link"]}
|
||||
)
|
||||
if self.feed["feed_url"] is not None:
|
||||
handler.addQuickElement(
|
||||
"link", "", {"rel": "self", "href": self.feed["feed_url"]}
|
||||
)
|
||||
handler.addQuickElement("id", self.feed["id"])
|
||||
handler.addQuickElement("title", self.feed['title'])
|
||||
handler.addQuickElement("link", "", {"rel": "alternate", "href": self.feed['link']})
|
||||
if self.feed['feed_url'] is not None:
|
||||
handler.addQuickElement("link", "", {"rel": "self", "href": self.feed['feed_url']})
|
||||
handler.addQuickElement("id", self.feed['id'])
|
||||
handler.addQuickElement("updated", rfc3339_date(self.latest_post_date()))
|
||||
if self.feed["author_name"] is not None:
|
||||
if self.feed['author_name'] is not None:
|
||||
handler.startElement("author", {})
|
||||
handler.addQuickElement("name", self.feed["author_name"])
|
||||
if self.feed["author_email"] is not None:
|
||||
handler.addQuickElement("email", self.feed["author_email"])
|
||||
if self.feed["author_link"] is not None:
|
||||
handler.addQuickElement("uri", self.feed["author_link"])
|
||||
handler.addQuickElement("name", self.feed['author_name'])
|
||||
if self.feed['author_email'] is not None:
|
||||
handler.addQuickElement("email", self.feed['author_email'])
|
||||
if self.feed['author_link'] is not None:
|
||||
handler.addQuickElement("uri", self.feed['author_link'])
|
||||
handler.endElement("author")
|
||||
if self.feed["subtitle"] is not None:
|
||||
handler.addQuickElement("subtitle", self.feed["subtitle"])
|
||||
for cat in self.feed["categories"]:
|
||||
if self.feed['subtitle'] is not None:
|
||||
handler.addQuickElement("subtitle", self.feed['subtitle'])
|
||||
for cat in self.feed['categories']:
|
||||
handler.addQuickElement("category", "", {"term": cat})
|
||||
if self.feed["feed_copyright"] is not None:
|
||||
handler.addQuickElement("rights", self.feed["feed_copyright"])
|
||||
if self.feed['feed_copyright'] is not None:
|
||||
handler.addQuickElement("rights", self.feed['feed_copyright'])
|
||||
|
||||
def write_items(self, handler):
|
||||
for item in self.items:
|
||||
@@ -391,56 +339,52 @@ class Atom1Feed(SyndicationFeed):
|
||||
handler.endElement("entry")
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
handler.addQuickElement("title", item["title"])
|
||||
handler.addQuickElement("link", "", {"href": item["link"], "rel": "alternate"})
|
||||
handler.addQuickElement("title", item['title'])
|
||||
handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"})
|
||||
|
||||
if item["pubdate"] is not None:
|
||||
handler.addQuickElement("published", rfc3339_date(item["pubdate"]))
|
||||
if item['pubdate'] is not None:
|
||||
handler.addQuickElement('published', rfc3339_date(item['pubdate']))
|
||||
|
||||
if item["updateddate"] is not None:
|
||||
handler.addQuickElement("updated", rfc3339_date(item["updateddate"]))
|
||||
if item['updateddate'] is not None:
|
||||
handler.addQuickElement('updated', rfc3339_date(item['updateddate']))
|
||||
|
||||
# Author information.
|
||||
if item["author_name"] is not None:
|
||||
if item['author_name'] is not None:
|
||||
handler.startElement("author", {})
|
||||
handler.addQuickElement("name", item["author_name"])
|
||||
if item["author_email"] is not None:
|
||||
handler.addQuickElement("email", item["author_email"])
|
||||
if item["author_link"] is not None:
|
||||
handler.addQuickElement("uri", item["author_link"])
|
||||
handler.addQuickElement("name", item['author_name'])
|
||||
if item['author_email'] is not None:
|
||||
handler.addQuickElement("email", item['author_email'])
|
||||
if item['author_link'] is not None:
|
||||
handler.addQuickElement("uri", item['author_link'])
|
||||
handler.endElement("author")
|
||||
|
||||
# Unique ID.
|
||||
if item["unique_id"] is not None:
|
||||
unique_id = item["unique_id"]
|
||||
if item['unique_id'] is not None:
|
||||
unique_id = item['unique_id']
|
||||
else:
|
||||
unique_id = get_tag_uri(item["link"], item["pubdate"])
|
||||
unique_id = get_tag_uri(item['link'], item['pubdate'])
|
||||
handler.addQuickElement("id", unique_id)
|
||||
|
||||
# Summary.
|
||||
if item["description"] is not None:
|
||||
handler.addQuickElement("summary", item["description"], {"type": "html"})
|
||||
if item['description'] is not None:
|
||||
handler.addQuickElement("summary", item['description'], {"type": "html"})
|
||||
|
||||
# Enclosures.
|
||||
for enclosure in item["enclosures"]:
|
||||
handler.addQuickElement(
|
||||
"link",
|
||||
"",
|
||||
{
|
||||
"rel": "enclosure",
|
||||
"href": enclosure.url,
|
||||
"length": enclosure.length,
|
||||
"type": enclosure.mime_type,
|
||||
},
|
||||
)
|
||||
for enclosure in item['enclosures']:
|
||||
handler.addQuickElement('link', '', {
|
||||
'rel': 'enclosure',
|
||||
'href': enclosure.url,
|
||||
'length': enclosure.length,
|
||||
'type': enclosure.mime_type,
|
||||
})
|
||||
|
||||
# Categories.
|
||||
for cat in item["categories"]:
|
||||
for cat in item['categories']:
|
||||
handler.addQuickElement("category", "", {"term": cat})
|
||||
|
||||
# Rights.
|
||||
if item["item_copyright"] is not None:
|
||||
handler.addQuickElement("rights", item["item_copyright"])
|
||||
if item['item_copyright'] is not None:
|
||||
handler.addQuickElement("rights", item['item_copyright'])
|
||||
|
||||
|
||||
# This isolates the decision of what the system default is, so calling code can
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import functools
|
||||
import re
|
||||
import unicodedata
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import dateformat, numberformat
|
||||
from django.utils import dateformat, datetime_safe, numberformat
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.translation import check_for_language, get_language, to_locale
|
||||
from django.utils.translation import (
|
||||
check_for_language, get_language, to_locale,
|
||||
)
|
||||
|
||||
# format_cache is a mapping from (format_type, lang) to the format string.
|
||||
# By using the cache, it is possible to avoid running get_format_modules
|
||||
@@ -17,35 +17,33 @@ _format_cache = {}
|
||||
_format_modules_cache = {}
|
||||
|
||||
ISO_INPUT_FORMATS = {
|
||||
"DATE_INPUT_FORMATS": ["%Y-%m-%d"],
|
||||
"TIME_INPUT_FORMATS": ["%H:%M:%S", "%H:%M:%S.%f", "%H:%M"],
|
||||
"DATETIME_INPUT_FORMATS": [
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M:%S.%f",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y-%m-%d",
|
||||
'DATE_INPUT_FORMATS': ['%Y-%m-%d'],
|
||||
'TIME_INPUT_FORMATS': ['%H:%M:%S', '%H:%M:%S.%f', '%H:%M'],
|
||||
'DATETIME_INPUT_FORMATS': [
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%Y-%m-%d %H:%M:%S.%f',
|
||||
'%Y-%m-%d %H:%M',
|
||||
'%Y-%m-%d'
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
FORMAT_SETTINGS = frozenset(
|
||||
[
|
||||
"DECIMAL_SEPARATOR",
|
||||
"THOUSAND_SEPARATOR",
|
||||
"NUMBER_GROUPING",
|
||||
"FIRST_DAY_OF_WEEK",
|
||||
"MONTH_DAY_FORMAT",
|
||||
"TIME_FORMAT",
|
||||
"DATE_FORMAT",
|
||||
"DATETIME_FORMAT",
|
||||
"SHORT_DATE_FORMAT",
|
||||
"SHORT_DATETIME_FORMAT",
|
||||
"YEAR_MONTH_FORMAT",
|
||||
"DATE_INPUT_FORMATS",
|
||||
"TIME_INPUT_FORMATS",
|
||||
"DATETIME_INPUT_FORMATS",
|
||||
]
|
||||
)
|
||||
FORMAT_SETTINGS = frozenset([
|
||||
'DECIMAL_SEPARATOR',
|
||||
'THOUSAND_SEPARATOR',
|
||||
'NUMBER_GROUPING',
|
||||
'FIRST_DAY_OF_WEEK',
|
||||
'MONTH_DAY_FORMAT',
|
||||
'TIME_FORMAT',
|
||||
'DATE_FORMAT',
|
||||
'DATETIME_FORMAT',
|
||||
'SHORT_DATE_FORMAT',
|
||||
'SHORT_DATETIME_FORMAT',
|
||||
'YEAR_MONTH_FORMAT',
|
||||
'DATE_INPUT_FORMATS',
|
||||
'TIME_INPUT_FORMATS',
|
||||
'DATETIME_INPUT_FORMATS',
|
||||
])
|
||||
|
||||
|
||||
def reset_format_cache():
|
||||
@@ -72,29 +70,30 @@ def iter_format_modules(lang, format_module_path=None):
|
||||
if isinstance(format_module_path, str):
|
||||
format_module_path = [format_module_path]
|
||||
for path in format_module_path:
|
||||
format_locations.append(path + ".%s")
|
||||
format_locations.append("django.conf.locale.%s")
|
||||
format_locations.append(path + '.%s')
|
||||
format_locations.append('django.conf.locale.%s')
|
||||
locale = to_locale(lang)
|
||||
locales = [locale]
|
||||
if "_" in locale:
|
||||
locales.append(locale.split("_")[0])
|
||||
if '_' in locale:
|
||||
locales.append(locale.split('_')[0])
|
||||
for location in format_locations:
|
||||
for loc in locales:
|
||||
try:
|
||||
yield import_module("%s.formats" % (location % loc))
|
||||
yield import_module('%s.formats' % (location % loc))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def get_format_modules(lang=None):
|
||||
def get_format_modules(lang=None, reverse=False):
|
||||
"""Return a list of the format modules found."""
|
||||
if lang is None:
|
||||
lang = get_language()
|
||||
if lang not in _format_modules_cache:
|
||||
_format_modules_cache[lang] = list(
|
||||
iter_format_modules(lang, settings.FORMAT_MODULE_PATH)
|
||||
)
|
||||
return _format_modules_cache[lang]
|
||||
_format_modules_cache[lang] = list(iter_format_modules(lang, settings.FORMAT_MODULE_PATH))
|
||||
modules = _format_modules_cache[lang]
|
||||
if reverse:
|
||||
return list(reversed(modules))
|
||||
return modules
|
||||
|
||||
|
||||
def get_format(format_type, lang=None, use_l10n=None):
|
||||
@@ -106,14 +105,7 @@ def get_format(format_type, lang=None, use_l10n=None):
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), overriding the value of settings.USE_L10N.
|
||||
"""
|
||||
use_l10n = use_l10n or (
|
||||
use_l10n is None
|
||||
and (
|
||||
settings._USE_L10N_INTERNAL
|
||||
if hasattr(settings, "_USE_L10N_INTERNAL")
|
||||
else settings.USE_L10N
|
||||
)
|
||||
)
|
||||
use_l10n = use_l10n or (use_l10n is None and settings.USE_L10N)
|
||||
if use_l10n and lang is None:
|
||||
lang = get_language()
|
||||
cache_key = (format_type, lang)
|
||||
@@ -157,9 +149,7 @@ def date_format(value, format=None, use_l10n=None):
|
||||
If use_l10n is provided and is not None, that will force the value to
|
||||
be localized (or not), overriding the value of settings.USE_L10N.
|
||||
"""
|
||||
return dateformat.format(
|
||||
value, get_format(format or "DATE_FORMAT", use_l10n=use_l10n)
|
||||
)
|
||||
return dateformat.format(value, get_format(format or 'DATE_FORMAT', use_l10n=use_l10n))
|
||||
|
||||
|
||||
def time_format(value, format=None, use_l10n=None):
|
||||
@@ -169,9 +159,7 @@ def time_format(value, format=None, use_l10n=None):
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), overriding the value of settings.USE_L10N.
|
||||
"""
|
||||
return dateformat.time_format(
|
||||
value, get_format(format or "TIME_FORMAT", use_l10n=use_l10n)
|
||||
)
|
||||
return dateformat.time_format(value, get_format(format or 'TIME_FORMAT', use_l10n=use_l10n))
|
||||
|
||||
|
||||
def number_format(value, decimal_pos=None, use_l10n=None, force_grouping=False):
|
||||
@@ -181,21 +169,16 @@ def number_format(value, decimal_pos=None, use_l10n=None, force_grouping=False):
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), overriding the value of settings.USE_L10N.
|
||||
"""
|
||||
use_l10n = use_l10n or (
|
||||
use_l10n is None
|
||||
and (
|
||||
settings._USE_L10N_INTERNAL
|
||||
if hasattr(settings, "_USE_L10N_INTERNAL")
|
||||
else settings.USE_L10N
|
||||
)
|
||||
)
|
||||
lang = get_language() if use_l10n else None
|
||||
if use_l10n or (use_l10n is None and settings.USE_L10N):
|
||||
lang = get_language()
|
||||
else:
|
||||
lang = None
|
||||
return numberformat.format(
|
||||
value,
|
||||
get_format("DECIMAL_SEPARATOR", lang, use_l10n=use_l10n),
|
||||
get_format('DECIMAL_SEPARATOR', lang, use_l10n=use_l10n),
|
||||
decimal_pos,
|
||||
get_format("NUMBER_GROUPING", lang, use_l10n=use_l10n),
|
||||
get_format("THOUSAND_SEPARATOR", lang, use_l10n=use_l10n),
|
||||
get_format('NUMBER_GROUPING', lang, use_l10n=use_l10n),
|
||||
get_format('THOUSAND_SEPARATOR', lang, use_l10n=use_l10n),
|
||||
force_grouping=force_grouping,
|
||||
use_l10n=use_l10n,
|
||||
)
|
||||
@@ -218,11 +201,11 @@ def localize(value, use_l10n=None):
|
||||
return str(value)
|
||||
return number_format(value, use_l10n=use_l10n)
|
||||
elif isinstance(value, datetime.datetime):
|
||||
return date_format(value, "DATETIME_FORMAT", use_l10n=use_l10n)
|
||||
return date_format(value, 'DATETIME_FORMAT', use_l10n=use_l10n)
|
||||
elif isinstance(value, datetime.date):
|
||||
return date_format(value, use_l10n=use_l10n)
|
||||
elif isinstance(value, datetime.time):
|
||||
return time_format(value, "TIME_FORMAT", use_l10n=use_l10n)
|
||||
return time_format(value, 'TIME_FORMAT', use_l10n=use_l10n)
|
||||
return value
|
||||
|
||||
|
||||
@@ -238,52 +221,19 @@ def localize_input(value, default=None):
|
||||
elif isinstance(value, (decimal.Decimal, float, int)):
|
||||
return number_format(value)
|
||||
elif isinstance(value, datetime.datetime):
|
||||
format = default or get_format("DATETIME_INPUT_FORMATS")[0]
|
||||
format = sanitize_strftime_format(format)
|
||||
value = datetime_safe.new_datetime(value)
|
||||
format = default or get_format('DATETIME_INPUT_FORMATS')[0]
|
||||
return value.strftime(format)
|
||||
elif isinstance(value, datetime.date):
|
||||
format = default or get_format("DATE_INPUT_FORMATS")[0]
|
||||
format = sanitize_strftime_format(format)
|
||||
value = datetime_safe.new_date(value)
|
||||
format = default or get_format('DATE_INPUT_FORMATS')[0]
|
||||
return value.strftime(format)
|
||||
elif isinstance(value, datetime.time):
|
||||
format = default or get_format("TIME_INPUT_FORMATS")[0]
|
||||
format = default or get_format('TIME_INPUT_FORMATS')[0]
|
||||
return value.strftime(format)
|
||||
return value
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def sanitize_strftime_format(fmt):
|
||||
"""
|
||||
Ensure that certain specifiers are correctly padded with leading zeros.
|
||||
|
||||
For years < 1000 specifiers %C, %F, %G, and %Y don't work as expected for
|
||||
strftime provided by glibc on Linux as they don't pad the year or century
|
||||
with leading zeros. Support for specifying the padding explicitly is
|
||||
available, however, which can be used to fix this issue.
|
||||
|
||||
FreeBSD, macOS, and Windows do not support explicitly specifying the
|
||||
padding, but return four digit years (with leading zeros) as expected.
|
||||
|
||||
This function checks whether the %Y produces a correctly padded string and,
|
||||
if not, makes the following substitutions:
|
||||
|
||||
- %C → %02C
|
||||
- %F → %010F
|
||||
- %G → %04G
|
||||
- %Y → %04Y
|
||||
|
||||
See https://bugs.python.org/issue13305 for more details.
|
||||
"""
|
||||
if datetime.date(1, 1, 1).strftime("%Y") == "0001":
|
||||
return fmt
|
||||
mapping = {"C": 2, "F": 10, "G": 4, "Y": 4}
|
||||
return re.sub(
|
||||
r"((?:^|[^%])(?:%%)*)%([CFGY])",
|
||||
lambda m: r"%s%%0%s%s" % (m[1], mapping[m[2]], m[2]),
|
||||
fmt,
|
||||
)
|
||||
|
||||
|
||||
def sanitize_separators(value):
|
||||
"""
|
||||
Sanitize a value according to the current decimal and
|
||||
@@ -291,26 +241,19 @@ def sanitize_separators(value):
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
parts = []
|
||||
decimal_separator = get_format("DECIMAL_SEPARATOR")
|
||||
decimal_separator = get_format('DECIMAL_SEPARATOR')
|
||||
if decimal_separator in value:
|
||||
value, decimals = value.split(decimal_separator, 1)
|
||||
parts.append(decimals)
|
||||
if settings.USE_THOUSAND_SEPARATOR:
|
||||
thousand_sep = get_format("THOUSAND_SEPARATOR")
|
||||
if (
|
||||
thousand_sep == "."
|
||||
and value.count(".") == 1
|
||||
and len(value.split(".")[-1]) != 3
|
||||
):
|
||||
# Special case where we suspect a dot meant decimal separator
|
||||
# (see #22171).
|
||||
thousand_sep = get_format('THOUSAND_SEPARATOR')
|
||||
if thousand_sep == '.' and value.count('.') == 1 and len(value.split('.')[-1]) != 3:
|
||||
# Special case where we suspect a dot meant decimal separator (see #22171)
|
||||
pass
|
||||
else:
|
||||
for replacement in {
|
||||
thousand_sep,
|
||||
unicodedata.normalize("NFKD", thousand_sep),
|
||||
}:
|
||||
value = value.replace(replacement, "")
|
||||
thousand_sep, unicodedata.normalize('NFKD', thousand_sep)}:
|
||||
value = value.replace(replacement, '')
|
||||
parts.append(value)
|
||||
value = ".".join(reversed(parts))
|
||||
value = '.'.join(reversed(parts))
|
||||
return value
|
||||
|
||||
@@ -14,19 +14,18 @@ class cached_property:
|
||||
The optional ``name`` argument is obsolete as of Python 3.6 and will be
|
||||
deprecated in Django 4.0 (#30127).
|
||||
"""
|
||||
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def func(instance):
|
||||
raise TypeError(
|
||||
"Cannot use cached_property instance without calling "
|
||||
"__set_name__() on it."
|
||||
'Cannot use cached_property instance without calling '
|
||||
'__set_name__() on it.'
|
||||
)
|
||||
|
||||
def __init__(self, func, name=None):
|
||||
self.real_func = func
|
||||
self.__doc__ = getattr(func, "__doc__")
|
||||
self.__doc__ = getattr(func, '__doc__')
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if self.name is None:
|
||||
@@ -55,7 +54,6 @@ class classproperty:
|
||||
Decorator that converts a method with a single cls argument into a property
|
||||
that can be accessed directly from the class.
|
||||
"""
|
||||
|
||||
def __init__(self, method=None):
|
||||
self.fget = method
|
||||
|
||||
@@ -72,7 +70,6 @@ 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
|
||||
|
||||
|
||||
@@ -91,7 +88,6 @@ def lazy(func, *resultclasses):
|
||||
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):
|
||||
@@ -104,7 +100,7 @@ def lazy(func, *resultclasses):
|
||||
def __reduce__(self):
|
||||
return (
|
||||
_lazy_proxy_unpickle,
|
||||
(func, self.__args, self.__kw) + resultclasses,
|
||||
(func, self.__args, self.__kw) + resultclasses
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -123,10 +119,8 @@ def lazy(func, *resultclasses):
|
||||
setattr(cls, method_name, meth)
|
||||
cls._delegate_bytes = bytes in resultclasses
|
||||
cls._delegate_text = str in resultclasses
|
||||
if cls._delegate_bytes and cls._delegate_text:
|
||||
raise ValueError(
|
||||
"Cannot call lazy() with both bytes and text return types."
|
||||
)
|
||||
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:
|
||||
@@ -140,7 +134,6 @@ def lazy(func, *resultclasses):
|
||||
# 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):
|
||||
@@ -230,15 +223,10 @@ def keep_lazy(*resultclasses):
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if any(
|
||||
isinstance(arg, Promise)
|
||||
for arg in itertools.chain(args, kwargs.values())
|
||||
):
|
||||
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
|
||||
|
||||
|
||||
@@ -257,7 +245,6 @@ def new_method_proxy(func):
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
return func(self._wrapped, *args)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@@ -300,9 +287,7 @@ class LazyObject:
|
||||
"""
|
||||
Must be implemented by subclasses to initialize the wrapped object.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of LazyObject must provide a _setup() method"
|
||||
)
|
||||
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
|
||||
@@ -381,7 +366,6 @@ class SimpleLazyObject(LazyObject):
|
||||
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.
|
||||
@@ -391,7 +375,7 @@ class SimpleLazyObject(LazyObject):
|
||||
callable can be safely run more than once and will return the same
|
||||
value.
|
||||
"""
|
||||
self.__dict__["_setupfunc"] = func
|
||||
self.__dict__['_setupfunc'] = func
|
||||
super().__init__()
|
||||
|
||||
def _setup(self):
|
||||
@@ -404,7 +388,7 @@ class SimpleLazyObject(LazyObject):
|
||||
repr_attr = self._setupfunc
|
||||
else:
|
||||
repr_attr = self._wrapped
|
||||
return "<%s: %r>" % (type(self).__name__, repr_attr)
|
||||
return '<%s: %r>' % (type(self).__name__, repr_attr)
|
||||
|
||||
def __copy__(self):
|
||||
if self._wrapped is empty:
|
||||
|
||||
@@ -8,12 +8,10 @@ def make_hashable(value):
|
||||
The returned value should generate the same hash for equal values.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return tuple(
|
||||
[
|
||||
(key, make_hashable(nested_value))
|
||||
for key, nested_value in sorted(value.items())
|
||||
]
|
||||
)
|
||||
return tuple([
|
||||
(key, make_hashable(nested_value))
|
||||
for key, nested_value in sorted(value.items())
|
||||
])
|
||||
# Try hash to avoid converting a hashable iterable (e.g. string, frozenset)
|
||||
# to a tuple.
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,9 @@ import html
|
||||
import json
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
|
||||
from urllib.parse import (
|
||||
parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit,
|
||||
)
|
||||
|
||||
from django.utils.encoding import punycode
|
||||
from django.utils.functional import Promise, keep_lazy, keep_lazy_text
|
||||
@@ -14,16 +16,17 @@ from django.utils.safestring import SafeData, SafeString, mark_safe
|
||||
from django.utils.text import normalize_newlines
|
||||
|
||||
# Configuration for urlize() function.
|
||||
TRAILING_PUNCTUATION_CHARS = ".,:;!"
|
||||
WRAPPING_PUNCTUATION = [("(", ")"), ("[", "]")]
|
||||
TRAILING_PUNCTUATION_CHARS = '.,:;!'
|
||||
WRAPPING_PUNCTUATION = [('(', ')'), ('[', ']')]
|
||||
|
||||
# List of possible strings used for bullets in bulleted lists.
|
||||
DOTS = ["·", "*", "\u2022", "•", "•", "•"]
|
||||
DOTS = ['·', '*', '\u2022', '•', '•', '•']
|
||||
|
||||
word_split_re = _lazy_re_compile(r"""([\s<>"']+)""")
|
||||
simple_url_re = _lazy_re_compile(r"^https?://\[?\w", re.IGNORECASE)
|
||||
word_split_re = _lazy_re_compile(r'''([\s<>"']+)''')
|
||||
simple_url_re = _lazy_re_compile(r'^https?://\[?\w', re.IGNORECASE)
|
||||
simple_url_2_re = _lazy_re_compile(
|
||||
r"^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)($|/.*)$", re.IGNORECASE
|
||||
r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)($|/.*)$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
@@ -41,22 +44,22 @@ def escape(text):
|
||||
|
||||
|
||||
_js_escapes = {
|
||||
ord("\\"): "\\u005C",
|
||||
ord("'"): "\\u0027",
|
||||
ord('"'): "\\u0022",
|
||||
ord(">"): "\\u003E",
|
||||
ord("<"): "\\u003C",
|
||||
ord("&"): "\\u0026",
|
||||
ord("="): "\\u003D",
|
||||
ord("-"): "\\u002D",
|
||||
ord(";"): "\\u003B",
|
||||
ord("`"): "\\u0060",
|
||||
ord("\u2028"): "\\u2028",
|
||||
ord("\u2029"): "\\u2029",
|
||||
ord('\\'): '\\u005C',
|
||||
ord('\''): '\\u0027',
|
||||
ord('"'): '\\u0022',
|
||||
ord('>'): '\\u003E',
|
||||
ord('<'): '\\u003C',
|
||||
ord('&'): '\\u0026',
|
||||
ord('='): '\\u003D',
|
||||
ord('-'): '\\u002D',
|
||||
ord(';'): '\\u003B',
|
||||
ord('`'): '\\u0060',
|
||||
ord('\u2028'): '\\u2028',
|
||||
ord('\u2029'): '\\u2029'
|
||||
}
|
||||
|
||||
# Escape every ASCII character with a value less than 32.
|
||||
_js_escapes.update((ord("%c" % z), "\\u%04X" % z) for z in range(32))
|
||||
_js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32))
|
||||
|
||||
|
||||
@keep_lazy(str, SafeString)
|
||||
@@ -66,9 +69,9 @@ def escapejs(value):
|
||||
|
||||
|
||||
_json_script_escapes = {
|
||||
ord(">"): "\\u003E",
|
||||
ord("<"): "\\u003C",
|
||||
ord("&"): "\\u0026",
|
||||
ord('>'): '\\u003E',
|
||||
ord('<'): '\\u003C',
|
||||
ord('&'): '\\u0026',
|
||||
}
|
||||
|
||||
|
||||
@@ -79,12 +82,10 @@ def json_script(value, element_id):
|
||||
the escaped JSON in a script tag.
|
||||
"""
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
json_str = json.dumps(value, cls=DjangoJSONEncoder).translate(_json_script_escapes)
|
||||
return format_html(
|
||||
'<script id="{}" type="application/json">{}</script>',
|
||||
element_id,
|
||||
mark_safe(json_str),
|
||||
element_id, mark_safe(json_str)
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +98,7 @@ def conditional_escape(text):
|
||||
"""
|
||||
if isinstance(text, Promise):
|
||||
text = str(text)
|
||||
if hasattr(text, "__html__"):
|
||||
if hasattr(text, '__html__'):
|
||||
return text.__html__()
|
||||
else:
|
||||
return escape(text)
|
||||
@@ -128,23 +129,22 @@ def format_html_join(sep, format_string, args_generator):
|
||||
format_html_join('\n', "<li>{} {}</li>", ((u.first_name, u.last_name)
|
||||
for u in users))
|
||||
"""
|
||||
return mark_safe(
|
||||
conditional_escape(sep).join(
|
||||
format_html(format_string, *args) for args in args_generator
|
||||
)
|
||||
)
|
||||
return mark_safe(conditional_escape(sep).join(
|
||||
format_html(format_string, *args)
|
||||
for args in args_generator
|
||||
))
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def linebreaks(value, autoescape=False):
|
||||
"""Convert newlines into <p> and <br>s."""
|
||||
value = normalize_newlines(value)
|
||||
paras = re.split("\n{2,}", str(value))
|
||||
paras = re.split('\n{2,}', str(value))
|
||||
if autoescape:
|
||||
paras = ["<p>%s</p>" % escape(p).replace("\n", "<br>") for p in paras]
|
||||
paras = ['<p>%s</p>' % escape(p).replace('\n', '<br>') for p in paras]
|
||||
else:
|
||||
paras = ["<p>%s</p>" % p.replace("\n", "<br>") for p in paras]
|
||||
return "\n\n".join(paras)
|
||||
paras = ['<p>%s</p>' % p.replace('\n', '<br>') for p in paras]
|
||||
return '\n\n'.join(paras)
|
||||
|
||||
|
||||
class MLStripper(HTMLParser):
|
||||
@@ -157,13 +157,13 @@ class MLStripper(HTMLParser):
|
||||
self.fed.append(d)
|
||||
|
||||
def handle_entityref(self, name):
|
||||
self.fed.append("&%s;" % name)
|
||||
self.fed.append('&%s;' % name)
|
||||
|
||||
def handle_charref(self, name):
|
||||
self.fed.append("&#%s;" % name)
|
||||
self.fed.append('&#%s;' % name)
|
||||
|
||||
def get_data(self):
|
||||
return "".join(self.fed)
|
||||
return ''.join(self.fed)
|
||||
|
||||
|
||||
def _strip_once(value):
|
||||
@@ -182,9 +182,9 @@ def strip_tags(value):
|
||||
# Note: in typical case this loop executes _strip_once once. Loop condition
|
||||
# is redundant, but helps to reduce number of executions of _strip_once.
|
||||
value = str(value)
|
||||
while "<" in value and ">" in value:
|
||||
while '<' in value and '>' in value:
|
||||
new_value = _strip_once(value)
|
||||
if value.count("<") == new_value.count("<"):
|
||||
if value.count('<') == new_value.count('<'):
|
||||
# _strip_once wasn't able to detect more tags.
|
||||
break
|
||||
value = new_value
|
||||
@@ -194,18 +194,17 @@ def strip_tags(value):
|
||||
@keep_lazy_text
|
||||
def strip_spaces_between_tags(value):
|
||||
"""Return the given HTML with spaces between tags removed."""
|
||||
return re.sub(r">\s+<", "><", str(value))
|
||||
return re.sub(r'>\s+<', '><', str(value))
|
||||
|
||||
|
||||
def smart_urlquote(url):
|
||||
"""Quote a URL if it isn't already quoted."""
|
||||
|
||||
def unquote_quote(segment):
|
||||
segment = unquote(segment)
|
||||
# Tilde is part of RFC3986 Unreserved Characters
|
||||
# https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
# See also https://bugs.python.org/issue16285
|
||||
return quote(segment, safe=RFC3986_SUBDELIMS + RFC3986_GENDELIMS + "~")
|
||||
return quote(segment, safe=RFC3986_SUBDELIMS + RFC3986_GENDELIMS + '~')
|
||||
|
||||
# Handle IDN before quoting.
|
||||
try:
|
||||
@@ -222,10 +221,8 @@ def smart_urlquote(url):
|
||||
if query:
|
||||
# Separately unquoting key/value, so as to not mix querystring separators
|
||||
# included in query values. See #22267.
|
||||
query_parts = [
|
||||
(unquote(q[0]), unquote(q[1]))
|
||||
for q in parse_qsl(query, keep_blank_values=True)
|
||||
]
|
||||
query_parts = [(unquote(q[0]), unquote(q[1]))
|
||||
for q in parse_qsl(query, keep_blank_values=True)]
|
||||
# urlencode will take care of quoting
|
||||
query = urlencode(query_parts)
|
||||
|
||||
@@ -257,7 +254,7 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
def trim_url(x, limit=trim_url_limit):
|
||||
if limit is None or len(x) <= limit:
|
||||
return x
|
||||
return "%s…" % x[: max(0, limit - 1)]
|
||||
return '%s…' % x[:max(0, limit - 1)]
|
||||
|
||||
def trim_punctuation(lead, middle, trail):
|
||||
"""
|
||||
@@ -271,15 +268,13 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
# Trim wrapping punctuation.
|
||||
for opening, closing in WRAPPING_PUNCTUATION:
|
||||
if middle.startswith(opening):
|
||||
middle = middle[len(opening) :]
|
||||
middle = middle[len(opening):]
|
||||
lead += opening
|
||||
trimmed_something = True
|
||||
# Keep parentheses at the end only if they're balanced.
|
||||
if (
|
||||
middle.endswith(closing)
|
||||
and middle.count(closing) == middle.count(opening) + 1
|
||||
):
|
||||
middle = middle[: -len(closing)]
|
||||
if (middle.endswith(closing) and
|
||||
middle.count(closing) == middle.count(opening) + 1):
|
||||
middle = middle[:-len(closing)]
|
||||
trail = closing + trail
|
||||
trimmed_something = True
|
||||
# Trim trailing punctuation (after trimming wrapping punctuation,
|
||||
@@ -288,52 +283,51 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
middle_unescaped = html.unescape(middle)
|
||||
stripped = middle_unescaped.rstrip(TRAILING_PUNCTUATION_CHARS)
|
||||
if middle_unescaped != stripped:
|
||||
punctuation_count = len(middle_unescaped) - len(stripped)
|
||||
trail = middle[-punctuation_count:] + trail
|
||||
middle = middle[:-punctuation_count]
|
||||
trail = middle[len(stripped):] + trail
|
||||
middle = middle[:len(stripped) - len(middle_unescaped)]
|
||||
trimmed_something = True
|
||||
return lead, middle, trail
|
||||
|
||||
def is_email_simple(value):
|
||||
"""Return True if value looks like an email address."""
|
||||
# An @ must be in the middle of the value.
|
||||
if "@" not in value or value.startswith("@") or value.endswith("@"):
|
||||
if '@' not in value or value.startswith('@') or value.endswith('@'):
|
||||
return False
|
||||
try:
|
||||
p1, p2 = value.split("@")
|
||||
p1, p2 = value.split('@')
|
||||
except ValueError:
|
||||
# value contains more than one @.
|
||||
return False
|
||||
# Dot must be in p2 (e.g. example.com)
|
||||
if "." not in p2 or p2.startswith("."):
|
||||
if '.' not in p2 or p2.startswith('.'):
|
||||
return False
|
||||
return True
|
||||
|
||||
words = word_split_re.split(str(text))
|
||||
for i, word in enumerate(words):
|
||||
if "." in word or "@" in word or ":" in word:
|
||||
if '.' in word or '@' in word or ':' in word:
|
||||
# lead: Current punctuation trimmed from the beginning of the word.
|
||||
# middle: Current state of the word.
|
||||
# trail: Current punctuation trimmed from the end of the word.
|
||||
lead, middle, trail = "", word, ""
|
||||
lead, middle, trail = '', word, ''
|
||||
# Deal with punctuation.
|
||||
lead, middle, trail = trim_punctuation(lead, middle, trail)
|
||||
|
||||
# Make URL we want to point to.
|
||||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ""
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ''
|
||||
if simple_url_re.match(middle):
|
||||
url = smart_urlquote(html.unescape(middle))
|
||||
elif simple_url_2_re.match(middle):
|
||||
url = smart_urlquote("http://%s" % html.unescape(middle))
|
||||
elif ":" not in middle and is_email_simple(middle):
|
||||
local, domain = middle.rsplit("@", 1)
|
||||
url = smart_urlquote('http://%s' % html.unescape(middle))
|
||||
elif ':' not in middle and is_email_simple(middle):
|
||||
local, domain = middle.rsplit('@', 1)
|
||||
try:
|
||||
domain = punycode(domain)
|
||||
except UnicodeError:
|
||||
continue
|
||||
url = "mailto:%s@%s" % (local, domain)
|
||||
nofollow_attr = ""
|
||||
url = 'mailto:%s@%s' % (local, domain)
|
||||
nofollow_attr = ''
|
||||
|
||||
# Make link.
|
||||
if url:
|
||||
@@ -342,7 +336,7 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
lead, trail = escape(lead), escape(trail)
|
||||
trimmed = escape(trimmed)
|
||||
middle = '<a href="%s"%s>%s</a>' % (escape(url), nofollow_attr, trimmed)
|
||||
words[i] = mark_safe("%s%s%s" % (lead, middle, trail))
|
||||
words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
|
||||
else:
|
||||
if safe_input:
|
||||
words[i] = mark_safe(word)
|
||||
@@ -352,7 +346,7 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
words[i] = mark_safe(word)
|
||||
elif autoescape:
|
||||
words[i] = escape(word)
|
||||
return "".join(words)
|
||||
return ''.join(words)
|
||||
|
||||
|
||||
def avoid_wrapping(value):
|
||||
@@ -368,12 +362,12 @@ def html_safe(klass):
|
||||
A decorator that defines the __html__ method. This helps non-Django
|
||||
templates to detect classes whose __str__ methods return SafeString.
|
||||
"""
|
||||
if "__html__" in klass.__dict__:
|
||||
if '__html__' in klass.__dict__:
|
||||
raise ValueError(
|
||||
"can't apply @html_safe to %s because it defines "
|
||||
"__html__()." % klass.__name__
|
||||
)
|
||||
if "__str__" not in klass.__dict__:
|
||||
if '__str__' not in klass.__dict__:
|
||||
raise ValueError(
|
||||
"can't apply @html_safe to %s because it doesn't "
|
||||
"define __str__()." % klass.__name__
|
||||
|
||||
@@ -1,51 +1,103 @@
|
||||
import base64
|
||||
import calendar
|
||||
import datetime
|
||||
import re
|
||||
import unicodedata
|
||||
import warnings
|
||||
from binascii import Error as BinasciiError
|
||||
from email.utils import formatdate
|
||||
from urllib.parse import (
|
||||
ParseResult,
|
||||
SplitResult,
|
||||
_coerce_args,
|
||||
_splitnetloc,
|
||||
_splitparams,
|
||||
scheme_chars,
|
||||
ParseResult, SplitResult, _coerce_args, _splitnetloc, _splitparams, quote,
|
||||
quote_plus, scheme_chars, unquote, unquote_plus,
|
||||
urlencode as original_urlencode, uses_params,
|
||||
)
|
||||
from urllib.parse import urlencode as original_urlencode
|
||||
from urllib.parse import uses_params
|
||||
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.deprecation import RemovedInDjango40Warning
|
||||
from django.utils.functional import keep_lazy_text
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
# based on RFC 7232, Appendix C
|
||||
ETAG_MATCH = _lazy_re_compile(
|
||||
r"""
|
||||
ETAG_MATCH = _lazy_re_compile(r'''
|
||||
\A( # start of string and capture group
|
||||
(?:W/)? # optional weak indicator
|
||||
" # opening quote
|
||||
[^"]* # any sequence of non-quote characters
|
||||
" # end quote
|
||||
)\Z # end of string and capture group
|
||||
""",
|
||||
re.X,
|
||||
)
|
||||
''', re.X)
|
||||
|
||||
MONTHS = "jan feb mar apr may jun jul aug sep oct nov dec".split()
|
||||
__D = r"(?P<day>\d{2})"
|
||||
__D2 = r"(?P<day>[ \d]\d)"
|
||||
__M = r"(?P<mon>\w{3})"
|
||||
__Y = r"(?P<year>\d{4})"
|
||||
__Y2 = r"(?P<year>\d{2})"
|
||||
__T = r"(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})"
|
||||
RFC1123_DATE = _lazy_re_compile(r"^\w{3}, %s %s %s %s GMT$" % (__D, __M, __Y, __T))
|
||||
RFC850_DATE = _lazy_re_compile(r"^\w{6,9}, %s-%s-%s %s GMT$" % (__D, __M, __Y2, __T))
|
||||
ASCTIME_DATE = _lazy_re_compile(r"^\w{3} %s %s %s %s$" % (__M, __D2, __T, __Y))
|
||||
MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()
|
||||
__D = r'(?P<day>\d{2})'
|
||||
__D2 = r'(?P<day>[ \d]\d)'
|
||||
__M = r'(?P<mon>\w{3})'
|
||||
__Y = r'(?P<year>\d{4})'
|
||||
__Y2 = r'(?P<year>\d{2})'
|
||||
__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
|
||||
RFC1123_DATE = _lazy_re_compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
|
||||
RFC850_DATE = _lazy_re_compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
|
||||
ASCTIME_DATE = _lazy_re_compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
|
||||
|
||||
RFC3986_GENDELIMS = ":/?#[]@"
|
||||
RFC3986_SUBDELIMS = "!$&'()*+,;="
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def urlquote(url, safe='/'):
|
||||
"""
|
||||
A legacy compatibility wrapper to Python's urllib.parse.quote() function.
|
||||
(was used for unicode handling on Python 2)
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.http.urlquote() is deprecated in favor of '
|
||||
'urllib.parse.quote().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return quote(url, safe)
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def urlquote_plus(url, safe=''):
|
||||
"""
|
||||
A legacy compatibility wrapper to Python's urllib.parse.quote_plus()
|
||||
function. (was used for unicode handling on Python 2)
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.http.urlquote_plus() is deprecated in favor of '
|
||||
'urllib.parse.quote_plus(),',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return quote_plus(url, safe)
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def urlunquote(quoted_url):
|
||||
"""
|
||||
A legacy compatibility wrapper to Python's urllib.parse.unquote() function.
|
||||
(was used for unicode handling on Python 2)
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.http.urlunquote() is deprecated in favor of '
|
||||
'urllib.parse.unquote().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return unquote(quoted_url)
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def urlunquote_plus(quoted_url):
|
||||
"""
|
||||
A legacy compatibility wrapper to Python's urllib.parse.unquote_plus()
|
||||
function. (was used for unicode handling on Python 2)
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.http.urlunquote_plus() is deprecated in favor of '
|
||||
'urllib.parse.unquote_plus().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return unquote_plus(quoted_url)
|
||||
|
||||
|
||||
def urlencode(query, doseq=False):
|
||||
"""
|
||||
A version of Python's urllib.parse.urlencode() function that can operate on
|
||||
@@ -53,7 +105,7 @@ def urlencode(query, doseq=False):
|
||||
"""
|
||||
if isinstance(query, MultiValueDict):
|
||||
query = query.lists()
|
||||
elif hasattr(query, "items"):
|
||||
elif hasattr(query, 'items'):
|
||||
query = query.items()
|
||||
query_params = []
|
||||
for key, value in query:
|
||||
@@ -120,10 +172,9 @@ def parse_http_date(date):
|
||||
else:
|
||||
raise ValueError("%r is not in a valid HTTP date format" % date)
|
||||
try:
|
||||
tz = datetime.timezone.utc
|
||||
year = int(m["year"])
|
||||
year = int(m['year'])
|
||||
if year < 100:
|
||||
current_year = datetime.datetime.now(tz=tz).year
|
||||
current_year = datetime.datetime.utcnow().year
|
||||
current_century = current_year - (current_year % 100)
|
||||
if year - (current_year % 100) > 50:
|
||||
# year that appears to be more than 50 years in the future are
|
||||
@@ -131,13 +182,13 @@ def parse_http_date(date):
|
||||
year += current_century - 100
|
||||
else:
|
||||
year += current_century
|
||||
month = MONTHS.index(m["mon"].lower()) + 1
|
||||
day = int(m["day"])
|
||||
hour = int(m["hour"])
|
||||
min = int(m["min"])
|
||||
sec = int(m["sec"])
|
||||
result = datetime.datetime(year, month, day, hour, min, sec, tzinfo=tz)
|
||||
return int(result.timestamp())
|
||||
month = MONTHS.index(m['mon'].lower()) + 1
|
||||
day = int(m['day'])
|
||||
hour = int(m['hour'])
|
||||
min = int(m['min'])
|
||||
sec = int(m['sec'])
|
||||
result = datetime.datetime(year, month, day, hour, min, sec)
|
||||
return calendar.timegm(result.utctimetuple())
|
||||
except Exception as exc:
|
||||
raise ValueError("%r is not a valid date" % date) from exc
|
||||
|
||||
@@ -154,7 +205,6 @@ def parse_http_date_safe(date):
|
||||
|
||||
# Base 36 functions: useful for generating compact URLs
|
||||
|
||||
|
||||
def base36_to_int(s):
|
||||
"""
|
||||
Convert a base 36 string to an int. Raise ValueError if the input won't fit
|
||||
@@ -170,12 +220,12 @@ def base36_to_int(s):
|
||||
|
||||
def int_to_base36(i):
|
||||
"""Convert an integer to a base36 string."""
|
||||
char_set = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
char_set = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
if i < 0:
|
||||
raise ValueError("Negative base36 conversion input.")
|
||||
if i < 36:
|
||||
return char_set[i]
|
||||
b36 = ""
|
||||
b36 = ''
|
||||
while i != 0:
|
||||
i, n = divmod(i, 36)
|
||||
b36 = char_set[n] + b36
|
||||
@@ -187,7 +237,7 @@ def urlsafe_base64_encode(s):
|
||||
Encode a bytestring to a base64 string for use in URLs. Strip any trailing
|
||||
equal signs.
|
||||
"""
|
||||
return base64.urlsafe_b64encode(s).rstrip(b"\n=").decode("ascii")
|
||||
return base64.urlsafe_b64encode(s).rstrip(b'\n=').decode('ascii')
|
||||
|
||||
|
||||
def urlsafe_base64_decode(s):
|
||||
@@ -197,7 +247,7 @@ def urlsafe_base64_decode(s):
|
||||
"""
|
||||
s = s.encode()
|
||||
try:
|
||||
return base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, b"="))
|
||||
return base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, b'='))
|
||||
except (LookupError, BinasciiError) as e:
|
||||
raise ValueError(e)
|
||||
|
||||
@@ -208,11 +258,11 @@ def parse_etags(etag_str):
|
||||
defined by RFC 7232. Return a list of quoted ETags, or ['*'] if all ETags
|
||||
should be matched.
|
||||
"""
|
||||
if etag_str.strip() == "*":
|
||||
return ["*"]
|
||||
if etag_str.strip() == '*':
|
||||
return ['*']
|
||||
else:
|
||||
# Parse each ETag individually, and return any that are valid.
|
||||
etag_matches = (ETAG_MATCH.match(etag.strip()) for etag in etag_str.split(","))
|
||||
etag_matches = (ETAG_MATCH.match(etag.strip()) for etag in etag_str.split(','))
|
||||
return [match[1] for match in etag_matches if match]
|
||||
|
||||
|
||||
@@ -241,9 +291,8 @@ def is_same_domain(host, pattern):
|
||||
|
||||
pattern = pattern.lower()
|
||||
return (
|
||||
pattern[0] == "."
|
||||
and (host.endswith(pattern) or host == pattern[1:])
|
||||
or pattern == host
|
||||
pattern[0] == '.' and (host.endswith(pattern) or host == pattern[1:]) or
|
||||
pattern == host
|
||||
)
|
||||
|
||||
|
||||
@@ -270,15 +319,23 @@ def url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
|
||||
allowed_hosts = {allowed_hosts}
|
||||
# Chrome treats \ completely as / in paths but it could be part of some
|
||||
# basic auth credentials so we need to check both URLs.
|
||||
return _url_has_allowed_host_and_scheme(
|
||||
url, allowed_hosts, require_https=require_https
|
||||
) and _url_has_allowed_host_and_scheme(
|
||||
url.replace("\\", "/"), allowed_hosts, require_https=require_https
|
||||
return (
|
||||
_url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=require_https) and
|
||||
_url_has_allowed_host_and_scheme(url.replace('\\', '/'), allowed_hosts, require_https=require_https)
|
||||
)
|
||||
|
||||
|
||||
def is_safe_url(url, allowed_hosts, require_https=False):
|
||||
warnings.warn(
|
||||
'django.utils.http.is_safe_url() is deprecated in favor of '
|
||||
'url_has_allowed_host_and_scheme().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return url_has_allowed_host_and_scheme(url, allowed_hosts, require_https)
|
||||
|
||||
|
||||
# Copied from urllib.parse.urlparse() but uses fixed urlsplit() function.
|
||||
def _urlparse(url, scheme="", allow_fragments=True):
|
||||
def _urlparse(url, scheme='', allow_fragments=True):
|
||||
"""Parse a URL into 6 components:
|
||||
<scheme>://<netloc>/<path>;<params>?<query>#<fragment>
|
||||
Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
|
||||
@@ -287,42 +344,41 @@ def _urlparse(url, scheme="", allow_fragments=True):
|
||||
url, scheme, _coerce_result = _coerce_args(url, scheme)
|
||||
splitresult = _urlsplit(url, scheme, allow_fragments)
|
||||
scheme, netloc, url, query, fragment = splitresult
|
||||
if scheme in uses_params and ";" in url:
|
||||
if scheme in uses_params and ';' in url:
|
||||
url, params = _splitparams(url)
|
||||
else:
|
||||
params = ""
|
||||
params = ''
|
||||
result = ParseResult(scheme, netloc, url, params, query, fragment)
|
||||
return _coerce_result(result)
|
||||
|
||||
|
||||
# Copied from urllib.parse.urlsplit() with
|
||||
# https://github.com/python/cpython/pull/661 applied.
|
||||
def _urlsplit(url, scheme="", allow_fragments=True):
|
||||
def _urlsplit(url, scheme='', allow_fragments=True):
|
||||
"""Parse a URL into 5 components:
|
||||
<scheme>://<netloc>/<path>?<query>#<fragment>
|
||||
Return a 5-tuple: (scheme, netloc, path, query, fragment).
|
||||
Note that we don't break the components up in smaller bits
|
||||
(e.g. netloc is a single string) and we don't expand % escapes."""
|
||||
url, scheme, _coerce_result = _coerce_args(url, scheme)
|
||||
netloc = query = fragment = ""
|
||||
i = url.find(":")
|
||||
netloc = query = fragment = ''
|
||||
i = url.find(':')
|
||||
if i > 0:
|
||||
for c in url[:i]:
|
||||
if c not in scheme_chars:
|
||||
break
|
||||
else:
|
||||
scheme, url = url[:i].lower(), url[i + 1 :]
|
||||
scheme, url = url[:i].lower(), url[i + 1:]
|
||||
|
||||
if url[:2] == "//":
|
||||
if url[:2] == '//':
|
||||
netloc, url = _splitnetloc(url, 2)
|
||||
if ("[" in netloc and "]" not in netloc) or (
|
||||
"]" in netloc and "[" not in netloc
|
||||
):
|
||||
if (('[' in netloc and ']' not in netloc) or
|
||||
(']' in netloc and '[' not in netloc)):
|
||||
raise ValueError("Invalid IPv6 URL")
|
||||
if allow_fragments and "#" in url:
|
||||
url, fragment = url.split("#", 1)
|
||||
if "?" in url:
|
||||
url, query = url.split("?", 1)
|
||||
if allow_fragments and '#' in url:
|
||||
url, fragment = url.split('#', 1)
|
||||
if '?' in url:
|
||||
url, query = url.split('?', 1)
|
||||
v = SplitResult(scheme, netloc, url, query, fragment)
|
||||
return _coerce_result(v)
|
||||
|
||||
@@ -330,7 +386,7 @@ def _urlsplit(url, scheme="", allow_fragments=True):
|
||||
def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
|
||||
# Chrome considers any URL with more than two slashes to be absolute, but
|
||||
# urlparse is not so flexible. Treat any url with three slashes as unsafe.
|
||||
if url.startswith("///"):
|
||||
if url.startswith('///'):
|
||||
return False
|
||||
try:
|
||||
url_info = _urlparse(url)
|
||||
@@ -345,16 +401,93 @@ def _url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
|
||||
# Forbid URLs that start with control characters. Some browsers (like
|
||||
# Chrome) ignore quite a few control characters at the start of a
|
||||
# URL and might consider the URL as scheme relative.
|
||||
if unicodedata.category(url[0])[0] == "C":
|
||||
if unicodedata.category(url[0])[0] == 'C':
|
||||
return False
|
||||
scheme = url_info.scheme
|
||||
# Consider URLs without a scheme (e.g. //example.com/p) to be http.
|
||||
if not url_info.scheme and url_info.netloc:
|
||||
scheme = "http"
|
||||
valid_schemes = ["https"] if require_https else ["http", "https"]
|
||||
return (not url_info.netloc or url_info.netloc in allowed_hosts) and (
|
||||
not scheme or scheme in valid_schemes
|
||||
)
|
||||
scheme = 'http'
|
||||
valid_schemes = ['https'] if require_https else ['http', 'https']
|
||||
return ((not url_info.netloc or url_info.netloc in allowed_hosts) and
|
||||
(not scheme or scheme in valid_schemes))
|
||||
|
||||
|
||||
# TODO: Remove when dropping support for PY37.
|
||||
def parse_qsl(
|
||||
qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8',
|
||||
errors='replace', max_num_fields=None, separator='&',
|
||||
):
|
||||
"""
|
||||
Return a list of key/value tuples parsed from query string.
|
||||
|
||||
Backport of urllib.parse.parse_qsl() from Python 3.8.8.
|
||||
Copyright (C) 2021 Python Software Foundation (see LICENSE.python).
|
||||
|
||||
----
|
||||
|
||||
Parse a query given as a string argument.
|
||||
|
||||
Arguments:
|
||||
|
||||
qs: percent-encoded query string to be parsed
|
||||
|
||||
keep_blank_values: flag indicating whether blank values in
|
||||
percent-encoded queries should be treated as blank strings. A
|
||||
true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
|
||||
strict_parsing: flag indicating what to do with parsing errors. If false
|
||||
(the default), errors are silently ignored. If true, errors raise a
|
||||
ValueError exception.
|
||||
|
||||
encoding and errors: specify how to decode percent-encoded sequences
|
||||
into Unicode characters, as accepted by the bytes.decode() method.
|
||||
|
||||
max_num_fields: int. If set, then throws a ValueError if there are more
|
||||
than n fields read by parse_qsl().
|
||||
|
||||
separator: str. The symbol to use for separating the query arguments.
|
||||
Defaults to &.
|
||||
|
||||
Returns a list, as G-d intended.
|
||||
"""
|
||||
qs, _coerce_result = _coerce_args(qs)
|
||||
|
||||
if not separator or not isinstance(separator, (str, bytes)):
|
||||
raise ValueError('Separator must be of type string or bytes.')
|
||||
|
||||
# If max_num_fields is defined then check that the number of fields is less
|
||||
# than max_num_fields. This prevents a memory exhaustion DOS attack via
|
||||
# post bodies with many fields.
|
||||
if max_num_fields is not None:
|
||||
num_fields = 1 + qs.count(separator)
|
||||
if max_num_fields < num_fields:
|
||||
raise ValueError('Max number of fields exceeded')
|
||||
|
||||
pairs = [s1 for s1 in qs.split(separator)]
|
||||
r = []
|
||||
for name_value in pairs:
|
||||
if not name_value and not strict_parsing:
|
||||
continue
|
||||
nv = name_value.split('=', 1)
|
||||
if len(nv) != 2:
|
||||
if strict_parsing:
|
||||
raise ValueError("bad query field: %r" % (name_value,))
|
||||
# Handle case of a control-name with no equal sign.
|
||||
if keep_blank_values:
|
||||
nv.append('')
|
||||
else:
|
||||
continue
|
||||
if len(nv[1]) or keep_blank_values:
|
||||
name = nv[0].replace('+', ' ')
|
||||
name = unquote(name, encoding=encoding, errors=errors)
|
||||
name = _coerce_result(name)
|
||||
value = nv[1].replace('+', ' ')
|
||||
value = unquote(value, encoding=encoding, errors=errors)
|
||||
value = _coerce_result(value)
|
||||
r.append((name, value))
|
||||
return r
|
||||
|
||||
|
||||
def escape_leading_slashes(url):
|
||||
@@ -363,6 +496,6 @@ def escape_leading_slashes(url):
|
||||
escaped to prevent browsers from handling the path as schemaless and
|
||||
redirecting to another host.
|
||||
"""
|
||||
if url.startswith("//"):
|
||||
url = "/%2F{}".format(url[2:])
|
||||
if url.startswith('//'):
|
||||
url = '/%2F{}'.format(url[2:])
|
||||
return url
|
||||
|
||||
@@ -19,8 +19,7 @@ def _get_callable_parameters(meth_or_func):
|
||||
def get_func_args(func):
|
||||
params = _get_callable_parameters(func)
|
||||
return [
|
||||
param.name
|
||||
for param in params
|
||||
param.name for param in params
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
]
|
||||
|
||||
@@ -36,12 +35,12 @@ def get_func_full_args(func):
|
||||
for param in params:
|
||||
name = param.name
|
||||
# Ignore 'self'
|
||||
if name == "self":
|
||||
if name == 'self':
|
||||
continue
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
name = "*" + name
|
||||
name = '*' + name
|
||||
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
name = "**" + name
|
||||
name = '**' + name
|
||||
if param.default != inspect.Parameter.empty:
|
||||
args.append((name, param.default))
|
||||
else:
|
||||
@@ -51,21 +50,28 @@ def get_func_full_args(func):
|
||||
|
||||
def func_accepts_kwargs(func):
|
||||
"""Return True if function 'func' accepts keyword arguments **kwargs."""
|
||||
return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_KEYWORD)
|
||||
return any(
|
||||
p for p in _get_callable_parameters(func)
|
||||
if p.kind == p.VAR_KEYWORD
|
||||
)
|
||||
|
||||
|
||||
def func_accepts_var_args(func):
|
||||
"""
|
||||
Return True if function 'func' accepts positional arguments *args.
|
||||
"""
|
||||
return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_POSITIONAL)
|
||||
return any(
|
||||
p for p in _get_callable_parameters(func)
|
||||
if p.kind == p.VAR_POSITIONAL
|
||||
)
|
||||
|
||||
|
||||
def method_has_no_args(meth):
|
||||
"""Return True if a method only accepts 'self'."""
|
||||
count = len(
|
||||
[p for p in _get_callable_parameters(meth) if p.kind == p.POSITIONAL_OR_KEYWORD]
|
||||
)
|
||||
count = len([
|
||||
p for p in _get_callable_parameters(meth)
|
||||
if p.kind == p.POSITIONAL_OR_KEYWORD
|
||||
])
|
||||
return count == 0 if inspect.ismethod(meth) else count == 1
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def clean_ipv6_address(
|
||||
ip_str, unpack_ipv4=False, error_message=_("This is not a valid IPv6 address.")
|
||||
):
|
||||
def clean_ipv6_address(ip_str, unpack_ipv4=False,
|
||||
error_message=_("This is not a valid IPv6 address.")):
|
||||
"""
|
||||
Clean an IPv6 address string.
|
||||
|
||||
@@ -26,12 +25,12 @@ def clean_ipv6_address(
|
||||
try:
|
||||
addr = ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str)))
|
||||
except ValueError:
|
||||
raise ValidationError(error_message, code="invalid")
|
||||
raise ValidationError(error_message, code='invalid')
|
||||
|
||||
if unpack_ipv4 and addr.ipv4_mapped:
|
||||
return str(addr.ipv4_mapped)
|
||||
elif addr.ipv4_mapped:
|
||||
return "::ffff:%s" % str(addr.ipv4_mapped)
|
||||
return '::ffff:%s' % str(addr.ipv4_mapped)
|
||||
|
||||
return str(addr)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""JsLex: a lexer for JavaScript"""
|
||||
"""JsLex: a lexer for Javascript"""
|
||||
# Originally from https://bitbucket.org/ned/jslex
|
||||
import re
|
||||
|
||||
@@ -7,7 +7,6 @@ class Tok:
|
||||
"""
|
||||
A specification for a token class.
|
||||
"""
|
||||
|
||||
num = 0
|
||||
|
||||
def __init__(self, name, regex, next=None):
|
||||
@@ -76,23 +75,23 @@ class Lexer:
|
||||
|
||||
class JsLexer(Lexer):
|
||||
"""
|
||||
A JavaScript lexer
|
||||
A Javascript lexer
|
||||
|
||||
>>> lexer = JsLexer()
|
||||
>>> list(lexer.lex("a = 1"))
|
||||
[('id', 'a'), ('ws', ' '), ('punct', '='), ('ws', ' '), ('dnum', '1')]
|
||||
|
||||
This doesn't properly handle non-ASCII characters in the JavaScript source.
|
||||
This doesn't properly handle non-ASCII characters in the Javascript source.
|
||||
"""
|
||||
|
||||
# Because these tokens are matched as alternatives in a regex, longer
|
||||
# possibilities must appear in the list before shorter ones, for example,
|
||||
# '>>' before '>'.
|
||||
#
|
||||
# Note that we don't have to detect malformed JavaScript, only properly
|
||||
# lex correct JavaScript, so much of this is simplified.
|
||||
# Note that we don't have to detect malformed Javascript, only properly
|
||||
# lex correct Javascript, so much of this is simplified.
|
||||
|
||||
# Details of JavaScript lexical structure are taken from
|
||||
# Details of Javascript lexical structure are taken from
|
||||
# http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf
|
||||
|
||||
# A useful explanation of automatic semicolon insertion is at
|
||||
@@ -102,34 +101,21 @@ class JsLexer(Lexer):
|
||||
Tok("comment", r"/\*(.|\n)*?\*/"),
|
||||
Tok("linecomment", r"//.*?$"),
|
||||
Tok("ws", r"\s+"),
|
||||
Tok(
|
||||
"keyword",
|
||||
literals(
|
||||
"""
|
||||
Tok("keyword", literals("""
|
||||
break case catch class const continue debugger
|
||||
default delete do else enum export extends
|
||||
finally for function if import in instanceof
|
||||
new return super switch this throw try typeof
|
||||
var void while with
|
||||
""",
|
||||
suffix=r"\b",
|
||||
),
|
||||
next="reg",
|
||||
),
|
||||
Tok("reserved", literals("null true false", suffix=r"\b"), next="div"),
|
||||
Tok(
|
||||
"id",
|
||||
r"""
|
||||
""", suffix=r"\b"), next='reg'),
|
||||
Tok("reserved", literals("null true false", suffix=r"\b"), next='div'),
|
||||
Tok("id", r"""
|
||||
([a-zA-Z_$ ]|\\u[0-9a-fA-Z]{4}) # first char
|
||||
([a-zA-Z_$0-9]|\\u[0-9a-fA-F]{4})* # rest chars
|
||||
""",
|
||||
next="div",
|
||||
),
|
||||
Tok("hnum", r"0[xX][0-9a-fA-F]+", next="div"),
|
||||
""", next='div'),
|
||||
Tok("hnum", r"0[xX][0-9a-fA-F]+", next='div'),
|
||||
Tok("onum", r"0[0-7]+"),
|
||||
Tok(
|
||||
"dnum",
|
||||
r"""
|
||||
Tok("dnum", r"""
|
||||
( (0|[1-9][0-9]*) # DecimalIntegerLiteral
|
||||
\. # dot
|
||||
[0-9]* # DecimalDigits-opt
|
||||
@@ -142,23 +128,15 @@ class JsLexer(Lexer):
|
||||
(0|[1-9][0-9]*) # DecimalIntegerLiteral
|
||||
([eE][-+]?[0-9]+)? # ExponentPart-opt
|
||||
)
|
||||
""",
|
||||
next="div",
|
||||
),
|
||||
Tok(
|
||||
"punct",
|
||||
literals(
|
||||
"""
|
||||
""", next='div'),
|
||||
Tok("punct", literals("""
|
||||
>>>= === !== >>> <<= >>= <= >= == != << >> &&
|
||||
|| += -= *= %= &= |= ^=
|
||||
"""
|
||||
),
|
||||
next="reg",
|
||||
),
|
||||
Tok("punct", literals("++ -- ) ]"), next="div"),
|
||||
Tok("punct", literals("{ } ( [ . ; , < > + - * % & | ^ ! ~ ? : ="), next="reg"),
|
||||
Tok("string", r'"([^"\\]|(\\(.|\n)))*?"', next="div"),
|
||||
Tok("string", r"'([^'\\]|(\\(.|\n)))*?'", next="div"),
|
||||
"""), next="reg"),
|
||||
Tok("punct", literals("++ -- ) ]"), next='div'),
|
||||
Tok("punct", literals("{ } ( [ . ; , < > + - * % & | ^ ! ~ ? : ="), next='reg'),
|
||||
Tok("string", r'"([^"\\]|(\\(.|\n)))*?"', next='div'),
|
||||
Tok("string", r"'([^'\\]|(\\(.|\n)))*?'", next='div'),
|
||||
]
|
||||
|
||||
both_after = [
|
||||
@@ -167,16 +145,13 @@ class JsLexer(Lexer):
|
||||
|
||||
states = {
|
||||
# slash will mean division
|
||||
"div": both_before
|
||||
+ [
|
||||
Tok("punct", literals("/= /"), next="reg"),
|
||||
]
|
||||
+ both_after,
|
||||
'div': both_before + [
|
||||
Tok("punct", literals("/= /"), next='reg'),
|
||||
] + both_after,
|
||||
|
||||
# slash will mean regex
|
||||
"reg": both_before
|
||||
+ [
|
||||
Tok(
|
||||
"regex",
|
||||
'reg': both_before + [
|
||||
Tok("regex",
|
||||
r"""
|
||||
/ # opening slash
|
||||
# First character is..
|
||||
@@ -199,51 +174,47 @@ class JsLexer(Lexer):
|
||||
)* # many times
|
||||
/ # closing slash
|
||||
[a-zA-Z0-9]* # trailing flags
|
||||
""",
|
||||
next="div",
|
||||
),
|
||||
]
|
||||
+ both_after,
|
||||
""", next='div'),
|
||||
] + both_after,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.states, "reg")
|
||||
super().__init__(self.states, 'reg')
|
||||
|
||||
|
||||
def prepare_js_for_gettext(js):
|
||||
"""
|
||||
Convert the JavaScript source `js` into something resembling C for
|
||||
Convert the Javascript source `js` into something resembling C for
|
||||
xgettext.
|
||||
|
||||
What actually happens is that all the regex literals are replaced with
|
||||
"REGEX".
|
||||
"""
|
||||
|
||||
def escape_quotes(m):
|
||||
"""Used in a regex to properly escape double quotes."""
|
||||
s = m[0]
|
||||
if s == '"':
|
||||
return r"\""
|
||||
return r'\"'
|
||||
else:
|
||||
return s
|
||||
|
||||
lexer = JsLexer()
|
||||
c = []
|
||||
for name, tok in lexer.lex(js):
|
||||
if name == "regex":
|
||||
if name == 'regex':
|
||||
# C doesn't grok regexes, and they aren't needed for gettext,
|
||||
# so just output a string instead.
|
||||
tok = '"REGEX"'
|
||||
elif name == "string":
|
||||
elif name == 'string':
|
||||
# C doesn't have single-quoted strings, so make all strings
|
||||
# double-quoted.
|
||||
if tok.startswith("'"):
|
||||
guts = re.sub(r"\\.|.", escape_quotes, tok[1:-1])
|
||||
tok = '"' + guts + '"'
|
||||
elif name == "id":
|
||||
elif name == 'id':
|
||||
# C can't deal with Unicode escapes in identifiers. We don't
|
||||
# need them for gettext anyway, so replace them with something
|
||||
# innocuous
|
||||
tok = tok.replace("\\", "U")
|
||||
c.append(tok)
|
||||
return "".join(c)
|
||||
return ''.join(c)
|
||||
|
||||
@@ -8,59 +8,58 @@ from django.core.mail import get_connection
|
||||
from django.core.management.color import color_style
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
request_logger = logging.getLogger("django.request")
|
||||
request_logger = logging.getLogger('django.request')
|
||||
|
||||
# Default logging for Django. This sends an email to the site admins on every
|
||||
# HTTP 500 error. Depending on DEBUG, all other log records are either sent to
|
||||
# the console (DEBUG=True) or discarded (DEBUG=False) by means of the
|
||||
# require_debug_true filter. This configuration is quoted in
|
||||
# docs/ref/logging.txt; please amend it there if edited here.
|
||||
# require_debug_true filter.
|
||||
DEFAULT_LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"filters": {
|
||||
"require_debug_false": {
|
||||
"()": "django.utils.log.RequireDebugFalse",
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'filters': {
|
||||
'require_debug_false': {
|
||||
'()': 'django.utils.log.RequireDebugFalse',
|
||||
},
|
||||
"require_debug_true": {
|
||||
"()": "django.utils.log.RequireDebugTrue",
|
||||
'require_debug_true': {
|
||||
'()': 'django.utils.log.RequireDebugTrue',
|
||||
},
|
||||
},
|
||||
"formatters": {
|
||||
"django.server": {
|
||||
"()": "django.utils.log.ServerFormatter",
|
||||
"format": "[{server_time}] {message}",
|
||||
"style": "{",
|
||||
'formatters': {
|
||||
'django.server': {
|
||||
'()': 'django.utils.log.ServerFormatter',
|
||||
'format': '[{server_time}] {message}',
|
||||
'style': '{',
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"filters": ["require_debug_true"],
|
||||
"class": "logging.StreamHandler",
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'INFO',
|
||||
'filters': ['require_debug_true'],
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
"django.server": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "django.server",
|
||||
},
|
||||
"mail_admins": {
|
||||
"level": "ERROR",
|
||||
"filters": ["require_debug_false"],
|
||||
"class": "django.utils.log.AdminEmailHandler",
|
||||
'django.server': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'django.server',
|
||||
},
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false'],
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["console", "mail_admins"],
|
||||
"level": "INFO",
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console', 'mail_admins'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
"django.server": {
|
||||
"handlers": ["django.server"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
'django.server': {
|
||||
'handlers': ['django.server'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,24 +86,22 @@ class AdminEmailHandler(logging.Handler):
|
||||
super().__init__()
|
||||
self.include_html = include_html
|
||||
self.email_backend = email_backend
|
||||
self.reporter_class = import_string(
|
||||
reporter_class or settings.DEFAULT_EXCEPTION_REPORTER
|
||||
)
|
||||
self.reporter_class = import_string(reporter_class or settings.DEFAULT_EXCEPTION_REPORTER)
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
request = record.request
|
||||
subject = "%s (%s IP): %s" % (
|
||||
subject = '%s (%s IP): %s' % (
|
||||
record.levelname,
|
||||
(
|
||||
"internal"
|
||||
if request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS
|
||||
else "EXTERNAL"
|
||||
),
|
||||
record.getMessage(),
|
||||
('internal' if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS
|
||||
else 'EXTERNAL'),
|
||||
record.getMessage()
|
||||
)
|
||||
except Exception:
|
||||
subject = "%s: %s" % (record.levelname, record.getMessage())
|
||||
subject = '%s: %s' % (
|
||||
record.levelname,
|
||||
record.getMessage()
|
||||
)
|
||||
request = None
|
||||
subject = self.format_subject(subject)
|
||||
|
||||
@@ -120,17 +117,12 @@ class AdminEmailHandler(logging.Handler):
|
||||
exc_info = (None, record.getMessage(), None)
|
||||
|
||||
reporter = self.reporter_class(request, is_email=True, *exc_info)
|
||||
message = "%s\n\n%s" % (
|
||||
self.format(no_exc_record),
|
||||
reporter.get_traceback_text(),
|
||||
)
|
||||
message = "%s\n\n%s" % (self.format(no_exc_record), reporter.get_traceback_text())
|
||||
html_message = reporter.get_traceback_html() if self.include_html else None
|
||||
self.send_mail(subject, message, fail_silently=True, html_message=html_message)
|
||||
|
||||
def send_mail(self, subject, message, *args, **kwargs):
|
||||
mail.mail_admins(
|
||||
subject, message, *args, connection=self.connection(), **kwargs
|
||||
)
|
||||
mail.mail_admins(subject, message, *args, connection=self.connection(), **kwargs)
|
||||
|
||||
def connection(self):
|
||||
return get_connection(backend=self.email_backend, fail_silently=True)
|
||||
@@ -139,7 +131,7 @@ class AdminEmailHandler(logging.Handler):
|
||||
"""
|
||||
Escape CR and LF characters.
|
||||
"""
|
||||
return subject.replace("\n", "\\n").replace("\r", "\\r")
|
||||
return subject.replace('\n', '\\n').replace('\r', '\\r')
|
||||
|
||||
|
||||
class CallbackFilter(logging.Filter):
|
||||
@@ -148,7 +140,6 @@ class CallbackFilter(logging.Filter):
|
||||
takes the record-to-be-logged as its only parameter) to decide whether to
|
||||
log a record.
|
||||
"""
|
||||
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
|
||||
@@ -169,7 +160,7 @@ class RequireDebugTrue(logging.Filter):
|
||||
|
||||
|
||||
class ServerFormatter(logging.Formatter):
|
||||
default_time_format = "%d/%b/%Y %H:%M:%S"
|
||||
default_time_format = '%d/%b/%Y %H:%M:%S'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.style = color_style()
|
||||
@@ -177,7 +168,7 @@ class ServerFormatter(logging.Formatter):
|
||||
|
||||
def format(self, record):
|
||||
msg = record.msg
|
||||
status_code = getattr(record, "status_code", None)
|
||||
status_code = getattr(record, 'status_code', None)
|
||||
|
||||
if status_code:
|
||||
if 200 <= status_code < 300:
|
||||
@@ -197,25 +188,17 @@ class ServerFormatter(logging.Formatter):
|
||||
# Any 5XX, or any other status code
|
||||
msg = self.style.HTTP_SERVER_ERROR(msg)
|
||||
|
||||
if self.uses_server_time() and not hasattr(record, "server_time"):
|
||||
if self.uses_server_time() and not hasattr(record, 'server_time'):
|
||||
record.server_time = self.formatTime(record, self.datefmt)
|
||||
|
||||
record.msg = msg
|
||||
return super().format(record)
|
||||
|
||||
def uses_server_time(self):
|
||||
return self._fmt.find("{server_time}") >= 0
|
||||
return self._fmt.find('{server_time}') >= 0
|
||||
|
||||
|
||||
def log_response(
|
||||
message,
|
||||
*args,
|
||||
response=None,
|
||||
request=None,
|
||||
logger=request_logger,
|
||||
level=None,
|
||||
exc_info=None,
|
||||
):
|
||||
def log_response(message, *args, response=None, request=None, logger=request_logger, level=None, exc_info=None):
|
||||
"""
|
||||
Log errors based on HttpResponse status.
|
||||
|
||||
@@ -227,23 +210,22 @@ def log_response(
|
||||
# the same response can be received in some cases, e.g., when the
|
||||
# response is the result of an exception and is logged at the time the
|
||||
# exception is caught so that the exc_info can be recorded.
|
||||
if getattr(response, "_has_been_logged", False):
|
||||
if getattr(response, '_has_been_logged', False):
|
||||
return
|
||||
|
||||
if level is None:
|
||||
if response.status_code >= 500:
|
||||
level = "error"
|
||||
level = 'error'
|
||||
elif response.status_code >= 400:
|
||||
level = "warning"
|
||||
level = 'warning'
|
||||
else:
|
||||
level = "info"
|
||||
level = 'info'
|
||||
|
||||
getattr(logger, level)(
|
||||
message,
|
||||
*args,
|
||||
message, *args,
|
||||
extra={
|
||||
"status_code": response.status_code,
|
||||
"request": request,
|
||||
'status_code': response.status_code,
|
||||
'request': request,
|
||||
},
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
@@ -5,220 +5,51 @@ 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."
|
||||
'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",
|
||||
'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",
|
||||
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur',
|
||||
'adipisicing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt',
|
||||
'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua',
|
||||
)
|
||||
|
||||
|
||||
@@ -231,13 +62,10 @@ def sentence():
|
||||
"""
|
||||
# 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)
|
||||
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("?."))
|
||||
return '%s%s%s' % (s[0].upper(), s[1:], random.choice('?.'))
|
||||
|
||||
|
||||
def paragraph():
|
||||
@@ -246,7 +74,7 @@ def paragraph():
|
||||
|
||||
The paragraph consists of between 1 and 4 sentences, inclusive.
|
||||
"""
|
||||
return " ".join(sentence() for i in range(random.randint(1, 4)))
|
||||
return ' '.join(sentence() for i in range(random.randint(1, 4)))
|
||||
|
||||
|
||||
def paragraphs(count, common=True):
|
||||
@@ -283,4 +111,4 @@ def words(count, common=True):
|
||||
word_list += random.sample(WORDS, c)
|
||||
else:
|
||||
word_list = word_list[:count]
|
||||
return " ".join(word_list)
|
||||
return ' '.join(word_list)
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from importlib.util import find_spec as importlib_find
|
||||
|
||||
|
||||
def cached_import(module_path, class_name):
|
||||
modules = sys.modules
|
||||
if module_path not in modules or (
|
||||
# Module is not fully initialized.
|
||||
getattr(modules[module_path], "__spec__", None) is not None
|
||||
and getattr(modules[module_path].__spec__, "_initializing", False) is True
|
||||
):
|
||||
import_module(module_path)
|
||||
return getattr(modules[module_path], class_name)
|
||||
|
||||
|
||||
def import_string(dotted_path):
|
||||
"""
|
||||
Import a dotted module path and return the attribute/class designated by the
|
||||
last name in the path. Raise ImportError if the import failed.
|
||||
"""
|
||||
try:
|
||||
module_path, class_name = dotted_path.rsplit(".", 1)
|
||||
module_path, class_name = dotted_path.rsplit('.', 1)
|
||||
except ValueError as err:
|
||||
raise ImportError("%s doesn't look like a module path" % dotted_path) from err
|
||||
|
||||
module = import_module(module_path)
|
||||
|
||||
try:
|
||||
return cached_import(module_path, class_name)
|
||||
return getattr(module, class_name)
|
||||
except AttributeError as err:
|
||||
raise ImportError(
|
||||
'Module "%s" does not define a "%s" attribute/class'
|
||||
% (module_path, class_name)
|
||||
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
|
||||
module_path, class_name)
|
||||
) from err
|
||||
|
||||
|
||||
@@ -47,7 +36,7 @@ def autodiscover_modules(*args, **kwargs):
|
||||
"""
|
||||
from django.apps import apps
|
||||
|
||||
register_to = kwargs.get("register_to")
|
||||
register_to = kwargs.get('register_to')
|
||||
for app_config in apps.get_app_configs():
|
||||
for module_to_search in args:
|
||||
# Attempt to import the app's module.
|
||||
@@ -55,7 +44,7 @@ def autodiscover_modules(*args, **kwargs):
|
||||
if register_to:
|
||||
before_import_registry = copy.copy(register_to._registry)
|
||||
|
||||
import_module("%s.%s" % (app_config.name, module_to_search))
|
||||
import_module('%s.%s' % (app_config.name, module_to_search))
|
||||
except Exception:
|
||||
# Reset the registry to the state before the last import
|
||||
# as this import will have to reoccur on the next request and
|
||||
@@ -80,12 +69,13 @@ def module_has_submodule(package, module_name):
|
||||
# package isn't a package.
|
||||
return False
|
||||
|
||||
full_module_name = package_name + "." + module_name
|
||||
full_module_name = package_name + '.' + module_name
|
||||
try:
|
||||
return importlib_find(full_module_name, package_path) is not None
|
||||
except ModuleNotFoundError:
|
||||
except (ModuleNotFoundError, AttributeError):
|
||||
# When module_name is an invalid dotted path, Python raises
|
||||
# ModuleNotFoundError.
|
||||
# ModuleNotFoundError. AttributeError is raised on PY36 (fixed in PY37)
|
||||
# if the penultimate part of the path is not a package.
|
||||
return False
|
||||
|
||||
|
||||
@@ -96,12 +86,12 @@ def module_dir(module):
|
||||
Raise ValueError otherwise, e.g. for namespace packages that are split
|
||||
over several directories.
|
||||
"""
|
||||
# Convert to list because __path__ may not support indexing.
|
||||
paths = list(getattr(module, "__path__", []))
|
||||
# Convert to list because _NamespacePath does not support indexing.
|
||||
paths = list(getattr(module, '__path__', []))
|
||||
if len(paths) == 1:
|
||||
return paths[0]
|
||||
else:
|
||||
filename = getattr(module, "__file__", None)
|
||||
filename = getattr(module, '__file__', None)
|
||||
if filename is not None:
|
||||
return os.path.dirname(filename)
|
||||
raise ValueError("Cannot determine directory containing %s" % module)
|
||||
|
||||
@@ -4,15 +4,8 @@ from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
def format(
|
||||
number,
|
||||
decimal_sep,
|
||||
decimal_pos=None,
|
||||
grouping=0,
|
||||
thousand_sep="",
|
||||
force_grouping=False,
|
||||
use_l10n=None,
|
||||
):
|
||||
def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='',
|
||||
force_grouping=False, use_l10n=None):
|
||||
"""
|
||||
Get a number (as a number or string), and return it as a string,
|
||||
using formats defined as arguments:
|
||||
@@ -25,61 +18,54 @@ def format(
|
||||
module in locale.localeconv() LC_NUMERIC grouping (e.g. (3, 2, 0)).
|
||||
* thousand_sep: Thousand separator symbol (for example ",")
|
||||
"""
|
||||
use_grouping = (
|
||||
use_l10n or (use_l10n is None and settings.USE_L10N)
|
||||
) and settings.USE_THOUSAND_SEPARATOR
|
||||
use_grouping = (use_l10n or (use_l10n is None and settings.USE_L10N)) and settings.USE_THOUSAND_SEPARATOR
|
||||
use_grouping = use_grouping or force_grouping
|
||||
use_grouping = use_grouping and grouping != 0
|
||||
# Make the common case fast
|
||||
if isinstance(number, int) and not use_grouping and not decimal_pos:
|
||||
return mark_safe(number)
|
||||
# sign
|
||||
sign = ""
|
||||
sign = ''
|
||||
# Treat potentially very large/small floats as Decimals.
|
||||
if isinstance(number, float) and "e" in str(number).lower():
|
||||
if isinstance(number, float) and 'e' in str(number).lower():
|
||||
number = Decimal(str(number))
|
||||
if isinstance(number, Decimal):
|
||||
|
||||
if decimal_pos is not None:
|
||||
# If the provided number is too small to affect any of the visible
|
||||
# decimal places, consider it equal to '0'.
|
||||
cutoff = Decimal("0." + "1".rjust(decimal_pos, "0"))
|
||||
cutoff = Decimal('0.' + '1'.rjust(decimal_pos, '0'))
|
||||
if abs(number) < cutoff:
|
||||
number = Decimal("0")
|
||||
number = Decimal('0')
|
||||
|
||||
# Format values with more than 200 digits (an arbitrary cutoff) using
|
||||
# scientific notation to avoid high memory usage in {:f}'.format().
|
||||
_, digits, exponent = number.as_tuple()
|
||||
if abs(exponent) + len(digits) > 200:
|
||||
number = "{:e}".format(number)
|
||||
coefficient, exponent = number.split("e")
|
||||
number = '{:e}'.format(number)
|
||||
coefficient, exponent = number.split('e')
|
||||
# Format the coefficient.
|
||||
coefficient = format(
|
||||
coefficient,
|
||||
decimal_sep,
|
||||
decimal_pos,
|
||||
grouping,
|
||||
thousand_sep,
|
||||
force_grouping,
|
||||
use_l10n,
|
||||
coefficient, decimal_sep, decimal_pos, grouping,
|
||||
thousand_sep, force_grouping, use_l10n,
|
||||
)
|
||||
return "{}e{}".format(coefficient, exponent)
|
||||
return '{}e{}'.format(coefficient, exponent)
|
||||
else:
|
||||
str_number = "{:f}".format(number)
|
||||
str_number = '{:f}'.format(number)
|
||||
else:
|
||||
str_number = str(number)
|
||||
if str_number[0] == "-":
|
||||
sign = "-"
|
||||
if str_number[0] == '-':
|
||||
sign = '-'
|
||||
str_number = str_number[1:]
|
||||
# decimal part
|
||||
if "." in str_number:
|
||||
int_part, dec_part = str_number.split(".")
|
||||
if '.' in str_number:
|
||||
int_part, dec_part = str_number.split('.')
|
||||
if decimal_pos is not None:
|
||||
dec_part = dec_part[:decimal_pos]
|
||||
else:
|
||||
int_part, dec_part = str_number, ""
|
||||
int_part, dec_part = str_number, ''
|
||||
if decimal_pos is not None:
|
||||
dec_part = dec_part + ("0" * (decimal_pos - len(dec_part)))
|
||||
dec_part = dec_part + ('0' * (decimal_pos - len(dec_part)))
|
||||
dec_part = dec_part and decimal_sep + dec_part
|
||||
# grouping
|
||||
if use_grouping:
|
||||
@@ -90,7 +76,7 @@ def format(
|
||||
# grouping is a single value
|
||||
intervals = [grouping, 0]
|
||||
active_interval = intervals.pop(0)
|
||||
int_part_gd = ""
|
||||
int_part_gd = ''
|
||||
cnt = 0
|
||||
for digit in int_part[::-1]:
|
||||
if cnt and cnt == active_interval:
|
||||
|
||||
@@ -73,23 +73,23 @@ def normalize(pattern):
|
||||
try:
|
||||
ch, escaped = next(pattern_iter)
|
||||
except StopIteration:
|
||||
return [("", [])]
|
||||
return [('', [])]
|
||||
|
||||
try:
|
||||
while True:
|
||||
if escaped:
|
||||
result.append(ch)
|
||||
elif ch == ".":
|
||||
elif ch == '.':
|
||||
# Replace "any character" with an arbitrary representative.
|
||||
result.append(".")
|
||||
elif ch == "|":
|
||||
elif ch == '|':
|
||||
# FIXME: One day we'll should do this, but not in 1.0.
|
||||
raise NotImplementedError("Awaiting Implementation")
|
||||
raise NotImplementedError('Awaiting Implementation')
|
||||
elif ch == "^":
|
||||
pass
|
||||
elif ch == "$":
|
||||
elif ch == '$':
|
||||
break
|
||||
elif ch == ")":
|
||||
elif ch == ')':
|
||||
# This can only be the end of a non-capturing group, since all
|
||||
# other unescaped parentheses are handled by the grouping
|
||||
# section later (and the full group is handled there).
|
||||
@@ -99,17 +99,17 @@ def normalize(pattern):
|
||||
start = non_capturing_groups.pop()
|
||||
inner = NonCapture(result[start:])
|
||||
result = result[:start] + [inner]
|
||||
elif ch == "[":
|
||||
elif ch == '[':
|
||||
# Replace ranges with the first character in the range.
|
||||
ch, escaped = next(pattern_iter)
|
||||
result.append(ch)
|
||||
ch, escaped = next(pattern_iter)
|
||||
while escaped or ch != "]":
|
||||
while escaped or ch != ']':
|
||||
ch, escaped = next(pattern_iter)
|
||||
elif ch == "(":
|
||||
elif ch == '(':
|
||||
# Some kind of group.
|
||||
ch, escaped = next(pattern_iter)
|
||||
if ch != "?" or escaped:
|
||||
if ch != '?' or escaped:
|
||||
# A positional group
|
||||
name = "_%d" % num_args
|
||||
num_args += 1
|
||||
@@ -117,39 +117,37 @@ def normalize(pattern):
|
||||
walk_to_end(ch, pattern_iter)
|
||||
else:
|
||||
ch, escaped = next(pattern_iter)
|
||||
if ch in "!=<":
|
||||
if ch in '!=<':
|
||||
# All of these are ignorable. Walk to the end of the
|
||||
# group.
|
||||
walk_to_end(ch, pattern_iter)
|
||||
elif ch == ":":
|
||||
elif ch == ':':
|
||||
# Non-capturing group
|
||||
non_capturing_groups.append(len(result))
|
||||
elif ch != "P":
|
||||
elif ch != 'P':
|
||||
# Anything else, other than a named group, is something
|
||||
# we cannot reverse.
|
||||
raise ValueError("Non-reversible reg-exp portion: '(?%s'" % ch)
|
||||
else:
|
||||
ch, escaped = next(pattern_iter)
|
||||
if ch not in ("<", "="):
|
||||
raise ValueError(
|
||||
"Non-reversible reg-exp portion: '(?P%s'" % ch
|
||||
)
|
||||
if ch not in ('<', '='):
|
||||
raise ValueError("Non-reversible reg-exp portion: '(?P%s'" % ch)
|
||||
# We are in a named capturing group. Extra the name and
|
||||
# then skip to the end.
|
||||
if ch == "<":
|
||||
terminal_char = ">"
|
||||
if ch == '<':
|
||||
terminal_char = '>'
|
||||
# We are in a named backreference.
|
||||
else:
|
||||
terminal_char = ")"
|
||||
terminal_char = ')'
|
||||
name = []
|
||||
ch, escaped = next(pattern_iter)
|
||||
while ch != terminal_char:
|
||||
name.append(ch)
|
||||
ch, escaped = next(pattern_iter)
|
||||
param = "".join(name)
|
||||
param = ''.join(name)
|
||||
# Named backreferences have already consumed the
|
||||
# parenthesis.
|
||||
if terminal_char != ")":
|
||||
if terminal_char != ')':
|
||||
result.append(Group((("%%(%s)s" % param), param)))
|
||||
walk_to_end(ch, pattern_iter)
|
||||
else:
|
||||
@@ -187,7 +185,7 @@ def normalize(pattern):
|
||||
pass
|
||||
except NotImplementedError:
|
||||
# A case of using the disjunctive form. No results for you!
|
||||
return [("", [])]
|
||||
return [('', [])]
|
||||
|
||||
return list(zip(*flatten_result(result)))
|
||||
|
||||
@@ -203,7 +201,7 @@ def next_char(input_iter):
|
||||
raw (unescaped) character or not.
|
||||
"""
|
||||
for ch in input_iter:
|
||||
if ch != "\\":
|
||||
if ch != '\\':
|
||||
yield ch, False
|
||||
continue
|
||||
ch = next(input_iter)
|
||||
@@ -219,16 +217,16 @@ def walk_to_end(ch, input_iter):
|
||||
this group, skipping over any nested groups and handling escaped
|
||||
parentheses correctly.
|
||||
"""
|
||||
if ch == "(":
|
||||
if ch == '(':
|
||||
nesting = 1
|
||||
else:
|
||||
nesting = 0
|
||||
for ch, escaped in input_iter:
|
||||
if escaped:
|
||||
continue
|
||||
elif ch == "(":
|
||||
elif ch == '(':
|
||||
nesting += 1
|
||||
elif ch == ")":
|
||||
elif ch == ')':
|
||||
if not nesting:
|
||||
return
|
||||
nesting -= 1
|
||||
@@ -243,30 +241,30 @@ def get_quantifier(ch, input_iter):
|
||||
either None or the next character from the input_iter if the next character
|
||||
is not part of the quantifier.
|
||||
"""
|
||||
if ch in "*?+":
|
||||
if ch in '*?+':
|
||||
try:
|
||||
ch2, escaped = next(input_iter)
|
||||
except StopIteration:
|
||||
ch2 = None
|
||||
if ch2 == "?":
|
||||
if ch2 == '?':
|
||||
ch2 = None
|
||||
if ch == "+":
|
||||
if ch == '+':
|
||||
return 1, ch2
|
||||
return 0, ch2
|
||||
|
||||
quant = []
|
||||
while ch != "}":
|
||||
while ch != '}':
|
||||
ch, escaped = next(input_iter)
|
||||
quant.append(ch)
|
||||
quant = quant[:-1]
|
||||
values = "".join(quant).split(",")
|
||||
values = ''.join(quant).split(',')
|
||||
|
||||
# Consume the trailing '?', if necessary.
|
||||
try:
|
||||
ch, escaped = next(input_iter)
|
||||
except StopIteration:
|
||||
ch = None
|
||||
if ch == "?":
|
||||
if ch == '?':
|
||||
ch = None
|
||||
return int(values[0]), ch
|
||||
|
||||
@@ -292,20 +290,20 @@ def flatten_result(source):
|
||||
Each of the two lists will be of the same length.
|
||||
"""
|
||||
if source is None:
|
||||
return [""], [[]]
|
||||
return [''], [[]]
|
||||
if isinstance(source, Group):
|
||||
if source[1] is None:
|
||||
params = []
|
||||
else:
|
||||
params = [source[1]]
|
||||
return [source[0]], [params]
|
||||
result = [""]
|
||||
result = ['']
|
||||
result_args = [[]]
|
||||
pos = last = 0
|
||||
for pos, elt in enumerate(source):
|
||||
if isinstance(elt, str):
|
||||
continue
|
||||
piece = "".join(source[last:pos])
|
||||
piece = ''.join(source[last:pos])
|
||||
if isinstance(elt, Group):
|
||||
piece += elt[0]
|
||||
param = elt[1]
|
||||
@@ -333,7 +331,7 @@ def flatten_result(source):
|
||||
result = new_result
|
||||
result_args = new_args
|
||||
if pos >= last:
|
||||
piece = "".join(source[last:])
|
||||
piece = ''.join(source[last:])
|
||||
for i in range(len(result)):
|
||||
result[i] += piece
|
||||
return result, result_args
|
||||
@@ -341,13 +339,13 @@ def flatten_result(source):
|
||||
|
||||
def _lazy_re_compile(regex, flags=0):
|
||||
"""Lazily compile a regex with flags."""
|
||||
|
||||
def _compile():
|
||||
# Compile the regex if it was not passed pre-compiled.
|
||||
if isinstance(regex, (str, bytes)):
|
||||
return re.compile(regex, flags)
|
||||
else:
|
||||
assert not flags, "flags must be empty if regex is passed pre-compiled"
|
||||
assert not flags, (
|
||||
'flags must be empty if regex is passed pre-compiled'
|
||||
)
|
||||
return regex
|
||||
|
||||
return SimpleLazyObject(_compile)
|
||||
|
||||
@@ -23,7 +23,6 @@ class SafeString(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
|
||||
@@ -45,7 +44,6 @@ def _safety_decorator(safety_marker, func):
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return safety_marker(func(*args, **kwargs))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@@ -58,7 +56,7 @@ def mark_safe(s):
|
||||
|
||||
Can be called multiple times on a single string.
|
||||
"""
|
||||
if hasattr(s, "__html__"):
|
||||
if hasattr(s, '__html__'):
|
||||
return s
|
||||
if callable(s):
|
||||
return _safety_decorator(mark_safe, s)
|
||||
|
||||
@@ -2,21 +2,15 @@
|
||||
termcolors.py
|
||||
"""
|
||||
|
||||
color_names = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white")
|
||||
foreground = {color_names[x]: "3%s" % x for x in range(8)}
|
||||
background = {color_names[x]: "4%s" % x for x in range(8)}
|
||||
color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
|
||||
foreground = {color_names[x]: '3%s' % x for x in range(8)}
|
||||
background = {color_names[x]: '4%s' % x for x in range(8)}
|
||||
|
||||
RESET = "0"
|
||||
opt_dict = {
|
||||
"bold": "1",
|
||||
"underscore": "4",
|
||||
"blink": "5",
|
||||
"reverse": "7",
|
||||
"conceal": "8",
|
||||
}
|
||||
RESET = '0'
|
||||
opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'}
|
||||
|
||||
|
||||
def colorize(text="", opts=(), **kwargs):
|
||||
def colorize(text='', opts=(), **kwargs):
|
||||
"""
|
||||
Return your text, enclosed in ANSI graphics codes.
|
||||
|
||||
@@ -46,19 +40,19 @@ def colorize(text="", opts=(), **kwargs):
|
||||
print('this should not be red')
|
||||
"""
|
||||
code_list = []
|
||||
if text == "" and len(opts) == 1 and opts[0] == "reset":
|
||||
return "\x1b[%sm" % RESET
|
||||
if text == '' and len(opts) == 1 and opts[0] == 'reset':
|
||||
return '\x1b[%sm' % RESET
|
||||
for k, v in kwargs.items():
|
||||
if k == "fg":
|
||||
if k == 'fg':
|
||||
code_list.append(foreground[v])
|
||||
elif k == "bg":
|
||||
elif k == 'bg':
|
||||
code_list.append(background[v])
|
||||
for o in opts:
|
||||
if o in opt_dict:
|
||||
code_list.append(opt_dict[o])
|
||||
if "noreset" not in opts:
|
||||
text = "%s\x1b[%sm" % (text or "", RESET)
|
||||
return "%s%s" % (("\x1b[%sm" % ";".join(code_list)), text or "")
|
||||
if 'noreset' not in opts:
|
||||
text = '%s\x1b[%sm' % (text or '', RESET)
|
||||
return '%s%s' % (('\x1b[%sm' % ';'.join(code_list)), text or '')
|
||||
|
||||
|
||||
def make_style(opts=(), **kwargs):
|
||||
@@ -74,68 +68,68 @@ def make_style(opts=(), **kwargs):
|
||||
return lambda text: colorize(text, opts, **kwargs)
|
||||
|
||||
|
||||
NOCOLOR_PALETTE = "nocolor"
|
||||
DARK_PALETTE = "dark"
|
||||
LIGHT_PALETTE = "light"
|
||||
NOCOLOR_PALETTE = 'nocolor'
|
||||
DARK_PALETTE = 'dark'
|
||||
LIGHT_PALETTE = 'light'
|
||||
|
||||
PALETTES = {
|
||||
NOCOLOR_PALETTE: {
|
||||
"ERROR": {},
|
||||
"SUCCESS": {},
|
||||
"WARNING": {},
|
||||
"NOTICE": {},
|
||||
"SQL_FIELD": {},
|
||||
"SQL_COLTYPE": {},
|
||||
"SQL_KEYWORD": {},
|
||||
"SQL_TABLE": {},
|
||||
"HTTP_INFO": {},
|
||||
"HTTP_SUCCESS": {},
|
||||
"HTTP_REDIRECT": {},
|
||||
"HTTP_NOT_MODIFIED": {},
|
||||
"HTTP_BAD_REQUEST": {},
|
||||
"HTTP_NOT_FOUND": {},
|
||||
"HTTP_SERVER_ERROR": {},
|
||||
"MIGRATE_HEADING": {},
|
||||
"MIGRATE_LABEL": {},
|
||||
'ERROR': {},
|
||||
'SUCCESS': {},
|
||||
'WARNING': {},
|
||||
'NOTICE': {},
|
||||
'SQL_FIELD': {},
|
||||
'SQL_COLTYPE': {},
|
||||
'SQL_KEYWORD': {},
|
||||
'SQL_TABLE': {},
|
||||
'HTTP_INFO': {},
|
||||
'HTTP_SUCCESS': {},
|
||||
'HTTP_REDIRECT': {},
|
||||
'HTTP_NOT_MODIFIED': {},
|
||||
'HTTP_BAD_REQUEST': {},
|
||||
'HTTP_NOT_FOUND': {},
|
||||
'HTTP_SERVER_ERROR': {},
|
||||
'MIGRATE_HEADING': {},
|
||||
'MIGRATE_LABEL': {},
|
||||
},
|
||||
DARK_PALETTE: {
|
||||
"ERROR": {"fg": "red", "opts": ("bold",)},
|
||||
"SUCCESS": {"fg": "green", "opts": ("bold",)},
|
||||
"WARNING": {"fg": "yellow", "opts": ("bold",)},
|
||||
"NOTICE": {"fg": "red"},
|
||||
"SQL_FIELD": {"fg": "green", "opts": ("bold",)},
|
||||
"SQL_COLTYPE": {"fg": "green"},
|
||||
"SQL_KEYWORD": {"fg": "yellow"},
|
||||
"SQL_TABLE": {"opts": ("bold",)},
|
||||
"HTTP_INFO": {"opts": ("bold",)},
|
||||
"HTTP_SUCCESS": {},
|
||||
"HTTP_REDIRECT": {"fg": "green"},
|
||||
"HTTP_NOT_MODIFIED": {"fg": "cyan"},
|
||||
"HTTP_BAD_REQUEST": {"fg": "red", "opts": ("bold",)},
|
||||
"HTTP_NOT_FOUND": {"fg": "yellow"},
|
||||
"HTTP_SERVER_ERROR": {"fg": "magenta", "opts": ("bold",)},
|
||||
"MIGRATE_HEADING": {"fg": "cyan", "opts": ("bold",)},
|
||||
"MIGRATE_LABEL": {"opts": ("bold",)},
|
||||
'ERROR': {'fg': 'red', 'opts': ('bold',)},
|
||||
'SUCCESS': {'fg': 'green', 'opts': ('bold',)},
|
||||
'WARNING': {'fg': 'yellow', 'opts': ('bold',)},
|
||||
'NOTICE': {'fg': 'red'},
|
||||
'SQL_FIELD': {'fg': 'green', 'opts': ('bold',)},
|
||||
'SQL_COLTYPE': {'fg': 'green'},
|
||||
'SQL_KEYWORD': {'fg': 'yellow'},
|
||||
'SQL_TABLE': {'opts': ('bold',)},
|
||||
'HTTP_INFO': {'opts': ('bold',)},
|
||||
'HTTP_SUCCESS': {},
|
||||
'HTTP_REDIRECT': {'fg': 'green'},
|
||||
'HTTP_NOT_MODIFIED': {'fg': 'cyan'},
|
||||
'HTTP_BAD_REQUEST': {'fg': 'red', 'opts': ('bold',)},
|
||||
'HTTP_NOT_FOUND': {'fg': 'yellow'},
|
||||
'HTTP_SERVER_ERROR': {'fg': 'magenta', 'opts': ('bold',)},
|
||||
'MIGRATE_HEADING': {'fg': 'cyan', 'opts': ('bold',)},
|
||||
'MIGRATE_LABEL': {'opts': ('bold',)},
|
||||
},
|
||||
LIGHT_PALETTE: {
|
||||
"ERROR": {"fg": "red", "opts": ("bold",)},
|
||||
"SUCCESS": {"fg": "green", "opts": ("bold",)},
|
||||
"WARNING": {"fg": "yellow", "opts": ("bold",)},
|
||||
"NOTICE": {"fg": "red"},
|
||||
"SQL_FIELD": {"fg": "green", "opts": ("bold",)},
|
||||
"SQL_COLTYPE": {"fg": "green"},
|
||||
"SQL_KEYWORD": {"fg": "blue"},
|
||||
"SQL_TABLE": {"opts": ("bold",)},
|
||||
"HTTP_INFO": {"opts": ("bold",)},
|
||||
"HTTP_SUCCESS": {},
|
||||
"HTTP_REDIRECT": {"fg": "green", "opts": ("bold",)},
|
||||
"HTTP_NOT_MODIFIED": {"fg": "green"},
|
||||
"HTTP_BAD_REQUEST": {"fg": "red", "opts": ("bold",)},
|
||||
"HTTP_NOT_FOUND": {"fg": "red"},
|
||||
"HTTP_SERVER_ERROR": {"fg": "magenta", "opts": ("bold",)},
|
||||
"MIGRATE_HEADING": {"fg": "cyan", "opts": ("bold",)},
|
||||
"MIGRATE_LABEL": {"opts": ("bold",)},
|
||||
},
|
||||
'ERROR': {'fg': 'red', 'opts': ('bold',)},
|
||||
'SUCCESS': {'fg': 'green', 'opts': ('bold',)},
|
||||
'WARNING': {'fg': 'yellow', 'opts': ('bold',)},
|
||||
'NOTICE': {'fg': 'red'},
|
||||
'SQL_FIELD': {'fg': 'green', 'opts': ('bold',)},
|
||||
'SQL_COLTYPE': {'fg': 'green'},
|
||||
'SQL_KEYWORD': {'fg': 'blue'},
|
||||
'SQL_TABLE': {'opts': ('bold',)},
|
||||
'HTTP_INFO': {'opts': ('bold',)},
|
||||
'HTTP_SUCCESS': {},
|
||||
'HTTP_REDIRECT': {'fg': 'green', 'opts': ('bold',)},
|
||||
'HTTP_NOT_MODIFIED': {'fg': 'green'},
|
||||
'HTTP_BAD_REQUEST': {'fg': 'red', 'opts': ('bold',)},
|
||||
'HTTP_NOT_FOUND': {'fg': 'red'},
|
||||
'HTTP_SERVER_ERROR': {'fg': 'magenta', 'opts': ('bold',)},
|
||||
'MIGRATE_HEADING': {'fg': 'cyan', 'opts': ('bold',)},
|
||||
'MIGRATE_LABEL': {'opts': ('bold',)},
|
||||
}
|
||||
}
|
||||
DEFAULT_PALETTE = DARK_PALETTE
|
||||
|
||||
@@ -175,39 +169,39 @@ def parse_color_setting(config_string):
|
||||
return PALETTES[DEFAULT_PALETTE]
|
||||
|
||||
# Split the color configuration into parts
|
||||
parts = config_string.lower().split(";")
|
||||
parts = config_string.lower().split(';')
|
||||
palette = PALETTES[NOCOLOR_PALETTE].copy()
|
||||
for part in parts:
|
||||
if part in PALETTES:
|
||||
# A default palette has been specified
|
||||
palette.update(PALETTES[part])
|
||||
elif "=" in part:
|
||||
elif '=' in part:
|
||||
# Process a palette defining string
|
||||
definition = {}
|
||||
|
||||
# Break the definition into the role,
|
||||
# plus the list of specific instructions.
|
||||
# The role must be in upper case
|
||||
role, instructions = part.split("=")
|
||||
role, instructions = part.split('=')
|
||||
role = role.upper()
|
||||
|
||||
styles = instructions.split(",")
|
||||
styles = instructions.split(',')
|
||||
styles.reverse()
|
||||
|
||||
# The first instruction can contain a slash
|
||||
# to break apart fg/bg.
|
||||
colors = styles.pop().split("/")
|
||||
colors = styles.pop().split('/')
|
||||
colors.reverse()
|
||||
fg = colors.pop()
|
||||
if fg in color_names:
|
||||
definition["fg"] = fg
|
||||
definition['fg'] = fg
|
||||
if colors and colors[-1] in color_names:
|
||||
definition["bg"] = colors[-1]
|
||||
definition['bg'] = colors[-1]
|
||||
|
||||
# All remaining instructions are options
|
||||
opts = tuple(s for s in styles if s in opt_dict)
|
||||
if opts:
|
||||
definition["opts"] = opts
|
||||
definition['opts'] = opts
|
||||
|
||||
# The nocolor palette has all available roles.
|
||||
# Use that palette as the basis for determining
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
import html.entities
|
||||
import re
|
||||
import unicodedata
|
||||
import warnings
|
||||
from gzip import GzipFile
|
||||
from gzip import compress as gzip_compress
|
||||
from io import BytesIO
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.utils.deprecation import RemovedInDjango40Warning
|
||||
from django.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy, pgettext
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def capfirst(x):
|
||||
"""Capitalize the first letter of a string."""
|
||||
if not x:
|
||||
return x
|
||||
if not isinstance(x, str):
|
||||
x = str(x)
|
||||
return x[0].upper() + x[1:]
|
||||
return x and str(x)[0].upper() + str(x)[1:]
|
||||
|
||||
|
||||
# Set up regular expressions
|
||||
re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
|
||||
re_chars = _lazy_re_compile(r"<[^>]+?>|(.)", re.S)
|
||||
re_tag = _lazy_re_compile(r"<(/)?(\S+?)(?:(\s*/)|\s.*?)?>", re.S)
|
||||
re_newlines = _lazy_re_compile(r"\r\n|\r") # Used in normalize_newlines
|
||||
re_camel_case = _lazy_re_compile(r"(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))")
|
||||
re_words = _lazy_re_compile(r'<[^>]+?>|([^<>\s]+)', re.S)
|
||||
re_chars = _lazy_re_compile(r'<[^>]+?>|(.)', re.S)
|
||||
re_tag = _lazy_re_compile(r'<(/)?(\S+?)(?:(\s*/)|\s.*?)?>', re.S)
|
||||
re_newlines = _lazy_re_compile(r'\r\n|\r') # Used in normalize_newlines
|
||||
re_camel_case = _lazy_re_compile(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
@@ -42,49 +38,46 @@ def wrap(text, width):
|
||||
Don't wrap long words, thus the output text may have lines longer than
|
||||
``width``.
|
||||
"""
|
||||
|
||||
def _generator():
|
||||
for line in text.splitlines(True): # True keeps trailing linebreaks
|
||||
max_width = min((line.endswith("\n") and width + 1 or width), width)
|
||||
max_width = min((line.endswith('\n') and width + 1 or width), width)
|
||||
while len(line) > max_width:
|
||||
space = line[: max_width + 1].rfind(" ") + 1
|
||||
space = line[:max_width + 1].rfind(' ') + 1
|
||||
if space == 0:
|
||||
space = line.find(" ") + 1
|
||||
space = line.find(' ') + 1
|
||||
if space == 0:
|
||||
yield line
|
||||
line = ""
|
||||
line = ''
|
||||
break
|
||||
yield "%s\n" % line[: space - 1]
|
||||
yield '%s\n' % line[:space - 1]
|
||||
line = line[space:]
|
||||
max_width = min((line.endswith("\n") and width + 1 or width), width)
|
||||
max_width = min((line.endswith('\n') and width + 1 or width), width)
|
||||
if line:
|
||||
yield line
|
||||
|
||||
return "".join(_generator())
|
||||
return ''.join(_generator())
|
||||
|
||||
|
||||
class Truncator(SimpleLazyObject):
|
||||
"""
|
||||
An object used to truncate text, either by characters or words.
|
||||
"""
|
||||
|
||||
def __init__(self, text):
|
||||
super().__init__(lambda: str(text))
|
||||
|
||||
def add_truncation_text(self, text, truncate=None):
|
||||
if truncate is None:
|
||||
truncate = pgettext(
|
||||
"String to return when truncating text", "%(truncated_text)s…"
|
||||
)
|
||||
if "%(truncated_text)s" in truncate:
|
||||
return truncate % {"truncated_text": text}
|
||||
'String to return when truncating text',
|
||||
'%(truncated_text)s…')
|
||||
if '%(truncated_text)s' in truncate:
|
||||
return truncate % {'truncated_text': text}
|
||||
# The truncation text didn't contain the %(truncated_text)s string
|
||||
# replacement argument so just append it to the text.
|
||||
if text.endswith(truncate):
|
||||
# But don't append the truncation text if the current text already
|
||||
# ends in this.
|
||||
return text
|
||||
return "%s%s" % (text, truncate)
|
||||
return '%s%s' % (text, truncate)
|
||||
|
||||
def chars(self, num, truncate=None, html=False):
|
||||
"""
|
||||
@@ -96,11 +89,11 @@ class Truncator(SimpleLazyObject):
|
||||
"""
|
||||
self._setup()
|
||||
length = int(num)
|
||||
text = unicodedata.normalize("NFC", self._wrapped)
|
||||
text = unicodedata.normalize('NFC', self._wrapped)
|
||||
|
||||
# Calculate the length to truncate to (max length - end_text length)
|
||||
truncate_len = length
|
||||
for char in self.add_truncation_text("", truncate):
|
||||
for char in self.add_truncation_text('', truncate):
|
||||
if not unicodedata.combining(char):
|
||||
truncate_len -= 1
|
||||
if truncate_len == 0:
|
||||
@@ -123,7 +116,8 @@ class Truncator(SimpleLazyObject):
|
||||
end_index = i
|
||||
if s_len > length:
|
||||
# Return the truncated string
|
||||
return self.add_truncation_text(text[: end_index or 0], truncate)
|
||||
return self.add_truncation_text(text[:end_index or 0],
|
||||
truncate)
|
||||
|
||||
# Return the original string since no truncation was necessary
|
||||
return text
|
||||
@@ -149,8 +143,8 @@ class Truncator(SimpleLazyObject):
|
||||
words = self._wrapped.split()
|
||||
if len(words) > length:
|
||||
words = words[:length]
|
||||
return self.add_truncation_text(" ".join(words), truncate)
|
||||
return " ".join(words)
|
||||
return self.add_truncation_text(' '.join(words), truncate)
|
||||
return ' '.join(words)
|
||||
|
||||
def _truncate_html(self, length, truncate, text, truncate_len, words):
|
||||
"""
|
||||
@@ -161,18 +155,11 @@ class Truncator(SimpleLazyObject):
|
||||
Preserve newlines in the HTML.
|
||||
"""
|
||||
if words and length <= 0:
|
||||
return ""
|
||||
return ''
|
||||
|
||||
html4_singlets = (
|
||||
"br",
|
||||
"col",
|
||||
"link",
|
||||
"base",
|
||||
"img",
|
||||
"param",
|
||||
"area",
|
||||
"hr",
|
||||
"input",
|
||||
'br', 'col', 'link', 'base', 'img',
|
||||
'param', 'area', 'hr', 'input'
|
||||
)
|
||||
|
||||
# Count non-HTML chars/words and keep note of open tags
|
||||
@@ -214,7 +201,7 @@ class Truncator(SimpleLazyObject):
|
||||
else:
|
||||
# SGML: An end tag closes, back to the matching start tag,
|
||||
# all unclosed intervening start tags with omitted end tags
|
||||
open_tags = open_tags[i + 1 :]
|
||||
open_tags = open_tags[i + 1:]
|
||||
else:
|
||||
# Add it to the start of the open tags list
|
||||
open_tags.insert(0, tagname)
|
||||
@@ -222,12 +209,12 @@ class Truncator(SimpleLazyObject):
|
||||
if current_len <= length:
|
||||
return text
|
||||
out = text[:end_text_pos]
|
||||
truncate_text = self.add_truncation_text("", truncate)
|
||||
truncate_text = self.add_truncation_text('', truncate)
|
||||
if truncate_text:
|
||||
out += truncate_text
|
||||
# Close any tags still open
|
||||
for tag in open_tags:
|
||||
out += "</%s>" % tag
|
||||
out += '</%s>' % tag
|
||||
# Return string
|
||||
return out
|
||||
|
||||
@@ -242,15 +229,15 @@ def get_valid_filename(name):
|
||||
>>> get_valid_filename("john's portrait in 2004.jpg")
|
||||
'johns_portrait_in_2004.jpg'
|
||||
"""
|
||||
s = str(name).strip().replace(" ", "_")
|
||||
s = re.sub(r"(?u)[^-\w.]", "", s)
|
||||
if s in {"", ".", ".."}:
|
||||
s = str(name).strip().replace(' ', '_')
|
||||
s = re.sub(r'(?u)[^-\w.]', '', s)
|
||||
if s in {'', '.', '..'}:
|
||||
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
|
||||
return s
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def get_text_list(list_, last_word=gettext_lazy("or")):
|
||||
def get_text_list(list_, last_word=gettext_lazy('or')):
|
||||
"""
|
||||
>>> get_text_list(['a', 'b', 'c', 'd'])
|
||||
'a, b, c or d'
|
||||
@@ -264,59 +251,40 @@ def get_text_list(list_, last_word=gettext_lazy("or")):
|
||||
''
|
||||
"""
|
||||
if not list_:
|
||||
return ""
|
||||
return ''
|
||||
if len(list_) == 1:
|
||||
return str(list_[0])
|
||||
return "%s %s %s" % (
|
||||
return '%s %s %s' % (
|
||||
# Translators: This string is used as a separator between list elements
|
||||
_(", ").join(str(i) for i in list_[:-1]),
|
||||
str(last_word),
|
||||
str(list_[-1]),
|
||||
_(', ').join(str(i) for i in list_[:-1]), str(last_word), str(list_[-1])
|
||||
)
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def normalize_newlines(text):
|
||||
"""Normalize CRLF and CR newlines to just LF."""
|
||||
return re_newlines.sub("\n", str(text))
|
||||
return re_newlines.sub('\n', str(text))
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def phone2numeric(phone):
|
||||
"""Convert a phone number with letters into its numeric equivalent."""
|
||||
char2number = {
|
||||
"a": "2",
|
||||
"b": "2",
|
||||
"c": "2",
|
||||
"d": "3",
|
||||
"e": "3",
|
||||
"f": "3",
|
||||
"g": "4",
|
||||
"h": "4",
|
||||
"i": "4",
|
||||
"j": "5",
|
||||
"k": "5",
|
||||
"l": "5",
|
||||
"m": "6",
|
||||
"n": "6",
|
||||
"o": "6",
|
||||
"p": "7",
|
||||
"q": "7",
|
||||
"r": "7",
|
||||
"s": "7",
|
||||
"t": "8",
|
||||
"u": "8",
|
||||
"v": "8",
|
||||
"w": "9",
|
||||
"x": "9",
|
||||
"y": "9",
|
||||
"z": "9",
|
||||
'a': '2', 'b': '2', 'c': '2', 'd': '3', 'e': '3', 'f': '3', 'g': '4',
|
||||
'h': '4', 'i': '4', 'j': '5', 'k': '5', 'l': '5', 'm': '6', 'n': '6',
|
||||
'o': '6', 'p': '7', 'q': '7', 'r': '7', 's': '7', 't': '8', 'u': '8',
|
||||
'v': '8', 'w': '9', 'x': '9', 'y': '9', 'z': '9',
|
||||
}
|
||||
return "".join(char2number.get(c, c) for c in phone.lower())
|
||||
return ''.join(char2number.get(c, c) for c in phone.lower())
|
||||
|
||||
|
||||
# From http://www.xhaus.com/alan/python/httpcomp.html#gzip
|
||||
# Used with permission.
|
||||
def compress_string(s):
|
||||
return gzip_compress(s, compresslevel=6, mtime=0)
|
||||
zbuf = BytesIO()
|
||||
with GzipFile(mode='wb', compresslevel=6, fileobj=zbuf, mtime=0) as zfile:
|
||||
zfile.write(s)
|
||||
return zbuf.getvalue()
|
||||
|
||||
|
||||
class StreamingBuffer(BytesIO):
|
||||
@@ -330,7 +298,7 @@ class StreamingBuffer(BytesIO):
|
||||
# Like compress_string, but for iterators of strings.
|
||||
def compress_sequence(sequence):
|
||||
buf = StreamingBuffer()
|
||||
with GzipFile(mode="wb", compresslevel=6, fileobj=buf, mtime=0) as zfile:
|
||||
with GzipFile(mode='wb', compresslevel=6, fileobj=buf, mtime=0) as zfile:
|
||||
# Output headers...
|
||||
yield buf.read()
|
||||
for item in sequence:
|
||||
@@ -343,8 +311,7 @@ def compress_sequence(sequence):
|
||||
|
||||
# Expression to match some_token and some_token="with spaces" (and similarly
|
||||
# for single-quoted strings).
|
||||
smart_split_re = _lazy_re_compile(
|
||||
r"""
|
||||
smart_split_re = _lazy_re_compile(r"""
|
||||
((?:
|
||||
[^\s'"]*
|
||||
(?:
|
||||
@@ -352,9 +319,7 @@ smart_split_re = _lazy_re_compile(
|
||||
[^\s'"]*
|
||||
)+
|
||||
) | \S+)
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
def smart_split(text):
|
||||
@@ -378,10 +343,10 @@ def smart_split(text):
|
||||
|
||||
def _replace_entity(match):
|
||||
text = match[1]
|
||||
if text[0] == "#":
|
||||
if text[0] == '#':
|
||||
text = text[1:]
|
||||
try:
|
||||
if text[0] in "xX":
|
||||
if text[0] in 'xX':
|
||||
c = int(text[1:], 16)
|
||||
else:
|
||||
c = int(text)
|
||||
@@ -398,6 +363,16 @@ def _replace_entity(match):
|
||||
_entity_re = _lazy_re_compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def unescape_entities(text):
|
||||
warnings.warn(
|
||||
'django.utils.text.unescape_entities() is deprecated in favor of '
|
||||
'html.unescape().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return _entity_re.sub(_replace_entity, str(text))
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def unescape_string_literal(s):
|
||||
r"""
|
||||
@@ -416,7 +391,7 @@ def unescape_string_literal(s):
|
||||
if s[0] not in "\"'" or s[-1] != s[0]:
|
||||
raise ValueError("Not a string literal: %r" % s)
|
||||
quote = s[0]
|
||||
return s[1:-1].replace(r"\%s" % quote, quote).replace(r"\\", "\\")
|
||||
return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
@@ -429,22 +404,18 @@ def slugify(value, allow_unicode=False):
|
||||
"""
|
||||
value = str(value)
|
||||
if allow_unicode:
|
||||
value = unicodedata.normalize("NFKC", value)
|
||||
value = unicodedata.normalize('NFKC', value)
|
||||
else:
|
||||
value = (
|
||||
unicodedata.normalize("NFKD", value)
|
||||
.encode("ascii", "ignore")
|
||||
.decode("ascii")
|
||||
)
|
||||
value = re.sub(r"[^\w\s-]", "", value.lower())
|
||||
return re.sub(r"[-\s]+", "-", value).strip("-_")
|
||||
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
|
||||
value = re.sub(r'[^\w\s-]', '', value.lower())
|
||||
return re.sub(r'[-\s]+', '-', value).strip('-_')
|
||||
|
||||
|
||||
def camel_case_to_spaces(value):
|
||||
"""
|
||||
Split CamelCase and convert to lowercase. Strip surrounding whitespace.
|
||||
"""
|
||||
return re_camel_case.sub(r" \1", value).strip().lower()
|
||||
return re_camel_case.sub(r' \1', value).strip().lower()
|
||||
|
||||
|
||||
def _format_lazy(format_string, *args, **kwargs):
|
||||
|
||||
@@ -6,21 +6,21 @@ from django.utils.timezone import is_aware, utc
|
||||
from django.utils.translation import gettext, ngettext_lazy
|
||||
|
||||
TIME_STRINGS = {
|
||||
"year": ngettext_lazy("%(num)d year", "%(num)d years", "num"),
|
||||
"month": ngettext_lazy("%(num)d month", "%(num)d months", "num"),
|
||||
"week": ngettext_lazy("%(num)d week", "%(num)d weeks", "num"),
|
||||
"day": ngettext_lazy("%(num)d day", "%(num)d days", "num"),
|
||||
"hour": ngettext_lazy("%(num)d hour", "%(num)d hours", "num"),
|
||||
"minute": ngettext_lazy("%(num)d minute", "%(num)d minutes", "num"),
|
||||
'year': ngettext_lazy('%d year', '%d years'),
|
||||
'month': ngettext_lazy('%d month', '%d months'),
|
||||
'week': ngettext_lazy('%d week', '%d weeks'),
|
||||
'day': ngettext_lazy('%d day', '%d days'),
|
||||
'hour': ngettext_lazy('%d hour', '%d hours'),
|
||||
'minute': ngettext_lazy('%d minute', '%d minutes'),
|
||||
}
|
||||
|
||||
TIMESINCE_CHUNKS = (
|
||||
(60 * 60 * 24 * 365, "year"),
|
||||
(60 * 60 * 24 * 30, "month"),
|
||||
(60 * 60 * 24 * 7, "week"),
|
||||
(60 * 60 * 24, "day"),
|
||||
(60 * 60, "hour"),
|
||||
(60, "minute"),
|
||||
(60 * 60 * 24 * 365, 'year'),
|
||||
(60 * 60 * 24 * 30, 'month'),
|
||||
(60 * 60 * 24 * 7, 'week'),
|
||||
(60 * 60 * 24, 'day'),
|
||||
(60 * 60, 'hour'),
|
||||
(60, 'minute'),
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
|
||||
if time_strings is None:
|
||||
time_strings = TIME_STRINGS
|
||||
if depth <= 0:
|
||||
raise ValueError("depth must be greater than 0.")
|
||||
raise ValueError('depth must be greater than 0.')
|
||||
# Convert datetime.date to datetime.datetime for comparison.
|
||||
if not isinstance(d, datetime.datetime):
|
||||
d = datetime.datetime(d.year, d.month, d.day)
|
||||
@@ -73,13 +73,13 @@ def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
|
||||
since = delta.days * 24 * 60 * 60 + delta.seconds
|
||||
if since <= 0:
|
||||
# d is in the future compared to now, stop processing.
|
||||
return avoid_wrapping(time_strings["minute"] % {"num": 0})
|
||||
return avoid_wrapping(time_strings['minute'] % 0)
|
||||
for i, (seconds, name) in enumerate(TIMESINCE_CHUNKS):
|
||||
count = since // seconds
|
||||
if count != 0:
|
||||
break
|
||||
else:
|
||||
return avoid_wrapping(time_strings["minute"] % {"num": 0})
|
||||
return avoid_wrapping(time_strings['minute'] % 0)
|
||||
result = []
|
||||
current_depth = 0
|
||||
while i < len(TIMESINCE_CHUNKS) and current_depth < depth:
|
||||
@@ -87,11 +87,11 @@ def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
|
||||
count = since // seconds
|
||||
if count == 0:
|
||||
break
|
||||
result.append(avoid_wrapping(time_strings[name] % {"num": count}))
|
||||
result.append(avoid_wrapping(time_strings[name] % count))
|
||||
since -= seconds * count
|
||||
current_depth += 1
|
||||
i += 1
|
||||
return gettext(", ").join(result)
|
||||
return gettext(', ').join(result)
|
||||
|
||||
|
||||
def timeuntil(d, now=None, time_strings=None, depth=2):
|
||||
|
||||
@@ -3,53 +3,39 @@ Timezone-related classes and functions.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import zoneinfo
|
||||
except ImportError:
|
||||
from backports import zoneinfo
|
||||
|
||||
from contextlib import ContextDecorator
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
|
||||
import pytz
|
||||
from asgiref.local import Local
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
|
||||
__all__ = [
|
||||
"utc",
|
||||
"get_fixed_timezone",
|
||||
"get_default_timezone",
|
||||
"get_default_timezone_name",
|
||||
"get_current_timezone",
|
||||
"get_current_timezone_name",
|
||||
"activate",
|
||||
"deactivate",
|
||||
"override",
|
||||
"localtime",
|
||||
"now",
|
||||
"is_aware",
|
||||
"is_naive",
|
||||
"make_aware",
|
||||
"make_naive",
|
||||
'utc', 'get_fixed_timezone',
|
||||
'get_default_timezone', 'get_default_timezone_name',
|
||||
'get_current_timezone', 'get_current_timezone_name',
|
||||
'activate', 'deactivate', 'override',
|
||||
'localtime', 'now',
|
||||
'is_aware', 'is_naive', 'make_aware', 'make_naive',
|
||||
]
|
||||
|
||||
# RemovedInDjango50Warning: sentinel for deprecation of is_dst parameters.
|
||||
NOT_PASSED = object()
|
||||
|
||||
# UTC time zone as a tzinfo instance.
|
||||
utc = pytz.utc
|
||||
|
||||
utc = timezone.utc
|
||||
_PYTZ_BASE_CLASSES = (pytz.tzinfo.BaseTzInfo, pytz._FixedOffset)
|
||||
# In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo
|
||||
if not isinstance(pytz.UTC, pytz._FixedOffset):
|
||||
_PYTZ_BASE_CLASSES = _PYTZ_BASE_CLASSES + (type(pytz.UTC),)
|
||||
|
||||
|
||||
def get_fixed_timezone(offset):
|
||||
"""Return a tzinfo instance with a fixed offset from UTC."""
|
||||
if isinstance(offset, timedelta):
|
||||
offset = offset.total_seconds() // 60
|
||||
sign = "-" if offset < 0 else "+"
|
||||
hhmm = "%02d%02d" % divmod(abs(offset), 60)
|
||||
sign = '-' if offset < 0 else '+'
|
||||
hhmm = '%02d%02d' % divmod(abs(offset), 60)
|
||||
name = sign + hhmm
|
||||
return timezone(timedelta(minutes=offset), name)
|
||||
|
||||
@@ -63,11 +49,7 @@ def get_default_timezone():
|
||||
|
||||
This is the time zone defined by settings.TIME_ZONE.
|
||||
"""
|
||||
if settings.USE_DEPRECATED_PYTZ:
|
||||
import pytz
|
||||
|
||||
return pytz.timezone(settings.TIME_ZONE)
|
||||
return zoneinfo.ZoneInfo(settings.TIME_ZONE)
|
||||
return pytz.timezone(settings.TIME_ZONE)
|
||||
|
||||
|
||||
# This function exists for consistency with get_current_timezone_name
|
||||
@@ -90,12 +72,8 @@ def get_current_timezone_name():
|
||||
|
||||
|
||||
def _get_timezone_name(timezone):
|
||||
"""
|
||||
Return the offset for fixed offset timezones, or the name of timezone if
|
||||
not set.
|
||||
"""
|
||||
return timezone.tzname(None) or str(timezone)
|
||||
|
||||
"""Return the name of ``timezone``."""
|
||||
return str(timezone)
|
||||
|
||||
# Timezone selection functions.
|
||||
|
||||
@@ -113,12 +91,7 @@ def activate(timezone):
|
||||
if isinstance(timezone, tzinfo):
|
||||
_active.value = timezone
|
||||
elif isinstance(timezone, str):
|
||||
if settings.USE_DEPRECATED_PYTZ:
|
||||
import pytz
|
||||
|
||||
_active.value = pytz.timezone(timezone)
|
||||
else:
|
||||
_active.value = zoneinfo.ZoneInfo(timezone)
|
||||
_active.value = pytz.timezone(timezone)
|
||||
else:
|
||||
raise ValueError("Invalid timezone: %r" % timezone)
|
||||
|
||||
@@ -145,12 +118,11 @@ class override(ContextDecorator):
|
||||
time zone name, or ``None``. If it is ``None``, Django enables the default
|
||||
time zone.
|
||||
"""
|
||||
|
||||
def __init__(self, timezone):
|
||||
self.timezone = timezone
|
||||
|
||||
def __enter__(self):
|
||||
self.old_timezone = getattr(_active, "value", None)
|
||||
self.old_timezone = getattr(_active, 'value', None)
|
||||
if self.timezone is None:
|
||||
deactivate()
|
||||
else:
|
||||
@@ -165,7 +137,6 @@ class override(ContextDecorator):
|
||||
|
||||
# Templates
|
||||
|
||||
|
||||
def template_localtime(value, use_tz=None):
|
||||
"""
|
||||
Check if value is a datetime and converts it to local time if necessary.
|
||||
@@ -176,17 +147,16 @@ def template_localtime(value, use_tz=None):
|
||||
This function is designed for use by the template engine.
|
||||
"""
|
||||
should_convert = (
|
||||
isinstance(value, datetime)
|
||||
and (settings.USE_TZ if use_tz is None else use_tz)
|
||||
and not is_naive(value)
|
||||
and getattr(value, "convert_to_local_time", True)
|
||||
isinstance(value, datetime) and
|
||||
(settings.USE_TZ if use_tz is None else use_tz) and
|
||||
not is_naive(value) and
|
||||
getattr(value, 'convert_to_local_time', True)
|
||||
)
|
||||
return localtime(value) if should_convert else value
|
||||
|
||||
|
||||
# Utilities
|
||||
|
||||
|
||||
def localtime(value=None, timezone=None):
|
||||
"""
|
||||
Convert an aware datetime.datetime to local time.
|
||||
@@ -224,13 +194,16 @@ def now():
|
||||
"""
|
||||
Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
|
||||
"""
|
||||
return datetime.now(tz=utc if settings.USE_TZ else None)
|
||||
if settings.USE_TZ:
|
||||
# timeit shows that datetime.now(tz=utc) is 24% slower
|
||||
return datetime.utcnow().replace(tzinfo=utc)
|
||||
else:
|
||||
return datetime.now()
|
||||
|
||||
|
||||
# By design, these four functions don't perform any checks on their arguments.
|
||||
# The caller should ensure that they don't receive an invalid value like None.
|
||||
|
||||
|
||||
def is_aware(value):
|
||||
"""
|
||||
Determine if a given datetime.datetime is aware.
|
||||
@@ -257,17 +230,8 @@ def is_naive(value):
|
||||
return value.utcoffset() is None
|
||||
|
||||
|
||||
def make_aware(value, timezone=None, is_dst=NOT_PASSED):
|
||||
def make_aware(value, timezone=None, is_dst=None):
|
||||
"""Make a naive datetime.datetime in a given time zone aware."""
|
||||
if is_dst is NOT_PASSED:
|
||||
is_dst = None
|
||||
else:
|
||||
warnings.warn(
|
||||
"The is_dst argument to make_aware(), used by the Trunc() "
|
||||
"database functions and QuerySet.datetimes(), is deprecated as it "
|
||||
"has no effect with zoneinfo time zones.",
|
||||
RemovedInDjango50Warning,
|
||||
)
|
||||
if timezone is None:
|
||||
timezone = get_current_timezone()
|
||||
if _is_pytz_zone(timezone):
|
||||
@@ -276,7 +240,8 @@ def make_aware(value, timezone=None, is_dst=NOT_PASSED):
|
||||
else:
|
||||
# Check that we won't overwrite the timezone of an aware datetime.
|
||||
if is_aware(value):
|
||||
raise ValueError("make_aware expects a naive datetime, got %s" % value)
|
||||
raise ValueError(
|
||||
"make_aware expects a naive datetime, got %s" % value)
|
||||
# This may be wrong around DST changes!
|
||||
return value.replace(tzinfo=timezone)
|
||||
|
||||
@@ -291,46 +256,13 @@ def make_naive(value, timezone=None):
|
||||
return value.astimezone(timezone).replace(tzinfo=None)
|
||||
|
||||
|
||||
_PYTZ_IMPORTED = False
|
||||
|
||||
|
||||
def _pytz_imported():
|
||||
"""
|
||||
Detects whether or not pytz has been imported without importing pytz.
|
||||
|
||||
Copied from pytz_deprecation_shim with thanks to Paul Ganssle.
|
||||
"""
|
||||
global _PYTZ_IMPORTED
|
||||
|
||||
if not _PYTZ_IMPORTED and "pytz" in sys.modules:
|
||||
_PYTZ_IMPORTED = True
|
||||
|
||||
return _PYTZ_IMPORTED
|
||||
|
||||
|
||||
def _is_pytz_zone(tz):
|
||||
"""Checks if a zone is a pytz zone."""
|
||||
# See if pytz was already imported rather than checking
|
||||
# settings.USE_DEPRECATED_PYTZ to *allow* manually passing a pytz timezone,
|
||||
# which some of the test cases (at least) rely on.
|
||||
if not _pytz_imported():
|
||||
return False
|
||||
|
||||
# If tz could be pytz, then pytz is needed here.
|
||||
import pytz
|
||||
|
||||
_PYTZ_BASE_CLASSES = (pytz.tzinfo.BaseTzInfo, pytz._FixedOffset)
|
||||
# In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo
|
||||
if not isinstance(pytz.UTC, pytz._FixedOffset):
|
||||
_PYTZ_BASE_CLASSES = _PYTZ_BASE_CLASSES + (type(pytz.UTC),)
|
||||
|
||||
return isinstance(tz, _PYTZ_BASE_CLASSES)
|
||||
|
||||
|
||||
def _datetime_ambiguous_or_imaginary(dt, tz):
|
||||
if _is_pytz_zone(tz):
|
||||
import pytz
|
||||
|
||||
try:
|
||||
tz.utcoffset(dt)
|
||||
except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):
|
||||
|
||||
@@ -17,20 +17,14 @@ def topological_sort_as_sets(dependency_graph):
|
||||
current = {node for node, deps in todo.items() if not deps}
|
||||
|
||||
if not current:
|
||||
raise CyclicDependencyError(
|
||||
"Cyclic dependency in graph: {}".format(
|
||||
", ".join(repr(x) for x in todo.items())
|
||||
)
|
||||
)
|
||||
raise CyclicDependencyError('Cyclic dependency in graph: {}'.format(
|
||||
', '.join(repr(x) for x in todo.items())))
|
||||
|
||||
yield current
|
||||
|
||||
# remove current from todo's nodes & dependencies
|
||||
todo = {
|
||||
node: (dependencies - current)
|
||||
for node, dependencies in todo.items()
|
||||
if node not in current
|
||||
}
|
||||
todo = {node: (dependencies - current) for node, dependencies in
|
||||
todo.items() if node not in current}
|
||||
|
||||
|
||||
def stable_topological_sort(nodes, dependency_graph):
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
"""
|
||||
Internationalization support.
|
||||
"""
|
||||
import warnings
|
||||
from contextlib import ContextDecorator
|
||||
from decimal import ROUND_UP, Decimal
|
||||
|
||||
from django.utils.autoreload import autoreload_started, file_changed
|
||||
from django.utils.deprecation import RemovedInDjango40Warning
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
__all__ = [
|
||||
"activate",
|
||||
"deactivate",
|
||||
"override",
|
||||
"deactivate_all",
|
||||
"get_language",
|
||||
"get_language_from_request",
|
||||
"get_language_info",
|
||||
"get_language_bidi",
|
||||
"check_for_language",
|
||||
"to_language",
|
||||
"to_locale",
|
||||
"templatize",
|
||||
"gettext",
|
||||
"gettext_lazy",
|
||||
"gettext_noop",
|
||||
"ngettext",
|
||||
"ngettext_lazy",
|
||||
"pgettext",
|
||||
"pgettext_lazy",
|
||||
"npgettext",
|
||||
"npgettext_lazy",
|
||||
'activate', 'deactivate', 'override', 'deactivate_all',
|
||||
'get_language', 'get_language_from_request',
|
||||
'get_language_info', 'get_language_bidi',
|
||||
'check_for_language', 'to_language', 'to_locale', 'templatize',
|
||||
'gettext', 'gettext_lazy', 'gettext_noop',
|
||||
'ugettext', 'ugettext_lazy', 'ugettext_noop',
|
||||
'ngettext', 'ngettext_lazy',
|
||||
'ungettext', 'ungettext_lazy',
|
||||
'pgettext', 'pgettext_lazy',
|
||||
'npgettext', 'npgettext_lazy',
|
||||
'LANGUAGE_SESSION_KEY',
|
||||
]
|
||||
|
||||
LANGUAGE_SESSION_KEY = '_language'
|
||||
|
||||
|
||||
class TranslatorCommentWarning(SyntaxWarning):
|
||||
pass
|
||||
@@ -45,7 +39,6 @@ class TranslatorCommentWarning(SyntaxWarning):
|
||||
# replace the functions with their real counterparts (once we do access the
|
||||
# settings).
|
||||
|
||||
|
||||
class Trans:
|
||||
"""
|
||||
The purpose of this class is to store the actual translation function upon
|
||||
@@ -61,20 +54,13 @@ class Trans:
|
||||
|
||||
def __getattr__(self, real_name):
|
||||
from django.conf import settings
|
||||
|
||||
if settings.USE_I18N:
|
||||
from django.utils.translation import trans_real as trans
|
||||
from django.utils.translation.reloader import (
|
||||
translation_file_changed,
|
||||
watch_for_translation_changes,
|
||||
)
|
||||
|
||||
autoreload_started.connect(
|
||||
watch_for_translation_changes, dispatch_uid="translation_file_changed"
|
||||
)
|
||||
file_changed.connect(
|
||||
translation_file_changed, dispatch_uid="translation_file_changed"
|
||||
translation_file_changed, watch_for_translation_changes,
|
||||
)
|
||||
autoreload_started.connect(watch_for_translation_changes, dispatch_uid='translation_file_changed')
|
||||
file_changed.connect(translation_file_changed, dispatch_uid='translation_file_changed')
|
||||
else:
|
||||
from django.utils.translation import trans_null as trans
|
||||
setattr(self, real_name, getattr(trans, real_name))
|
||||
@@ -91,14 +77,53 @@ def gettext_noop(message):
|
||||
return _trans.gettext_noop(message)
|
||||
|
||||
|
||||
def ugettext_noop(message):
|
||||
"""
|
||||
A legacy compatibility wrapper for Unicode handling on Python 2.
|
||||
Alias of gettext_noop() since Django 2.0.
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.translation.ugettext_noop() is deprecated in favor of '
|
||||
'django.utils.translation.gettext_noop().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return gettext_noop(message)
|
||||
|
||||
|
||||
def gettext(message):
|
||||
return _trans.gettext(message)
|
||||
|
||||
|
||||
def ugettext(message):
|
||||
"""
|
||||
A legacy compatibility wrapper for Unicode handling on Python 2.
|
||||
Alias of gettext() since Django 2.0.
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.translation.ugettext() is deprecated in favor of '
|
||||
'django.utils.translation.gettext().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return gettext(message)
|
||||
|
||||
|
||||
def ngettext(singular, plural, number):
|
||||
return _trans.ngettext(singular, plural, number)
|
||||
|
||||
|
||||
def ungettext(singular, plural, number):
|
||||
"""
|
||||
A legacy compatibility wrapper for Unicode handling on Python 2.
|
||||
Alias of ngettext() since Django 2.0.
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.translation.ungettext() is deprecated in favor of '
|
||||
'django.utils.translation.ngettext().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return ngettext(singular, plural, number)
|
||||
|
||||
|
||||
def pgettext(context, message):
|
||||
return _trans.pgettext(context, message)
|
||||
|
||||
@@ -111,35 +136,46 @@ gettext_lazy = lazy(gettext, str)
|
||||
pgettext_lazy = lazy(pgettext, str)
|
||||
|
||||
|
||||
def ugettext_lazy(message):
|
||||
"""
|
||||
A legacy compatibility wrapper for Unicode handling on Python 2. Has been
|
||||
Alias of gettext_lazy since Django 2.0.
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.translation.ugettext_lazy() is deprecated in favor of '
|
||||
'django.utils.translation.gettext_lazy().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return gettext_lazy(message)
|
||||
|
||||
|
||||
def lazy_number(func, resultclass, number=None, **kwargs):
|
||||
if isinstance(number, int):
|
||||
kwargs["number"] = number
|
||||
kwargs['number'] = number
|
||||
proxy = lazy(func, resultclass)(**kwargs)
|
||||
else:
|
||||
original_kwargs = kwargs.copy()
|
||||
|
||||
class NumberAwareString(resultclass):
|
||||
def __bool__(self):
|
||||
return bool(kwargs["singular"])
|
||||
return bool(kwargs['singular'])
|
||||
|
||||
def _get_number_value(self, values):
|
||||
try:
|
||||
return values[number]
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Your dictionary lacks key '%s'. Please provide "
|
||||
"Your dictionary lacks key '%s\'. Please provide "
|
||||
"it, because it is required to determine whether "
|
||||
"string is singular or plural." % number
|
||||
)
|
||||
|
||||
def _translate(self, number_value):
|
||||
kwargs["number"] = number_value
|
||||
kwargs['number'] = number_value
|
||||
return func(**kwargs)
|
||||
|
||||
def format(self, *args, **kwargs):
|
||||
number_value = (
|
||||
self._get_number_value(kwargs) if kwargs and number else args[0]
|
||||
)
|
||||
number_value = self._get_number_value(kwargs) if kwargs and number else args[0]
|
||||
return self._translate(number_value).format(*args, **kwargs)
|
||||
|
||||
def __mod__(self, rhs):
|
||||
@@ -156,10 +192,7 @@ def lazy_number(func, resultclass, number=None, **kwargs):
|
||||
return translated
|
||||
|
||||
proxy = lazy(lambda **kwargs: NumberAwareString(), NumberAwareString)(**kwargs)
|
||||
proxy.__reduce__ = lambda: (
|
||||
_lazy_number_unpickle,
|
||||
(func, resultclass, number, original_kwargs),
|
||||
)
|
||||
proxy.__reduce__ = lambda: (_lazy_number_unpickle, (func, resultclass, number, original_kwargs))
|
||||
return proxy
|
||||
|
||||
|
||||
@@ -171,10 +204,21 @@ def ngettext_lazy(singular, plural, number=None):
|
||||
return lazy_number(ngettext, str, singular=singular, plural=plural, number=number)
|
||||
|
||||
|
||||
def npgettext_lazy(context, singular, plural, number=None):
|
||||
return lazy_number(
|
||||
npgettext, str, context=context, singular=singular, plural=plural, number=number
|
||||
def ungettext_lazy(singular, plural, number=None):
|
||||
"""
|
||||
A legacy compatibility wrapper for Unicode handling on Python 2.
|
||||
An alias of ungettext_lazy() since Django 2.0.
|
||||
"""
|
||||
warnings.warn(
|
||||
'django.utils.translation.ungettext_lazy() is deprecated in favor of '
|
||||
'django.utils.translation.ngettext_lazy().',
|
||||
RemovedInDjango40Warning, stacklevel=2,
|
||||
)
|
||||
return ngettext_lazy(singular, plural, number)
|
||||
|
||||
|
||||
def npgettext_lazy(context, singular, plural, number=None):
|
||||
return lazy_number(npgettext, str, context=context, singular=singular, plural=plural, number=number)
|
||||
|
||||
|
||||
def activate(language):
|
||||
@@ -220,27 +264,27 @@ def check_for_language(lang_code):
|
||||
|
||||
def to_language(locale):
|
||||
"""Turn a locale name (en_US) into a language name (en-us)."""
|
||||
p = locale.find("_")
|
||||
p = locale.find('_')
|
||||
if p >= 0:
|
||||
return locale[:p].lower() + "-" + locale[p + 1 :].lower()
|
||||
return locale[:p].lower() + '-' + locale[p + 1:].lower()
|
||||
else:
|
||||
return locale.lower()
|
||||
|
||||
|
||||
def to_locale(language):
|
||||
"""Turn a language name (en-us) into a locale name (en_US)."""
|
||||
lang, _, country = language.lower().partition("-")
|
||||
language, _, country = language.lower().partition('-')
|
||||
if not country:
|
||||
return language[:3].lower() + language[3:]
|
||||
return language
|
||||
# A language with > 2 characters after the dash only has its first
|
||||
# character after the dash capitalized; e.g. sr-latn becomes sr_Latn.
|
||||
# A language with 2 characters after the dash has both characters
|
||||
# capitalized; e.g. en-us becomes en_US.
|
||||
country, _, tail = country.partition("-")
|
||||
country, _, tail = country.partition('-')
|
||||
country = country.title() if len(country) > 2 else country.upper()
|
||||
if tail:
|
||||
country += "-" + tail
|
||||
return lang + "_" + country
|
||||
country += '-' + tail
|
||||
return language + '_' + country
|
||||
|
||||
|
||||
def get_language_from_request(request, check_path=False):
|
||||
@@ -257,7 +301,6 @@ def get_supported_language_variant(lang_code, *, strict=False):
|
||||
|
||||
def templatize(src, **kwargs):
|
||||
from .template import templatize
|
||||
|
||||
return templatize(src, **kwargs)
|
||||
|
||||
|
||||
@@ -267,35 +310,32 @@ def deactivate_all():
|
||||
|
||||
def get_language_info(lang_code):
|
||||
from django.conf.locale import LANG_INFO
|
||||
|
||||
try:
|
||||
lang_info = LANG_INFO[lang_code]
|
||||
if "fallback" in lang_info and "name" not in lang_info:
|
||||
info = get_language_info(lang_info["fallback"][0])
|
||||
if 'fallback' in lang_info and 'name' not in lang_info:
|
||||
info = get_language_info(lang_info['fallback'][0])
|
||||
else:
|
||||
info = lang_info
|
||||
except KeyError:
|
||||
if "-" not in lang_code:
|
||||
if '-' not in lang_code:
|
||||
raise KeyError("Unknown language code %s." % lang_code)
|
||||
generic_lang_code = lang_code.split("-")[0]
|
||||
generic_lang_code = lang_code.split('-')[0]
|
||||
try:
|
||||
info = LANG_INFO[generic_lang_code]
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Unknown language code %s and %s." % (lang_code, generic_lang_code)
|
||||
)
|
||||
raise KeyError("Unknown language code %s and %s." % (lang_code, generic_lang_code))
|
||||
|
||||
if info:
|
||||
info["name_translated"] = gettext_lazy(info["name"])
|
||||
info['name_translated'] = gettext_lazy(info['name'])
|
||||
return info
|
||||
|
||||
|
||||
trim_whitespace_re = _lazy_re_compile(r"\s*\n\s*")
|
||||
trim_whitespace_re = _lazy_re_compile(r'\s*\n\s*')
|
||||
|
||||
|
||||
def trim_whitespace(s):
|
||||
return trim_whitespace_re.sub(" ", s.strip())
|
||||
return trim_whitespace_re.sub(' ', s.strip())
|
||||
|
||||
|
||||
def round_away_from_one(value):
|
||||
return int(Decimal(value - 1).quantize(Decimal("0"), rounding=ROUND_UP)) + 1
|
||||
return int(Decimal(value - 1).quantize(Decimal('0'), rounding=ROUND_UP)) + 1
|
||||
|
||||
@@ -11,24 +11,23 @@ def watch_for_translation_changes(sender, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
if settings.USE_I18N:
|
||||
directories = [Path("locale")]
|
||||
directories = [Path('locale')]
|
||||
directories.extend(
|
||||
Path(config.path) / "locale"
|
||||
Path(config.path) / 'locale'
|
||||
for config in apps.get_app_configs()
|
||||
if not is_django_module(config.module)
|
||||
)
|
||||
directories.extend(Path(p) for p in settings.LOCALE_PATHS)
|
||||
for path in directories:
|
||||
sender.watch_dir(path, "**/*.mo")
|
||||
sender.watch_dir(path, '**/*.mo')
|
||||
|
||||
|
||||
def translation_file_changed(sender, file_path, **kwargs):
|
||||
"""Clear the internal translations cache if a .mo file is modified."""
|
||||
if file_path.suffix == ".mo":
|
||||
if file_path.suffix == '.mo':
|
||||
import gettext
|
||||
|
||||
from django.utils.translation import trans_real
|
||||
|
||||
gettext._translations = {}
|
||||
trans_real._translations = {}
|
||||
trans_real._default = None
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import warnings
|
||||
from io import StringIO
|
||||
|
||||
from django.template.base import Lexer, TokenType
|
||||
from django.template.base import TRANSLATOR_COMMENT_MARK, Lexer, TokenType
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
from . import TranslatorCommentWarning, trim_whitespace
|
||||
|
||||
TRANSLATOR_COMMENT_MARK = "Translators"
|
||||
|
||||
dot_re = _lazy_re_compile(r"\S")
|
||||
dot_re = _lazy_re_compile(r'\S')
|
||||
|
||||
|
||||
def blankout(src, char):
|
||||
@@ -28,9 +26,7 @@ inline_re = _lazy_re_compile(
|
||||
# Match the optional context part
|
||||
r"""(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?\s*"""
|
||||
)
|
||||
block_re = _lazy_re_compile(
|
||||
r"""^\s*blocktrans(?:late)?(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)"""
|
||||
)
|
||||
block_re = _lazy_re_compile(r"""^\s*blocktrans(?:late)?(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)""")
|
||||
endblock_re = _lazy_re_compile(r"""^\s*endblocktrans(?:late)?$""")
|
||||
plural_re = _lazy_re_compile(r"""^\s*plural$""")
|
||||
constant_re = _lazy_re_compile(r"""_\(((?:".*?")|(?:'.*?'))\)""")
|
||||
@@ -42,7 +38,7 @@ def templatize(src, origin=None):
|
||||
does so by translating the Django translation tags into standard gettext
|
||||
function invocations.
|
||||
"""
|
||||
out = StringIO("")
|
||||
out = StringIO('')
|
||||
message_context = None
|
||||
intrans = False
|
||||
inplural = False
|
||||
@@ -54,30 +50,27 @@ def templatize(src, origin=None):
|
||||
lineno_comment_map = {}
|
||||
comment_lineno_cache = None
|
||||
# Adding the u prefix allows gettext to recognize the string (#26093).
|
||||
raw_prefix = "u"
|
||||
raw_prefix = 'u'
|
||||
|
||||
def join_tokens(tokens, trim=False):
|
||||
message = "".join(tokens)
|
||||
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)
|
||||
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)
|
||||
if translators_comment_start is not None and lineno >= translators_comment_start:
|
||||
out.write(' # %s' % line)
|
||||
else:
|
||||
out.write(" #\n")
|
||||
out.write(' #\n')
|
||||
incomment = False
|
||||
comment = []
|
||||
else:
|
||||
@@ -89,44 +82,36 @@ def templatize(src, origin=None):
|
||||
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,
|
||||
)
|
||||
)
|
||||
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,
|
||||
)
|
||||
)
|
||||
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"))
|
||||
out.write(blankout(part, 'S'))
|
||||
for part in plural:
|
||||
out.write(blankout(part, "P"))
|
||||
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,
|
||||
)
|
||||
)
|
||||
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,
|
||||
)
|
||||
)
|
||||
out.write(' gettext({p}{!r}) '.format(
|
||||
join_tokens(singular, trimmed),
|
||||
p=raw_prefix,
|
||||
))
|
||||
for part in singular:
|
||||
out.write(blankout(part, "S"))
|
||||
out.write(blankout(part, 'S'))
|
||||
message_context = None
|
||||
intrans = False
|
||||
inplural = False
|
||||
@@ -135,20 +120,20 @@ def templatize(src, origin=None):
|
||||
elif pluralmatch:
|
||||
inplural = True
|
||||
else:
|
||||
filemsg = ""
|
||||
filemsg = ''
|
||||
if origin:
|
||||
filemsg = "file %s, " % 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)
|
||||
plural.append('%%(%s)s' % t.contents)
|
||||
else:
|
||||
singular.append("%%(%s)s" % t.contents)
|
||||
singular.append('%%(%s)s' % t.contents)
|
||||
elif t.token_type == TokenType.TEXT:
|
||||
contents = t.contents.replace("%", "%%")
|
||||
contents = t.contents.replace('%', '%%')
|
||||
if inplural:
|
||||
plural.append(contents)
|
||||
else:
|
||||
@@ -157,13 +142,13 @@ def templatize(src, origin=None):
|
||||
# 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")
|
||||
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 = ""
|
||||
filemsg = ''
|
||||
if origin:
|
||||
filemsg = "file %s, " % origin
|
||||
filemsg = 'file %s, ' % origin
|
||||
warn_msg = (
|
||||
"The translator-targeted comment '%s' "
|
||||
"(%sline %d) was ignored, because it wasn't "
|
||||
@@ -172,9 +157,7 @@ def templatize(src, origin=None):
|
||||
warnings.warn(warn_msg, TranslatorCommentWarning)
|
||||
lineno_comment_map[comment_lineno_cache] = []
|
||||
else:
|
||||
out.write(
|
||||
"# %s" % " | ".join(lineno_comment_map[comment_lineno_cache])
|
||||
)
|
||||
out.write('# %s' % ' | '.join(lineno_comment_map[comment_lineno_cache]))
|
||||
comment_lineno_cache = None
|
||||
|
||||
if t.token_type == TokenType.BLOCK:
|
||||
@@ -187,7 +170,7 @@ def templatize(src, origin=None):
|
||||
g = g.strip('"')
|
||||
elif g[0] == "'":
|
||||
g = g.strip("'")
|
||||
g = g.replace("%", "%%")
|
||||
g = g.replace('%', '%%')
|
||||
if imatch[2]:
|
||||
# A context is provided
|
||||
context_match = context_re.match(imatch[2])
|
||||
@@ -196,17 +179,15 @@ def templatize(src, origin=None):
|
||||
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
|
||||
)
|
||||
)
|
||||
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))
|
||||
out.write(' gettext({p}{!r}) '.format(g, p=raw_prefix))
|
||||
elif bmatch:
|
||||
for fmatch in constant_re.findall(t.contents):
|
||||
out.write(" _(%s) " % fmatch)
|
||||
out.write(' _(%s) ' % fmatch)
|
||||
if bmatch[1]:
|
||||
# A context is provided
|
||||
context_match = context_re.match(bmatch[1])
|
||||
@@ -217,30 +198,30 @@ def templatize(src, origin=None):
|
||||
message_context = message_context.strip("'")
|
||||
intrans = True
|
||||
inplural = False
|
||||
trimmed = "trimmed" in t.split_contents()
|
||||
trimmed = 'trimmed' in t.split_contents()
|
||||
singular = []
|
||||
plural = []
|
||||
elif cmatches:
|
||||
for cmatch in cmatches:
|
||||
out.write(" _(%s) " % cmatch)
|
||||
elif t.contents == "comment":
|
||||
out.write(' _(%s) ' % cmatch)
|
||||
elif t.contents == 'comment':
|
||||
incomment = True
|
||||
else:
|
||||
out.write(blankout(t.contents, "B"))
|
||||
out.write(blankout(t.contents, 'B'))
|
||||
elif t.token_type == TokenType.VAR:
|
||||
parts = t.contents.split("|")
|
||||
parts = t.contents.split('|')
|
||||
cmatch = constant_re.match(parts[0])
|
||||
if cmatch:
|
||||
out.write(" _(%s) " % cmatch[1])
|
||||
out.write(' _(%s) ' % cmatch[1])
|
||||
for p in parts[1:]:
|
||||
if p.find(":_(") >= 0:
|
||||
out.write(" %s " % p.split(":", 1)[1])
|
||||
if p.find(':_(') >= 0:
|
||||
out.write(' %s ' % p.split(':', 1)[1])
|
||||
else:
|
||||
out.write(blankout(p, "F"))
|
||||
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"))
|
||||
out.write(blankout(t.contents, 'X'))
|
||||
return out.getvalue()
|
||||
|
||||
@@ -32,23 +32,18 @@ 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 = _lazy_re_compile(
|
||||
r"""
|
||||
# "en", "en-au", "x-y-z", "es-419", "*"
|
||||
([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*)
|
||||
# Optional "q=1.00", "q=0.8"
|
||||
(?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))?
|
||||
# Multiple accepts per header.
|
||||
(?:\s*,\s*|$)
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
accept_language_re = _lazy_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 = _lazy_re_compile(
|
||||
r"^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$", re.IGNORECASE
|
||||
r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+)?)(/|$)")
|
||||
language_code_prefix_re = _lazy_re_compile(r'^/(\w+([@-]\w+)?)(/|$)')
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
@@ -57,7 +52,7 @@ 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"):
|
||||
if kwargs['setting'] in ('LANGUAGES', 'LANGUAGE_CODE'):
|
||||
check_for_language.cache_clear()
|
||||
get_languages.cache_clear()
|
||||
get_supported_language_variant.cache_clear()
|
||||
@@ -68,7 +63,6 @@ class TranslationCatalog:
|
||||
Simulate a dict for DjangoTranslation._catalog so as multiple catalogs
|
||||
with different plural equations are kept separate.
|
||||
"""
|
||||
|
||||
def __init__(self, trans=None):
|
||||
self._catalogs = [trans._catalog.copy()] if trans else [{}]
|
||||
self._plurals = [trans.plural] if trans else [lambda n: int(n != 1)]
|
||||
@@ -130,8 +124,7 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
requested language and add a fallback to the default language, if it's
|
||||
different from the requested language.
|
||||
"""
|
||||
|
||||
domain = "django"
|
||||
domain = 'django'
|
||||
|
||||
def __init__(self, language, domain=None, localedirs=None):
|
||||
"""Create a GNUTranslations() using many locale directories"""
|
||||
@@ -147,12 +140,10 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
# pluralization: anything except one is pluralized.
|
||||
self.plural = lambda n: int(n != 1)
|
||||
|
||||
if self.domain == "django":
|
||||
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
|
||||
)
|
||||
warnings.warn("localedirs is ignored when domain is 'django'.", RuntimeWarning)
|
||||
localedirs = None
|
||||
self._init_translation_catalog()
|
||||
|
||||
@@ -164,16 +155,9 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
self._add_installed_apps_translations()
|
||||
|
||||
self._add_local_translations()
|
||||
if (
|
||||
self.__language == settings.LANGUAGE_CODE
|
||||
and self.domain == "django"
|
||||
and self._catalog is None
|
||||
):
|
||||
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 OSError(
|
||||
"No translation files found for default language %s."
|
||||
% settings.LANGUAGE_CODE
|
||||
)
|
||||
raise OSError('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.
|
||||
@@ -200,7 +184,7 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
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")
|
||||
localedir = os.path.join(os.path.dirname(settingsfile), 'locale')
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
@@ -212,10 +196,9 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
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."
|
||||
)
|
||||
"gettext calls at import time.")
|
||||
for app_config in app_configs:
|
||||
localedir = os.path.join(app_config.path, "locale")
|
||||
localedir = os.path.join(app_config.path, 'locale')
|
||||
if os.path.exists(localedir):
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
@@ -230,11 +213,9 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
"""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"
|
||||
):
|
||||
if self.__language == settings.LANGUAGE_CODE or self.__language.startswith('en'):
|
||||
return
|
||||
if self.domain == "django":
|
||||
if self.domain == 'django':
|
||||
# Get from cache
|
||||
default_translation = translation(settings.LANGUAGE_CODE)
|
||||
else:
|
||||
@@ -245,7 +226,7 @@ class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
|
||||
def merge(self, other):
|
||||
"""Merge another translation into this catalog."""
|
||||
if not getattr(other, "_catalog", None):
|
||||
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).
|
||||
@@ -340,7 +321,7 @@ def get_language_bidi():
|
||||
if lang is None:
|
||||
return False
|
||||
else:
|
||||
base_lang = get_language().split("-")[0]
|
||||
base_lang = get_language().split('-')[0]
|
||||
return base_lang in settings.LANGUAGES_BIDI
|
||||
|
||||
|
||||
@@ -368,7 +349,7 @@ def gettext(message):
|
||||
"""
|
||||
global _default
|
||||
|
||||
eol_message = message.replace("\r\n", "\n").replace("\r", "\n")
|
||||
eol_message = message.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
if eol_message:
|
||||
_default = _default or translation(settings.LANGUAGE_CODE)
|
||||
@@ -378,7 +359,7 @@ def gettext(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)("")
|
||||
result = type(message)('')
|
||||
|
||||
if isinstance(message, SafeData):
|
||||
return mark_safe(result)
|
||||
@@ -423,15 +404,13 @@ 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")
|
||||
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,
|
||||
)
|
||||
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
|
||||
@@ -444,11 +423,10 @@ 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"
|
||||
)
|
||||
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")
|
||||
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]
|
||||
@@ -469,7 +447,7 @@ def check_for_language(lang_code):
|
||||
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
|
||||
gettext_module.find('django', path, [to_locale(lang_code)]) is not None
|
||||
for path in all_locale_paths()
|
||||
)
|
||||
|
||||
@@ -496,17 +474,14 @@ def get_supported_language_variant(lang_code, strict=False):
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
if lang_code:
|
||||
# If 'zh-hant-tw' is not supported, try special fallback or subsequent
|
||||
# language codes i.e. 'zh-hant' and 'zh'.
|
||||
# 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"])
|
||||
possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
|
||||
except KeyError:
|
||||
pass
|
||||
i = None
|
||||
while (i := lang_code.rfind("-", 0, i)) > -1:
|
||||
possible_lang_codes.append(lang_code[:i])
|
||||
generic_lang_code = possible_lang_codes[-1]
|
||||
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:
|
||||
@@ -515,7 +490,7 @@ def get_supported_language_variant(lang_code, strict=False):
|
||||
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 + "-"):
|
||||
if supported_code.startswith(generic_lang_code + '-'):
|
||||
return supported_code
|
||||
raise LookupError(lang_code)
|
||||
|
||||
@@ -553,11 +528,7 @@ def get_language_from_request(request, check_path=False):
|
||||
return lang_code
|
||||
|
||||
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
||||
if (
|
||||
lang_code is not None
|
||||
and lang_code in get_languages()
|
||||
and check_for_language(lang_code)
|
||||
):
|
||||
if lang_code is not None and lang_code in get_languages() and check_for_language(lang_code):
|
||||
return lang_code
|
||||
|
||||
try:
|
||||
@@ -565,9 +536,9 @@ def get_language_from_request(request, check_path=False):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
|
||||
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
|
||||
for accept_lang, unused in parse_accept_lang_header(accept):
|
||||
if accept_lang == "*":
|
||||
if accept_lang == '*':
|
||||
break
|
||||
|
||||
if not language_code_re.search(accept_lang):
|
||||
@@ -597,7 +568,7 @@ def parse_accept_lang_header(lang_string):
|
||||
if pieces[-1]:
|
||||
return ()
|
||||
for i in range(0, len(pieces) - 1, 3):
|
||||
first, lang, priority = pieces[i : i + 3]
|
||||
first, lang, priority = pieces[i:i + 3]
|
||||
if first:
|
||||
return ()
|
||||
if priority:
|
||||
|
||||
@@ -14,10 +14,9 @@ class Node:
|
||||
connection (the root) with the children being either leaf nodes or other
|
||||
Node instances.
|
||||
"""
|
||||
|
||||
# Standard connector type. Clients usually won't use this at all and
|
||||
# subclasses will usually override the value.
|
||||
default = "DEFAULT"
|
||||
default = 'DEFAULT'
|
||||
|
||||
def __init__(self, children=None, connector=None, negated=False):
|
||||
"""Construct a new Node. If no connector is given, use the default."""
|
||||
@@ -42,8 +41,8 @@ class Node:
|
||||
return obj
|
||||
|
||||
def __str__(self):
|
||||
template = "(NOT (%s: %s))" if self.negated else "(%s: %s)"
|
||||
return template % (self.connector, ", ".join(str(c) for c in self.children))
|
||||
template = '(NOT (%s: %s))' if self.negated else '(%s: %s)'
|
||||
return template % (self.connector, ', '.join(str(c) for c in self.children))
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: %s>" % (self.__class__.__name__, self)
|
||||
@@ -68,23 +67,15 @@ class Node:
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.__class__ == other.__class__
|
||||
and self.connector == other.connector
|
||||
and self.negated == other.negated
|
||||
and self.children == other.children
|
||||
self.__class__ == other.__class__ and
|
||||
(self.connector, self.negated) == (other.connector, other.negated) and
|
||||
self.children == other.children
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(
|
||||
(
|
||||
self.__class__,
|
||||
self.connector,
|
||||
self.negated,
|
||||
*make_hashable(self.children),
|
||||
)
|
||||
)
|
||||
return hash((self.__class__, self.connector, self.negated, *make_hashable(self.children)))
|
||||
|
||||
def add(self, data, conn_type):
|
||||
def add(self, data, conn_type, squash=True):
|
||||
"""
|
||||
Combine this tree and the data represented by data using the
|
||||
connector conn_type. The combine is done by squashing the node other
|
||||
@@ -95,29 +86,38 @@ class Node:
|
||||
|
||||
Return a node which can be used in place of data regardless if the
|
||||
node other got squashed or not.
|
||||
|
||||
If `squash` is False the data is prepared and added as a child to
|
||||
this tree without further logic.
|
||||
"""
|
||||
if self.connector != conn_type:
|
||||
obj = self._new_instance(self.children, self.connector, self.negated)
|
||||
if self.connector == conn_type and data in self.children:
|
||||
return data
|
||||
if not squash:
|
||||
self.children.append(data)
|
||||
return data
|
||||
if self.connector == conn_type:
|
||||
# We can reuse self.children to append or squash the node other.
|
||||
if (isinstance(data, Node) and not data.negated and
|
||||
(data.connector == conn_type or len(data) == 1)):
|
||||
# We can squash the other node's children directly into this
|
||||
# node. We are just doing (AB)(CD) == (ABCD) here, with the
|
||||
# addition that if the length of the other node is 1 the
|
||||
# connector doesn't matter. However, for the len(self) == 1
|
||||
# case we don't want to do the squashing, as it would alter
|
||||
# self.connector.
|
||||
self.children.extend(data.children)
|
||||
return self
|
||||
else:
|
||||
# We could use perhaps additional logic here to see if some
|
||||
# children could be used for pushdown here.
|
||||
self.children.append(data)
|
||||
return data
|
||||
else:
|
||||
obj = self._new_instance(self.children, self.connector,
|
||||
self.negated)
|
||||
self.connector = conn_type
|
||||
self.children = [obj, data]
|
||||
return data
|
||||
elif (
|
||||
isinstance(data, Node)
|
||||
and not data.negated
|
||||
and (data.connector == conn_type or len(data) == 1)
|
||||
):
|
||||
# We can squash the other node's children directly into this node.
|
||||
# We are just doing (AB)(CD) == (ABCD) here, with the addition that
|
||||
# if the length of the other node is 1 the connector doesn't
|
||||
# matter. However, for the len(self) == 1 case we don't want to do
|
||||
# the squashing, as it would alter self.connector.
|
||||
self.children.extend(data.children)
|
||||
return self
|
||||
else:
|
||||
# We could use perhaps additional logic here to see if some
|
||||
# children could be used for pushdown here.
|
||||
self.children.append(data)
|
||||
return data
|
||||
|
||||
def negate(self):
|
||||
"""Negate the sense of the root connector."""
|
||||
|
||||
@@ -3,8 +3,7 @@ import functools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
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
|
||||
@@ -14,7 +13,6 @@ PY36 = sys.version_info >= (3, 6)
|
||||
PY37 = sys.version_info >= (3, 7)
|
||||
PY38 = sys.version_info >= (3, 8)
|
||||
PY39 = sys.version_info >= (3, 9)
|
||||
PY310 = sys.version_info >= (3, 10)
|
||||
|
||||
|
||||
def get_version(version=None):
|
||||
@@ -28,14 +26,14 @@ def get_version(version=None):
|
||||
|
||||
main = get_main_version(version)
|
||||
|
||||
sub = ""
|
||||
if version[3] == "alpha" and version[4] == 0:
|
||||
sub = ''
|
||||
if version[3] == 'alpha' and version[4] == 0:
|
||||
git_changeset = get_git_changeset()
|
||||
if git_changeset:
|
||||
sub = ".dev%s" % git_changeset
|
||||
sub = '.dev%s' % git_changeset
|
||||
|
||||
elif version[3] != "final":
|
||||
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
|
||||
elif version[3] != 'final':
|
||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
|
||||
sub = mapping[version[3]] + str(version[4])
|
||||
|
||||
return main + sub
|
||||
@@ -45,7 +43,7 @@ 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])
|
||||
return '.'.join(str(x) for x in version[:parts])
|
||||
|
||||
|
||||
def get_complete_version(version=None):
|
||||
@@ -57,17 +55,17 @@ def get_complete_version(version=None):
|
||||
from django import VERSION as version
|
||||
else:
|
||||
assert len(version) == 5
|
||||
assert version[3] in ("alpha", "beta", "rc", "final")
|
||||
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"
|
||||
if version[3] != 'final':
|
||||
return 'dev'
|
||||
else:
|
||||
return "%d.%d" % version[:2]
|
||||
return '%d.%d' % version[:2]
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
@@ -78,29 +76,18 @@ def get_git_changeset():
|
||||
This value isn't guaranteed to be unique, but collisions are very unlikely,
|
||||
so it's sufficient for generating the development version numbers.
|
||||
"""
|
||||
# Repository may not be found if __file__ is undefined, e.g. in a frozen
|
||||
# module.
|
||||
if "__file__" not in globals():
|
||||
return None
|
||||
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
git_log = subprocess.run(
|
||||
"git log --pretty=format:%ct --quiet -1 HEAD",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True,
|
||||
cwd=repo_dir,
|
||||
universal_newlines=True,
|
||||
['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.stdout
|
||||
tz = datetime.timezone.utc
|
||||
try:
|
||||
timestamp = datetime.datetime.fromtimestamp(int(timestamp), tz=tz)
|
||||
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
|
||||
except ValueError:
|
||||
return None
|
||||
return timestamp.strftime("%Y%m%d%H%M%S")
|
||||
|
||||
|
||||
version_component_re = _lazy_re_compile(r"(\d+|[a-z]+|\.)")
|
||||
return timestamp.strftime('%Y%m%d%H%M%S')
|
||||
|
||||
|
||||
def get_version_tuple(version):
|
||||
@@ -108,13 +95,10 @@ 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 version_component_re.split(version):
|
||||
if item and item != ".":
|
||||
try:
|
||||
component = int(item)
|
||||
except ValueError:
|
||||
break
|
||||
else:
|
||||
version_numbers.append(component)
|
||||
for item in loose_version.version:
|
||||
if not isinstance(item, int):
|
||||
break
|
||||
version_numbers.append(item)
|
||||
return tuple(version_numbers)
|
||||
|
||||
@@ -21,12 +21,10 @@ class SimplerXMLGenerator(XMLGenerator):
|
||||
self.endElement(name)
|
||||
|
||||
def characters(self, content):
|
||||
if content and re.search(r"[\x00-\x08\x0B-\x0C\x0E-\x1F]", 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"
|
||||
)
|
||||
raise UnserializableContentError("Control characters are not supported in XML 1.0")
|
||||
XMLGenerator.characters(self, content)
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
|
||||
Reference in New Issue
Block a user