일단 커밋. 오랫동안 커밋을 안해서 꼬였다.
리팩토리 중.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
62
.venv/lib/python3.12/site-packages/django/utils/_os.py
Normal file
62
.venv/lib/python3.12/site-packages/django/utils/_os.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
import tempfile
|
||||
from os.path import abspath, dirname, join, normcase, sep
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
|
||||
|
||||
def safe_join(base, *paths):
|
||||
"""
|
||||
Join one or more path components to the base path component intelligently.
|
||||
Return a normalized, absolute version of the final path.
|
||||
|
||||
Raise SuspiciousFileOperation if the final path isn't located inside of the
|
||||
base path component.
|
||||
"""
|
||||
final_path = abspath(join(base, *paths))
|
||||
base_path = abspath(base)
|
||||
# Ensure final_path starts with base_path (using normcase to ensure we
|
||||
# don't false-negative on case insensitive operating systems like Windows),
|
||||
# further, one of the following conditions must be true:
|
||||
# a) The next character is the path separator (to prevent conditions like
|
||||
# safe_join("/dir", "/../d"))
|
||||
# b) The final path must be the same as the base path.
|
||||
# c) The base path must be the most root path (meaning either "/" or "C:\\")
|
||||
if (
|
||||
not normcase(final_path).startswith(normcase(base_path + sep))
|
||||
and normcase(final_path) != normcase(base_path)
|
||||
and dirname(normcase(base_path)) != normcase(base_path)
|
||||
):
|
||||
raise SuspiciousFileOperation(
|
||||
"The joined path ({}) is located outside of the base path "
|
||||
"component ({})".format(final_path, base_path)
|
||||
)
|
||||
return final_path
|
||||
|
||||
|
||||
def symlinks_supported():
|
||||
"""
|
||||
Return whether or not creating symlinks are supported in the host platform
|
||||
and/or if they are allowed to be created (e.g. on Windows it requires admin
|
||||
permissions).
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
original_path = os.path.join(temp_dir, "original")
|
||||
symlink_path = os.path.join(temp_dir, "symlink")
|
||||
os.makedirs(original_path)
|
||||
try:
|
||||
os.symlink(original_path, symlink_path)
|
||||
supported = True
|
||||
except (OSError, NotImplementedError):
|
||||
supported = False
|
||||
return supported
|
||||
|
||||
|
||||
def to_path(value):
|
||||
"""Convert value to a pathlib.Path instance, if not already a Path."""
|
||||
if isinstance(value, Path):
|
||||
return value
|
||||
elif not isinstance(value, str):
|
||||
raise TypeError("Invalid path type: %s" % type(value).__name__)
|
||||
return Path(value)
|
||||
262
.venv/lib/python3.12/site-packages/django/utils/archive.py
Normal file
262
.venv/lib/python3.12/site-packages/django/utils/archive.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Based on "python-archive" -- https://pypi.org/project/python-archive/
|
||||
|
||||
Copyright (c) 2010 Gary Wilson Jr. <gary.wilson@gmail.com> and contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tarfile
|
||||
import zipfile
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
|
||||
class ArchiveException(Exception):
|
||||
"""
|
||||
Base exception class for all archive errors.
|
||||
"""
|
||||
|
||||
|
||||
class UnrecognizedArchiveFormat(ArchiveException):
|
||||
"""
|
||||
Error raised when passed file is not a recognized archive format.
|
||||
"""
|
||||
|
||||
|
||||
def extract(path, to_path):
|
||||
"""
|
||||
Unpack the tar or zip file at the specified path to the directory
|
||||
specified by to_path.
|
||||
"""
|
||||
with Archive(path) as archive:
|
||||
archive.extract(to_path)
|
||||
|
||||
|
||||
class Archive:
|
||||
"""
|
||||
The external API class that encapsulates an archive implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, file):
|
||||
self._archive = self._archive_cls(file)(file)
|
||||
|
||||
@staticmethod
|
||||
def _archive_cls(file):
|
||||
cls = None
|
||||
if isinstance(file, str):
|
||||
filename = file
|
||||
else:
|
||||
try:
|
||||
filename = file.name
|
||||
except AttributeError:
|
||||
raise UnrecognizedArchiveFormat(
|
||||
"File object not a recognized archive format."
|
||||
)
|
||||
base, tail_ext = os.path.splitext(filename.lower())
|
||||
cls = extension_map.get(tail_ext)
|
||||
if not cls:
|
||||
base, ext = os.path.splitext(base)
|
||||
cls = extension_map.get(ext)
|
||||
if not cls:
|
||||
raise UnrecognizedArchiveFormat(
|
||||
"Path not a recognized archive format: %s" % filename
|
||||
)
|
||||
return cls
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
def extract(self, to_path):
|
||||
self._archive.extract(to_path)
|
||||
|
||||
def list(self):
|
||||
self._archive.list()
|
||||
|
||||
def close(self):
|
||||
self._archive.close()
|
||||
|
||||
|
||||
class BaseArchive:
|
||||
"""
|
||||
Base Archive class. Implementations should inherit this class.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _copy_permissions(mode, filename):
|
||||
"""
|
||||
If the file in the archive has some permissions (this assumes a file
|
||||
won't be writable/executable without being readable), apply those
|
||||
permissions to the unarchived file.
|
||||
"""
|
||||
if mode & stat.S_IROTH:
|
||||
os.chmod(filename, mode)
|
||||
|
||||
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)
|
||||
else:
|
||||
return path, ""
|
||||
|
||||
def has_leading_dir(self, paths):
|
||||
"""
|
||||
Return True if all the paths have the same leading path name
|
||||
(i.e., everything is in one subdirectory in an archive).
|
||||
"""
|
||||
common_prefix = None
|
||||
for path in paths:
|
||||
prefix, rest = self.split_leading_dir(path)
|
||||
if not prefix:
|
||||
return False
|
||||
elif common_prefix is None:
|
||||
common_prefix = prefix
|
||||
elif prefix != common_prefix:
|
||||
return False
|
||||
return True
|
||||
|
||||
def target_filename(self, to_path, name):
|
||||
target_path = os.path.abspath(to_path)
|
||||
filename = os.path.abspath(os.path.join(target_path, name))
|
||||
try:
|
||||
if os.path.commonpath([target_path, filename]) != target_path:
|
||||
raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
|
||||
except ValueError:
|
||||
# Different drives on Windows raises ValueError.
|
||||
raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
|
||||
return filename
|
||||
|
||||
def extract(self):
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseArchive must provide an extract() method"
|
||||
)
|
||||
|
||||
def list(self):
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseArchive must provide a list() method"
|
||||
)
|
||||
|
||||
|
||||
class TarArchive(BaseArchive):
|
||||
def __init__(self, file):
|
||||
self._archive = tarfile.open(file)
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
self._archive.list(*args, **kwargs)
|
||||
|
||||
def extract(self, to_path):
|
||||
members = self._archive.getmembers()
|
||||
leading = self.has_leading_dir(x.name for x in members)
|
||||
for member in members:
|
||||
name = member.name
|
||||
if leading:
|
||||
name = self.split_leading_dir(name)[1]
|
||||
filename = self.target_filename(to_path, name)
|
||||
if member.isdir():
|
||||
if filename:
|
||||
os.makedirs(filename, exist_ok=True)
|
||||
else:
|
||||
try:
|
||||
extracted = self._archive.extractfile(member)
|
||||
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)
|
||||
)
|
||||
else:
|
||||
dirname = os.path.dirname(filename)
|
||||
if dirname:
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
with open(filename, "wb") as outfile:
|
||||
shutil.copyfileobj(extracted, outfile)
|
||||
self._copy_permissions(member.mode, filename)
|
||||
finally:
|
||||
if extracted:
|
||||
extracted.close()
|
||||
|
||||
def close(self):
|
||||
self._archive.close()
|
||||
|
||||
|
||||
class ZipArchive(BaseArchive):
|
||||
def __init__(self, file):
|
||||
self._archive = zipfile.ZipFile(file)
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
self._archive.printdir(*args, **kwargs)
|
||||
|
||||
def extract(self, to_path):
|
||||
namelist = self._archive.namelist()
|
||||
leading = self.has_leading_dir(namelist)
|
||||
for name in namelist:
|
||||
data = self._archive.read(name)
|
||||
info = self._archive.getinfo(name)
|
||||
if leading:
|
||||
name = self.split_leading_dir(name)[1]
|
||||
if not name:
|
||||
continue
|
||||
filename = self.target_filename(to_path, name)
|
||||
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:
|
||||
outfile.write(data)
|
||||
# Convert ZipInfo.external_attr to mode
|
||||
mode = info.external_attr >> 16
|
||||
self._copy_permissions(mode, filename)
|
||||
|
||||
def close(self):
|
||||
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
|
||||
39
.venv/lib/python3.12/site-packages/django/utils/asyncio.py
Normal file
39
.venv/lib/python3.12/site-packages/django/utils/asyncio.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
from asyncio import get_running_loop
|
||||
from functools import wraps
|
||||
|
||||
from django.core.exceptions import SynchronousOnlyOperation
|
||||
|
||||
|
||||
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):
|
||||
@wraps(func)
|
||||
def inner(*args, **kwargs):
|
||||
# Detect a running event loop in this thread.
|
||||
try:
|
||||
get_running_loop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
if not os.environ.get("DJANGO_ALLOW_ASYNC_UNSAFE"):
|
||||
raise SynchronousOnlyOperation(message)
|
||||
# Pass onward.
|
||||
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."
|
||||
)
|
||||
return decorator(func)
|
||||
else:
|
||||
return decorator
|
||||
675
.venv/lib/python3.12/site-packages/django/utils/autoreload.py
Normal file
675
.venv/lib/python3.12/site-packages/django/utils/autoreload.py
Normal file
@@ -0,0 +1,675 @@
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache, wraps
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from zipimport import zipimporter
|
||||
|
||||
import django
|
||||
from django.apps import apps
|
||||
from django.core.signals import request_finished
|
||||
from django.dispatch import Signal
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.version import get_version_tuple
|
||||
|
||||
autoreload_started = Signal()
|
||||
file_changed = Signal()
|
||||
|
||||
DJANGO_AUTORELOAD_ENV = "RUN_MAIN"
|
||||
|
||||
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
|
||||
# file paths to allow watching them in the future.
|
||||
_error_files = []
|
||||
_exception = None
|
||||
|
||||
try:
|
||||
import termios
|
||||
except ImportError:
|
||||
termios = None
|
||||
|
||||
|
||||
try:
|
||||
import pywatchman
|
||||
except ImportError:
|
||||
pywatchman = None
|
||||
|
||||
|
||||
def is_django_module(module):
|
||||
"""Return True if the given module is nested under Django."""
|
||||
return module.__name__.startswith("django.")
|
||||
|
||||
|
||||
def is_django_path(path):
|
||||
"""Return True if the given file path is nested under Django."""
|
||||
return Path(django.__file__).parent in Path(path).parents
|
||||
|
||||
|
||||
def check_errors(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
global _exception
|
||||
try:
|
||||
fn(*args, **kwargs)
|
||||
except Exception:
|
||||
_exception = sys.exc_info()
|
||||
|
||||
et, ev, tb = _exception
|
||||
|
||||
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:
|
||||
filename = ev.filename
|
||||
|
||||
if filename not in _error_files:
|
||||
_error_files.append(filename)
|
||||
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def raise_last_exception():
|
||||
if _exception is not None:
|
||||
raise _exception[1]
|
||||
|
||||
|
||||
def ensure_echo_on():
|
||||
"""
|
||||
Ensure that echo mode is enabled. Some tools such as PDB disable
|
||||
it which causes usability issues after reload.
|
||||
"""
|
||||
if not termios or not sys.stdin.isatty():
|
||||
return
|
||||
attr_list = termios.tcgetattr(sys.stdin)
|
||||
if not attr_list[3] & termios.ECHO:
|
||||
attr_list[3] |= termios.ECHO
|
||||
if hasattr(signal, "SIGTTOU"):
|
||||
old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
||||
else:
|
||||
old_handler = None
|
||||
termios.tcsetattr(sys.stdin, termios.TCSANOW, attr_list)
|
||||
if old_handler is not None:
|
||||
signal.signal(signal.SIGTTOU, old_handler)
|
||||
|
||||
|
||||
def iter_all_python_module_files():
|
||||
# This is a hot path during reloading. Create a stable sorted list of
|
||||
# modules based on the module name and pass it to iter_modules_and_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)
|
||||
)
|
||||
return iter_modules_and_files(modules, frozenset(_error_files))
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def iter_modules_and_files(modules, extra_files):
|
||||
"""Iterate through all modules needed to be watched."""
|
||||
sys_file_paths = []
|
||||
for module in modules:
|
||||
# During debugging (with PyDev) the 'typing.io' and 'typing.re' objects
|
||||
# are added to sys.modules, however they are types not modules and so
|
||||
# cause issues here.
|
||||
if not isinstance(module, ModuleType):
|
||||
continue
|
||||
if module.__name__ in ("__main__", "__mp_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__"):
|
||||
sys_file_paths.append(module.__file__)
|
||||
continue
|
||||
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
|
||||
)
|
||||
sys_file_paths.append(origin)
|
||||
|
||||
results = set()
|
||||
for filename in itertools.chain(sys_file_paths, extra_files):
|
||||
if not filename:
|
||||
continue
|
||||
path = Path(filename)
|
||||
try:
|
||||
if not path.exists():
|
||||
# The module could have been removed, don't fail loudly if this
|
||||
# is the case.
|
||||
continue
|
||||
except ValueError as e:
|
||||
# Network filesystems may return null bytes in file paths.
|
||||
logger.debug('"%s" raised when resolving path: "%s"', e, path)
|
||||
continue
|
||||
resolved_path = path.resolve().absolute()
|
||||
results.add(resolved_path)
|
||||
return frozenset(results)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def common_roots(paths):
|
||||
"""
|
||||
Return a tuple of common roots that are shared between the given paths.
|
||||
File system watchers operate on directories and aren't cheap to create.
|
||||
Try to find the minimum set of directories to watch that encompass all of
|
||||
the files that need to be watched.
|
||||
"""
|
||||
# Inspired from Werkzeug:
|
||||
# https://github.com/pallets/werkzeug/blob/7477be2853df70a022d9613e765581b9411c3c39/werkzeug/_reloader.py
|
||||
# Create a sorted list of the path components, longest first.
|
||||
path_parts = sorted([x.parts for x in paths], key=len, reverse=True)
|
||||
tree = {}
|
||||
for chunks in path_parts:
|
||||
node = tree
|
||||
# Add each part of the path to the tree.
|
||||
for chunk in chunks:
|
||||
node = node.setdefault(chunk, {})
|
||||
# Clear the last leaf in the tree.
|
||||
node.clear()
|
||||
|
||||
# Turn the tree into a list of Path instances.
|
||||
def _walk(node, path):
|
||||
for prefix, child in node.items():
|
||||
yield from _walk(child, path + (prefix,))
|
||||
if not node:
|
||||
yield Path(*path)
|
||||
|
||||
return tuple(_walk(tree, ()))
|
||||
|
||||
|
||||
def sys_path_directories():
|
||||
"""
|
||||
Yield absolute directories from sys.path, ignoring entries that don't
|
||||
exist.
|
||||
"""
|
||||
for path in sys.path:
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
continue
|
||||
resolved_path = path.resolve().absolute()
|
||||
# If the path is a file (like a zip file), watch the parent directory.
|
||||
if resolved_path.is_file():
|
||||
yield resolved_path.parent
|
||||
else:
|
||||
yield resolved_path
|
||||
|
||||
|
||||
def get_child_arguments():
|
||||
"""
|
||||
Return the executable. This contains a workaround for Windows if the
|
||||
executable is reported to not have the .exe extension which can cause bugs
|
||||
on reloading.
|
||||
"""
|
||||
import __main__
|
||||
|
||||
py_script = Path(sys.argv[0])
|
||||
exe_entrypoint = py_script.with_suffix(".exe")
|
||||
|
||||
args = [sys.executable] + ["-W%s" % o for o in sys.warnoptions]
|
||||
if sys.implementation.name in ("cpython", "pypy"):
|
||||
args.extend(
|
||||
f"-X{key}" if value is True else f"-X{key}={value}"
|
||||
for key, value in sys._xoptions.items()
|
||||
)
|
||||
# __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 and not exe_entrypoint.exists():
|
||||
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]
|
||||
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.
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
args += sys.argv
|
||||
return args
|
||||
|
||||
|
||||
def trigger_reload(filename):
|
||||
logger.info("%s changed, reloading.", filename)
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
|
||||
args = get_child_arguments()
|
||||
while True:
|
||||
p = subprocess.run(args, env=new_environ, close_fds=False)
|
||||
if p.returncode != 3:
|
||||
return p.returncode
|
||||
|
||||
|
||||
class BaseReloader:
|
||||
def __init__(self):
|
||||
self.extra_files = set()
|
||||
self.directory_globs = defaultdict(set)
|
||||
self._stop_condition = threading.Event()
|
||||
|
||||
def watch_dir(self, path, glob):
|
||||
path = Path(path)
|
||||
try:
|
||||
path = path.absolute()
|
||||
except FileNotFoundError:
|
||||
logger.debug(
|
||||
"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)
|
||||
self.directory_globs[path].add(glob)
|
||||
|
||||
def watched_files(self, include_globs=True):
|
||||
"""
|
||||
Yield all files that need to be watched, including module files and
|
||||
files within globs.
|
||||
"""
|
||||
yield from iter_all_python_module_files()
|
||||
yield from self.extra_files
|
||||
if include_globs:
|
||||
for directory, patterns in self.directory_globs.items():
|
||||
for pattern in patterns:
|
||||
yield from directory.glob(pattern)
|
||||
|
||||
def wait_for_apps_ready(self, app_reg, django_main_thread):
|
||||
"""
|
||||
Wait until Django reports that the apps have been loaded. If the given
|
||||
thread has terminated before the apps are ready, then a SyntaxError or
|
||||
other non-recoverable error has been raised. In that case, stop waiting
|
||||
for the apps_ready event and continue processing.
|
||||
|
||||
Return True if the thread is alive and the ready event has been
|
||||
triggered, or False if the thread is terminated while waiting for the
|
||||
event.
|
||||
"""
|
||||
while django_main_thread.is_alive():
|
||||
if app_reg.ready_event.wait(timeout=0.1):
|
||||
return True
|
||||
else:
|
||||
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.")
|
||||
self.wait_for_apps_ready(apps, django_main_thread)
|
||||
from django.urls import get_resolver
|
||||
|
||||
# Prevent a race condition where URL modules aren't loaded when the
|
||||
# reloader starts by accessing the urlconf_module property.
|
||||
try:
|
||||
get_resolver().urlconf_module
|
||||
except Exception:
|
||||
# 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.")
|
||||
autoreload_started.send(sender=self)
|
||||
self.run_loop()
|
||||
|
||||
def run_loop(self):
|
||||
ticker = self.tick()
|
||||
while not self.should_stop:
|
||||
try:
|
||||
next(ticker)
|
||||
except StopIteration:
|
||||
break
|
||||
self.stop()
|
||||
|
||||
def tick(self):
|
||||
"""
|
||||
This generator is called in a loop from run_loop. It's important that
|
||||
the method takes care of pausing or otherwise waiting for a period of
|
||||
time. This split between run_loop() and tick() is to improve the
|
||||
testability of the reloader implementations by decoupling the work they
|
||||
do from the loop.
|
||||
"""
|
||||
raise NotImplementedError("subclasses must implement tick().")
|
||||
|
||||
@classmethod
|
||||
def check_availability(cls):
|
||||
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)
|
||||
if not any(res[1] for res in results):
|
||||
trigger_reload(path)
|
||||
|
||||
# These are primarily used for testing.
|
||||
@property
|
||||
def should_stop(self):
|
||||
return self._stop_condition.is_set()
|
||||
|
||||
def stop(self):
|
||||
self._stop_condition.set()
|
||||
|
||||
|
||||
class StatReloader(BaseReloader):
|
||||
SLEEP_TIME = 1 # Check for changes once per second.
|
||||
|
||||
def tick(self):
|
||||
mtimes = {}
|
||||
while True:
|
||||
for filepath, mtime in self.snapshot_files():
|
||||
old_time = mtimes.get(filepath)
|
||||
mtimes[filepath] = mtime
|
||||
if old_time is None:
|
||||
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,
|
||||
)
|
||||
self.notify_file_changed(filepath)
|
||||
|
||||
time.sleep(self.SLEEP_TIME)
|
||||
yield
|
||||
|
||||
def snapshot_files(self):
|
||||
# watched_files may produce duplicate paths if globs overlap.
|
||||
seen_files = set()
|
||||
for file in self.watched_files():
|
||||
if file in seen_files:
|
||||
continue
|
||||
try:
|
||||
mtime = file.stat().st_mtime
|
||||
except OSError:
|
||||
# This is thrown when the file does not exist.
|
||||
continue
|
||||
seen_files.add(file)
|
||||
yield file, mtime
|
||||
|
||||
@classmethod
|
||||
def check_availability(cls):
|
||||
return True
|
||||
|
||||
|
||||
class WatchmanUnavailable(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
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))
|
||||
super().__init__()
|
||||
|
||||
@cached_property
|
||||
def client(self):
|
||||
return pywatchman.client(timeout=self.client_timeout)
|
||||
|
||||
def _watch_root(self, root):
|
||||
# In practice this shouldn't occur, however, it's possible that a
|
||||
# directory that doesn't exist yet is being watched. If it's outside of
|
||||
# sys.path then this will end up a new root. How to handle this isn't
|
||||
# clear: Not adding the root will likely break when subscribing to the
|
||||
# changes, however, as this is currently an internal API, no files
|
||||
# will be being watched outside of sys.path. Fixing this by checking
|
||||
# inside watch_glob() and watch_dir() is expensive, instead this could
|
||||
# could fall back to the StatReloader if this case is detected? For
|
||||
# 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,
|
||||
)
|
||||
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")
|
||||
|
||||
@lru_cache
|
||||
def _get_clock(self, root):
|
||||
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,
|
||||
]
|
||||
query = {
|
||||
"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)
|
||||
|
||||
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,
|
||||
)
|
||||
return
|
||||
prefix = "files-parent-%s" % directory.name
|
||||
filenames = ["%s/%s" % (directory.name, filename) for filename in filenames]
|
||||
directory = directory.parent
|
||||
expression = ["name", filenames, "wholename"]
|
||||
else:
|
||||
prefix = "files"
|
||||
expression = ["name", filenames]
|
||||
self._subscribe(directory, "%s:%s" % (prefix, directory), expression)
|
||||
|
||||
def _watch_glob(self, directory, patterns):
|
||||
"""
|
||||
Watch a directory with a specific glob. If the directory doesn't yet
|
||||
exist, attempt to watch the parent directory and amend the patterns to
|
||||
include this. It's important this method isn't called more than one per
|
||||
directory when updating all subscriptions. Subsequent calls will
|
||||
overwrite the named subscription, so it must include all possible glob
|
||||
expressions.
|
||||
"""
|
||||
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,
|
||||
)
|
||||
return
|
||||
prefix = "glob-parent-%s" % directory.name
|
||||
patterns = ["%s/%s" % (directory.name, pattern) for pattern in patterns]
|
||||
directory = directory.parent
|
||||
|
||||
expression = ["anyof"]
|
||||
for pattern in patterns:
|
||||
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()
|
||||
watched_file_dirs = [f.parent for f in watched_files]
|
||||
sys_paths = list(sys_path_directories())
|
||||
return frozenset((*extra_directories, *watched_file_dirs, *sys_paths))
|
||||
|
||||
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)
|
||||
# Setup initial roots for performance, shortest roots first.
|
||||
for root in sorted(found_roots):
|
||||
self._watch_root(root)
|
||||
for directory, patterns in self.directory_globs.items():
|
||||
self._watch_glob(directory, patterns)
|
||||
# Group sorted watched_files by their parent directory.
|
||||
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]
|
||||
)
|
||||
|
||||
def update_watches(self):
|
||||
try:
|
||||
self._update_watches()
|
||||
except Exception as ex:
|
||||
# If the service is still available, raise the original exception.
|
||||
if self.check_server_status(ex):
|
||||
raise
|
||||
|
||||
def _check_subscription(self, sub):
|
||||
subscription = self.client.getSubscription(sub)
|
||||
if not subscription:
|
||||
return
|
||||
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", []):
|
||||
self.notify_file_changed(root_directory / file)
|
||||
|
||||
def request_processed(self, **kwargs):
|
||||
logger.debug("Request processed. Setting update_watches event.")
|
||||
self.processed_request.set()
|
||||
|
||||
def tick(self):
|
||||
request_finished.connect(self.request_processed)
|
||||
self.update_watches()
|
||||
while True:
|
||||
if self.processed_request.is_set():
|
||||
self.update_watches()
|
||||
self.processed_request.clear()
|
||||
try:
|
||||
self.client.receive()
|
||||
except pywatchman.SocketTimeout:
|
||||
pass
|
||||
except pywatchman.WatchmanError as ex:
|
||||
logger.debug("Watchman error: %s, checking server status.", ex)
|
||||
self.check_server_status(ex)
|
||||
else:
|
||||
for sub in list(self.client.subs.keys()):
|
||||
self._check_subscription(sub)
|
||||
yield
|
||||
# Protect against busy loops.
|
||||
time.sleep(0.1)
|
||||
|
||||
def stop(self):
|
||||
self.client.close()
|
||||
super().stop()
|
||||
|
||||
def check_server_status(self, inner_ex=None):
|
||||
"""Return True if the server is available."""
|
||||
try:
|
||||
self.client.query("version")
|
||||
except Exception:
|
||||
raise WatchmanUnavailable(str(inner_ex)) from inner_ex
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_availability(cls):
|
||||
if not pywatchman:
|
||||
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"])
|
||||
# Watchman 4.9 includes multiple improvements to watching project
|
||||
# directories as well as case insensitive filesystems.
|
||||
logger.debug("Watchman version %s", version)
|
||||
if version < (4, 9):
|
||||
raise WatchmanUnavailable("Watchman 4.9 or later is required.")
|
||||
|
||||
|
||||
def get_reloader():
|
||||
"""Return the most suitable reloader for this environment."""
|
||||
try:
|
||||
WatchmanReloader.check_availability()
|
||||
except WatchmanUnavailable:
|
||||
return StatReloader()
|
||||
return WatchmanReloader()
|
||||
|
||||
|
||||
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.start()
|
||||
|
||||
while not reloader.should_stop:
|
||||
reloader.run(django_main_thread)
|
||||
|
||||
|
||||
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":
|
||||
reloader = get_reloader()
|
||||
logger.info(
|
||||
"Watching for file changes with %s", reloader.__class__.__name__
|
||||
)
|
||||
start_django(reloader, main_func, *args, **kwargs)
|
||||
else:
|
||||
exit_code = restart_with_reloader()
|
||||
sys.exit(exit_code)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
444
.venv/lib/python3.12/site-packages/django/utils/cache.py
Normal file
444
.venv/lib/python3.12/site-packages/django/utils/cache.py
Normal file
@@ -0,0 +1,444 @@
|
||||
"""
|
||||
This module contains helper functions for controlling caching. It does so by
|
||||
managing the "Vary" header of responses. It includes functions to patch the
|
||||
header of response objects directly and decorators that change functions to do
|
||||
that header-patching themselves.
|
||||
|
||||
For information on the Vary header, see RFC 9110 Section 12.5.5.
|
||||
|
||||
Essentially, the "Vary" HTTP header defines which headers a cache should take
|
||||
into account when building its cache key. Requests with the same path but
|
||||
different header content for headers named in "Vary" need to get different
|
||||
cache keys to prevent delivery of wrong content.
|
||||
|
||||
An example: i18n middleware would need to distinguish caches by the
|
||||
"Accept-language" header.
|
||||
"""
|
||||
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from hashlib import md5
|
||||
|
||||
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.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*")
|
||||
|
||||
|
||||
def patch_cache_control(response, **kwargs):
|
||||
"""
|
||||
Patch the Cache-Control header by adding all keyword arguments to it.
|
||||
The transformation is as follows:
|
||||
|
||||
* All keyword parameter names are turned to lowercase, and underscores
|
||||
are converted to hyphens.
|
||||
* If the value of a parameter is True (exactly True, not just a
|
||||
true value), only the parameter name is added to the header.
|
||||
* All other parameters are added with their value, after applying
|
||||
str() to it.
|
||||
"""
|
||||
|
||||
def dictitem(s):
|
||||
t = s.split("=", 1)
|
||||
if len(t) > 1:
|
||||
return (t[0].lower(), t[1])
|
||||
else:
|
||||
return (t[0].lower(), True)
|
||||
|
||||
def dictvalue(*t):
|
||||
if t[1] is True:
|
||||
return t[0]
|
||||
else:
|
||||
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"]):
|
||||
directive, value = dictitem(field)
|
||||
if directive == "no-cache":
|
||||
# no-cache supports multiple field names.
|
||||
cc[directive].add(value)
|
||||
else:
|
||||
cc[directive] = value
|
||||
|
||||
# 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"])
|
||||
|
||||
# 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"]
|
||||
|
||||
for k, v in kwargs.items():
|
||||
directive = k.replace("_", "-")
|
||||
if directive == "no-cache":
|
||||
# no-cache supports multiple field names.
|
||||
cc[directive].add(v)
|
||||
else:
|
||||
cc[directive] = v
|
||||
|
||||
directives = []
|
||||
for directive, values in cc.items():
|
||||
if isinstance(values, set):
|
||||
if True in values:
|
||||
# True takes precedence.
|
||||
values = {True}
|
||||
directives.extend([dictvalue(directive, value) for value in values])
|
||||
else:
|
||||
directives.append(dictvalue(directive, values))
|
||||
cc = ", ".join(directives)
|
||||
response.headers["Cache-Control"] = cc
|
||||
|
||||
|
||||
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"):
|
||||
return
|
||||
cc = dict(
|
||||
_to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
|
||||
)
|
||||
try:
|
||||
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(
|
||||
md5(response.content, usedforsecurity=False).hexdigest(),
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def _precondition_failed(request):
|
||||
response = HttpResponse(status=412)
|
||||
log_response(
|
||||
"Precondition Failed: %s",
|
||||
request.path,
|
||||
response=response,
|
||||
request=request,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def _not_modified(request, response=None):
|
||||
new_response = HttpResponseNotModified()
|
||||
if response:
|
||||
# Preserve the headers required by RFC 9110 Section 15.4.5, as well as
|
||||
# Last-Modified.
|
||||
for header in (
|
||||
"Cache-Control",
|
||||
"Content-Location",
|
||||
"Date",
|
||||
"ETag",
|
||||
"Expires",
|
||||
"Last-Modified",
|
||||
"Vary",
|
||||
):
|
||||
if header in response:
|
||||
new_response.headers[header] = response.headers[header]
|
||||
|
||||
# Preserve cookies as per the cookie specification: "If a proxy server
|
||||
# receives a response which contains a Set-cookie header, it should
|
||||
# propagate the Set-cookie header to the client, regardless of whether
|
||||
# the response was 304 (Not Modified) or 200 (OK).
|
||||
# https://curl.haxx.se/rfc/cookie_spec.html
|
||||
new_response.cookies = response.cookies
|
||||
return new_response
|
||||
|
||||
|
||||
def get_conditional_response(request, etag=None, last_modified=None, response=None):
|
||||
# Only return conditional responses on successful requests.
|
||||
if response and not (200 <= response.status_code < 300):
|
||||
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_modified_since = if_modified_since and parse_http_date_safe(if_modified_since)
|
||||
|
||||
# Evaluation of request preconditions below follows RFC 9110 Section
|
||||
# 13.2.2.
|
||||
# Step 1: Test the If-Match precondition.
|
||||
if if_match_etags and not _if_match_passes(etag, if_match_etags):
|
||||
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)
|
||||
):
|
||||
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"):
|
||||
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")
|
||||
):
|
||||
return _not_modified(request, response)
|
||||
|
||||
# Step 5: Test the If-Range precondition (not supported).
|
||||
# Step 6: Return original response since there isn't a conditional response.
|
||||
return response
|
||||
|
||||
|
||||
def _if_match_passes(target_etag, etags):
|
||||
"""
|
||||
Test the If-Match comparison as defined in RFC 9110 Section 13.1.1.
|
||||
"""
|
||||
if not target_etag:
|
||||
# If there isn't an ETag, then there can't be a match.
|
||||
return False
|
||||
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/"):
|
||||
# A weak ETag can never strongly match another ETag.
|
||||
return False
|
||||
else:
|
||||
# Since the ETag is strong, this will only return True if there's a
|
||||
# strong match.
|
||||
return target_etag in etags
|
||||
|
||||
|
||||
def _if_unmodified_since_passes(last_modified, if_unmodified_since):
|
||||
"""
|
||||
Test the If-Unmodified-Since comparison as defined in RFC 9110 Section
|
||||
13.1.4.
|
||||
"""
|
||||
return last_modified and last_modified <= if_unmodified_since
|
||||
|
||||
|
||||
def _if_none_match_passes(target_etag, etags):
|
||||
"""
|
||||
Test the If-None-Match comparison as defined in RFC 9110 Section 13.1.2.
|
||||
"""
|
||||
if not target_etag:
|
||||
# If there isn't an ETag, then there isn't a match.
|
||||
return True
|
||||
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)
|
||||
return target_etag not in etags
|
||||
|
||||
|
||||
def _if_modified_since_passes(last_modified, if_modified_since):
|
||||
"""
|
||||
Test the If-Modified-Since comparison as defined in RFC 9110 Section
|
||||
13.1.3.
|
||||
"""
|
||||
return not last_modified or last_modified > if_modified_since
|
||||
|
||||
|
||||
def patch_response_headers(response, cache_timeout=None):
|
||||
"""
|
||||
Add HTTP caching headers to the given HttpResponse: Expires and
|
||||
Cache-Control.
|
||||
|
||||
Each header is only added if it isn't already set.
|
||||
|
||||
cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
|
||||
by default.
|
||||
"""
|
||||
if cache_timeout is 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)
|
||||
patch_cache_control(response, max_age=cache_timeout)
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def patch_vary_headers(response, newheaders):
|
||||
"""
|
||||
Add (or update) the "Vary" header in the given HttpResponse object.
|
||||
newheaders is a list of header names that should be in "Vary". If headers
|
||||
contains an asterisk, then "Vary" header will consist of a single asterisk
|
||||
'*'. Otherwise, existing headers in "Vary" aren't removed.
|
||||
"""
|
||||
# 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"])
|
||||
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
|
||||
]
|
||||
vary_headers += additional_headers
|
||||
if "*" in vary_headers:
|
||||
response.headers["Vary"] = "*"
|
||||
else:
|
||||
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"):
|
||||
return False
|
||||
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
|
||||
|
||||
|
||||
def _i18n_cache_key_suffix(request, cache_key):
|
||||
"""If necessary, add the current locale or time zone to the cache key."""
|
||||
if settings.USE_I18N:
|
||||
# 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())
|
||||
if settings.USE_TZ:
|
||||
cache_key += ".%s" % get_current_timezone_name()
|
||||
return cache_key
|
||||
|
||||
|
||||
def _generate_cache_key(request, method, headerlist, key_prefix):
|
||||
"""Return a cache key from the headers given in the header list."""
|
||||
ctx = md5(usedforsecurity=False)
|
||||
for header in headerlist:
|
||||
value = request.META.get(header)
|
||||
if value is not None:
|
||||
ctx.update(value.encode())
|
||||
url = md5(request.build_absolute_uri().encode("ascii"), usedforsecurity=False)
|
||||
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 = md5(request.build_absolute_uri().encode("ascii"), usedforsecurity=False)
|
||||
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):
|
||||
"""
|
||||
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
|
||||
account from the global URL registry and uses those to build a cache key
|
||||
to check against.
|
||||
|
||||
If there isn't a headerlist stored, return None, indicating that the page
|
||||
needs to be rebuilt.
|
||||
"""
|
||||
if key_prefix is None:
|
||||
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
|
||||
cache_key = _generate_cache_header_key(key_prefix, request)
|
||||
if cache is None:
|
||||
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
|
||||
headerlist = cache.get(cache_key)
|
||||
if headerlist is not None:
|
||||
return _generate_cache_key(request, method, headerlist, key_prefix)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cache=None):
|
||||
"""
|
||||
Learn what headers to take into account for some request URL from the
|
||||
response object. Store those headers in a global URL registry so that
|
||||
later access to that URL will know what headers to take into account
|
||||
without building the response object itself. The headers are named in the
|
||||
Vary header of the response, but we want to prevent response generation.
|
||||
|
||||
The list of headers to use for cache key generation is stored in the same
|
||||
cache as the pages themselves. If the cache ages some data out of the
|
||||
cache, this just means that we have to build the response once to get at
|
||||
the Vary header and so at the list of headers to use for the cache key.
|
||||
"""
|
||||
if key_prefix is None:
|
||||
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
|
||||
if cache_timeout is None:
|
||||
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
|
||||
cache_key = _generate_cache_header_key(key_prefix, request)
|
||||
if cache is None:
|
||||
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
|
||||
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)
|
||||
headerlist.sort()
|
||||
cache.set(cache_key, headerlist, cache_timeout)
|
||||
return _generate_cache_key(request, request.method, headerlist, key_prefix)
|
||||
else:
|
||||
# if there is no Vary header, we still need a cache key
|
||||
# for the request.build_absolute_uri()
|
||||
cache.set(cache_key, [], cache_timeout)
|
||||
return _generate_cache_key(request, request.method, [], key_prefix)
|
||||
|
||||
|
||||
def _to_tuple(s):
|
||||
t = s.split("=", 1)
|
||||
if len(t) == 2:
|
||||
return t[0].lower(), t[1]
|
||||
return t[0].lower(), True
|
||||
116
.venv/lib/python3.12/site-packages/django/utils/choices.py
Normal file
116
.venv/lib/python3.12/site-packages/django/utils/choices.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||
from itertools import islice, tee, zip_longest
|
||||
|
||||
from django.utils.functional import Promise
|
||||
|
||||
__all__ = [
|
||||
"BaseChoiceIterator",
|
||||
"BlankChoiceIterator",
|
||||
"CallableChoiceIterator",
|
||||
"flatten_choices",
|
||||
"normalize_choices",
|
||||
]
|
||||
|
||||
|
||||
class BaseChoiceIterator:
|
||||
"""Base class for lazy iterators for choices."""
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Iterable):
|
||||
return all(a == b for a, b in zip_longest(self, other, fillvalue=object()))
|
||||
return super().__eq__(other)
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, slice) or index < 0:
|
||||
# Suboptimally consume whole iterator to handle slices and negative
|
||||
# indexes.
|
||||
return list(self)[index]
|
||||
try:
|
||||
return next(islice(self, index, index + 1))
|
||||
except StopIteration:
|
||||
raise IndexError("index out of range") from None
|
||||
|
||||
def __iter__(self):
|
||||
raise NotImplementedError(
|
||||
"BaseChoiceIterator subclasses must implement __iter__()."
|
||||
)
|
||||
|
||||
|
||||
class BlankChoiceIterator(BaseChoiceIterator):
|
||||
"""Iterator to lazily inject a blank choice."""
|
||||
|
||||
def __init__(self, choices, blank_choice):
|
||||
self.choices = choices
|
||||
self.blank_choice = blank_choice
|
||||
|
||||
def __iter__(self):
|
||||
choices, other = tee(self.choices)
|
||||
if not any(value in ("", None) for value, _ in flatten_choices(other)):
|
||||
yield from self.blank_choice
|
||||
yield from choices
|
||||
|
||||
|
||||
class CallableChoiceIterator(BaseChoiceIterator):
|
||||
"""Iterator to lazily normalize choices generated by a callable."""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __iter__(self):
|
||||
yield from normalize_choices(self.func())
|
||||
|
||||
|
||||
def flatten_choices(choices):
|
||||
"""Flatten choices by removing nested values."""
|
||||
for value_or_group, label_or_nested in choices or ():
|
||||
if isinstance(label_or_nested, (list, tuple)):
|
||||
yield from label_or_nested
|
||||
else:
|
||||
yield value_or_group, label_or_nested
|
||||
|
||||
|
||||
def normalize_choices(value, *, depth=0):
|
||||
"""Normalize choices values consistently for fields and widgets."""
|
||||
# Avoid circular import when importing django.forms.
|
||||
from django.db.models.enums import ChoicesType
|
||||
|
||||
match value:
|
||||
case BaseChoiceIterator() | Promise() | bytes() | str():
|
||||
# Avoid prematurely normalizing iterators that should be lazy.
|
||||
# Because string-like types are iterable, return early to avoid
|
||||
# iterating over them in the guard for the Iterable case below.
|
||||
return value
|
||||
case ChoicesType():
|
||||
# Choices enumeration helpers already output in canonical form.
|
||||
return value.choices
|
||||
case Mapping() if depth < 2:
|
||||
value = value.items()
|
||||
case Iterator() if depth < 2:
|
||||
# Although Iterator would be handled by the Iterable case below,
|
||||
# the iterator would be consumed prematurely while checking that
|
||||
# its elements are not string-like in the guard, so we handle it
|
||||
# separately.
|
||||
pass
|
||||
case Iterable() if depth < 2 and not any(
|
||||
isinstance(x, (Promise, bytes, str)) for x in value
|
||||
):
|
||||
# String-like types are iterable, so the guard above ensures that
|
||||
# they're handled by the default case below.
|
||||
pass
|
||||
case Callable() if depth == 0:
|
||||
# If at the top level, wrap callables to be evaluated lazily.
|
||||
return CallableChoiceIterator(value)
|
||||
case Callable() if depth < 2:
|
||||
value = value()
|
||||
case _:
|
||||
return value
|
||||
|
||||
try:
|
||||
# Recursive call to convert any nested values to a list of 2-tuples.
|
||||
return [(k, normalize_choices(v, depth=depth + 1)) for k, v in value]
|
||||
except (TypeError, ValueError):
|
||||
# Return original value for the system check to raise if it has items
|
||||
# that are not iterable or not 2-tuples:
|
||||
# - TypeError: cannot unpack non-iterable <type> object
|
||||
# - ValueError: <not enough / too many> values to unpack
|
||||
return value
|
||||
@@ -0,0 +1,85 @@
|
||||
from asgiref.local import Local
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class ConnectionProxy:
|
||||
"""Proxy for accessing a connection object's attributes."""
|
||||
|
||||
def __init__(self, connections, alias):
|
||||
self.__dict__["_connections"] = connections
|
||||
self.__dict__["_alias"] = alias
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._connections[self._alias], item)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
return setattr(self._connections[self._alias], name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
return delattr(self._connections[self._alias], name)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._connections[self._alias]
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._connections[self._alias] == other
|
||||
|
||||
|
||||
class ConnectionDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseConnectionHandler:
|
||||
settings_name = None
|
||||
exception_class = ConnectionDoesNotExist
|
||||
thread_critical = False
|
||||
|
||||
def __init__(self, settings=None):
|
||||
self._settings = settings
|
||||
self._connections = Local(self.thread_critical)
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
self._settings = self.configure_settings(self._settings)
|
||||
return self._settings
|
||||
|
||||
def configure_settings(self, settings):
|
||||
if settings is None:
|
||||
settings = getattr(django_settings, self.settings_name)
|
||||
return settings
|
||||
|
||||
def create_connection(self, alias):
|
||||
raise NotImplementedError("Subclasses must implement create_connection().")
|
||||
|
||||
def __getitem__(self, alias):
|
||||
try:
|
||||
return getattr(self._connections, alias)
|
||||
except AttributeError:
|
||||
if alias not in self.settings:
|
||||
raise self.exception_class(f"The connection '{alias}' doesn't exist.")
|
||||
conn = self.create_connection(alias)
|
||||
setattr(self._connections, alias, conn)
|
||||
return conn
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self._connections, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
delattr(self._connections, key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.settings)
|
||||
|
||||
def all(self, initialized_only=False):
|
||||
return [
|
||||
self[alias]
|
||||
for alias in self
|
||||
# If initialized_only is True, return only initialized connections.
|
||||
if not initialized_only or hasattr(self._connections, alias)
|
||||
]
|
||||
|
||||
def close_all(self):
|
||||
for conn in self.all(initialized_only=True):
|
||||
conn.close()
|
||||
77
.venv/lib/python3.12/site-packages/django/utils/crypto.py
Normal file
77
.venv/lib/python3.12/site-packages/django/utils/crypto.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Django's standard crypto functions and utilities.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
|
||||
from django.conf import settings
|
||||
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"):
|
||||
"""
|
||||
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,
|
||||
but any algorithm name supported by hashlib can be passed.
|
||||
|
||||
A different key_salt should be passed in for every application of HMAC.
|
||||
"""
|
||||
if secret is None:
|
||||
secret = settings.SECRET_KEY
|
||||
|
||||
key_salt = force_bytes(key_salt)
|
||||
secret = force_bytes(secret)
|
||||
try:
|
||||
hasher = getattr(hashlib, algorithm)
|
||||
except AttributeError as e:
|
||||
raise InvalidAlgorithm(
|
||||
"%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.
|
||||
key = hasher(key_salt + secret).digest()
|
||||
# If len(key_salt + secret) > block size of the hash algorithm, the above
|
||||
# line is redundant and could be replaced by key = key_salt + secret, since
|
||||
# the hmac module does the same thing for keys longer than the block size.
|
||||
# However, we need to ensure that we *always* do this.
|
||||
return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
|
||||
|
||||
|
||||
RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
|
||||
def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
||||
"""
|
||||
Return a securely generated random string.
|
||||
|
||||
The bit length of the returned value can be calculated with the formula:
|
||||
log_2(len(allowed_chars)^length)
|
||||
|
||||
For example, with default `allowed_chars` (26+26+10), this gives:
|
||||
* length: 12, bit length =~ 71 bits
|
||||
* length: 22, bit length =~ 131 bits
|
||||
"""
|
||||
return "".join(secrets.choice(allowed_chars) for i in range(length))
|
||||
|
||||
|
||||
def constant_time_compare(val1, val2):
|
||||
"""Return True if the two strings are equal, False otherwise."""
|
||||
return secrets.compare_digest(force_bytes(val1), force_bytes(val2))
|
||||
|
||||
|
||||
def pbkdf2(password, salt, iterations, dklen=0, digest=None):
|
||||
"""Return the hash of password using pbkdf2."""
|
||||
if digest is None:
|
||||
digest = hashlib.sha256
|
||||
dklen = dklen or None
|
||||
password = force_bytes(password)
|
||||
salt = force_bytes(salt)
|
||||
return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)
|
||||
@@ -0,0 +1,345 @@
|
||||
import copy
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
class OrderedSet:
|
||||
"""
|
||||
A set which keeps the ordering of the inserted items.
|
||||
"""
|
||||
|
||||
def __init__(self, iterable=None):
|
||||
self.dict = dict.fromkeys(iterable or ())
|
||||
|
||||
def add(self, item):
|
||||
self.dict[item] = None
|
||||
|
||||
def remove(self, item):
|
||||
del self.dict[item]
|
||||
|
||||
def discard(self, item):
|
||||
try:
|
||||
self.remove(item)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.dict)
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self.dict)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.dict
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.dict)
|
||||
|
||||
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
|
||||
|
||||
|
||||
class MultiValueDict(dict):
|
||||
"""
|
||||
A subclass of dictionary customized to handle multiple values for the
|
||||
same key.
|
||||
|
||||
>>> d = MultiValueDict({'name': ['Adrian', 'Simon'], 'position': ['Developer']})
|
||||
>>> d['name']
|
||||
'Simon'
|
||||
>>> d.getlist('name')
|
||||
['Adrian', 'Simon']
|
||||
>>> d.getlist('doesnotexist')
|
||||
[]
|
||||
>>> d.getlist('doesnotexist', ['Adrian', 'Simon'])
|
||||
['Adrian', 'Simon']
|
||||
>>> d.get('lastname', 'nonexistent')
|
||||
'nonexistent'
|
||||
>>> 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
|
||||
single name-value pairs.
|
||||
"""
|
||||
|
||||
def __init__(self, key_to_list_mapping=()):
|
||||
super().__init__(key_to_list_mapping)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: %s>" % (self.__class__.__name__, super().__repr__())
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return the last data value for this key, or [] if it's an empty list;
|
||||
raise KeyError if not found.
|
||||
"""
|
||||
try:
|
||||
list_ = super().__getitem__(key)
|
||||
except KeyError:
|
||||
raise MultiValueDictKeyError(key)
|
||||
try:
|
||||
return list_[-1]
|
||||
except IndexError:
|
||||
return []
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__setitem__(key, [value])
|
||||
|
||||
def __copy__(self):
|
||||
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)
|
||||
)
|
||||
return result
|
||||
|
||||
def __getstate__(self):
|
||||
return {**self.__dict__, "_data": {k: self._getlist(k) for k in self}}
|
||||
|
||||
def __setstate__(self, obj_dict):
|
||||
data = obj_dict.pop("_data", {})
|
||||
for k, v in data.items():
|
||||
self.setlist(k, v)
|
||||
self.__dict__.update(obj_dict)
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""
|
||||
Return the last data value for the passed key. If key doesn't exist
|
||||
or value is an empty list, return `default`.
|
||||
"""
|
||||
try:
|
||||
val = self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
if val == []:
|
||||
return default
|
||||
return val
|
||||
|
||||
def _getlist(self, key, default=None, force_list=False):
|
||||
"""
|
||||
Return a list of values for the key.
|
||||
|
||||
Used internally to manipulate values list. If force_list is True,
|
||||
return a new copy of values.
|
||||
"""
|
||||
try:
|
||||
values = super().__getitem__(key)
|
||||
except KeyError:
|
||||
if default is None:
|
||||
return []
|
||||
return default
|
||||
else:
|
||||
if force_list:
|
||||
values = list(values) if values is not None else None
|
||||
return values
|
||||
|
||||
def getlist(self, key, default=None):
|
||||
"""
|
||||
Return the list of values for the key. If key doesn't exist, return a
|
||||
default value.
|
||||
"""
|
||||
return self._getlist(key, default, force_list=True)
|
||||
|
||||
def setlist(self, key, list_):
|
||||
super().__setitem__(key, list_)
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
# Do not return default here because __setitem__() may store
|
||||
# another value -- QueryDict.__setitem__() does. Look it up.
|
||||
return self[key]
|
||||
|
||||
def setlistdefault(self, key, default_list=None):
|
||||
if key not in self:
|
||||
if default_list is None:
|
||||
default_list = []
|
||||
self.setlist(key, default_list)
|
||||
# Do not return default_list here because setlist() may store
|
||||
# another value -- QueryDict.setlist() does. Look it up.
|
||||
return self._getlist(key)
|
||||
|
||||
def appendlist(self, key, value):
|
||||
"""Append an item to the internal list associated with key."""
|
||||
self.setlistdefault(key).append(value)
|
||||
|
||||
def items(self):
|
||||
"""
|
||||
Yield (key, value) pairs, where value is the last item in the list
|
||||
associated with the key.
|
||||
"""
|
||||
for key in self:
|
||||
yield key, self[key]
|
||||
|
||||
def lists(self):
|
||||
"""Yield (key, list) pairs."""
|
||||
return iter(super().items())
|
||||
|
||||
def values(self):
|
||||
"""Yield the last value on every key list."""
|
||||
for key in self:
|
||||
yield self[key]
|
||||
|
||||
def copy(self):
|
||||
"""Return a shallow copy of this object."""
|
||||
return copy.copy(self)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
"""Extend rather than replace existing key lists."""
|
||||
if len(args) > 1:
|
||||
raise TypeError("update expected at most 1 argument, got %d" % len(args))
|
||||
if args:
|
||||
arg = args[0]
|
||||
if isinstance(arg, MultiValueDict):
|
||||
for key, value_list in arg.lists():
|
||||
self.setlistdefault(key).extend(value_list)
|
||||
else:
|
||||
if isinstance(arg, Mapping):
|
||||
arg = arg.items()
|
||||
for key, value in arg:
|
||||
self.setlistdefault(key).append(value)
|
||||
for key, value in kwargs.items():
|
||||
self.setlistdefault(key).append(value)
|
||||
|
||||
def dict(self):
|
||||
"""Return current object as a dict with singular values."""
|
||||
return {key: self[key] for key in self}
|
||||
|
||||
|
||||
class ImmutableList(tuple):
|
||||
"""
|
||||
A tuple-like object that raises useful errors when it is asked to mutate.
|
||||
|
||||
Example::
|
||||
|
||||
>>> a = ImmutableList(range(5), warning="You cannot mutate this.")
|
||||
>>> a[3] = '4'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: You cannot mutate this.
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, warning="ImmutableList object is immutable.", **kwargs):
|
||||
self = tuple.__new__(cls, *args, **kwargs)
|
||||
self.warning = warning
|
||||
return self
|
||||
|
||||
def complain(self, *args, **kwargs):
|
||||
raise AttributeError(self.warning)
|
||||
|
||||
# All list mutation functions complain.
|
||||
__delitem__ = complain
|
||||
__delslice__ = complain
|
||||
__iadd__ = complain
|
||||
__imul__ = complain
|
||||
__setitem__ = complain
|
||||
__setslice__ = complain
|
||||
append = complain
|
||||
extend = complain
|
||||
insert = complain
|
||||
pop = complain
|
||||
remove = complain
|
||||
sort = complain
|
||||
reverse = complain
|
||||
|
||||
|
||||
class DictWrapper(dict):
|
||||
"""
|
||||
Wrap accesses to a dictionary so that certain values (those starting with
|
||||
the specified prefix) are passed through a function before being returned.
|
||||
The prefix is removed before looking up the real value.
|
||||
|
||||
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
|
||||
self.prefix = prefix
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Retrieve the real value after stripping the prefix string (if
|
||||
present). If the prefix is present, pass the value through self.func
|
||||
before returning, otherwise return the raw value.
|
||||
"""
|
||||
use_func = key.startswith(self.prefix)
|
||||
key = key.removeprefix(self.prefix)
|
||||
value = super().__getitem__(key)
|
||||
if use_func:
|
||||
return self.func(value)
|
||||
return value
|
||||
|
||||
|
||||
class CaseInsensitiveMapping(Mapping):
|
||||
"""
|
||||
Mapping allowing case-insensitive key lookups. Original case of keys is
|
||||
preserved for iteration and string representation.
|
||||
|
||||
Example::
|
||||
|
||||
>>> ci_map = CaseInsensitiveMapping({'name': 'Jane'})
|
||||
>>> ci_map['Name']
|
||||
Jane
|
||||
>>> ci_map['NAME']
|
||||
Jane
|
||||
>>> ci_map['name']
|
||||
Jane
|
||||
>>> ci_map # original case preserved
|
||||
{'name': 'Jane'}
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
self._store = {k.lower(): (k, v) for k, v in self._unpack_items(data)}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._store[key.lower()][1]
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
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()}
|
||||
|
||||
def __iter__(self):
|
||||
return (original_key for original_key, value in self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
return repr({key: value for key, value in self._store.values()})
|
||||
|
||||
def copy(self):
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _unpack_items(data):
|
||||
# Explicitly test for dict first as the common case for performance,
|
||||
# avoiding abc's __instancecheck__ and _abc_instancecheck for the
|
||||
# general Mapping case.
|
||||
if isinstance(data, (dict, Mapping)):
|
||||
yield from data.items()
|
||||
return
|
||||
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))
|
||||
)
|
||||
if not isinstance(elem[0], str):
|
||||
raise ValueError(
|
||||
"Element key %r invalid, only strings are allowed" % elem[0]
|
||||
)
|
||||
yield elem
|
||||
331
.venv/lib/python3.12/site-packages/django/utils/dateformat.py
Normal file
331
.venv/lib/python3.12/site-packages/django/utils/dateformat.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
PHP date() style date formatting
|
||||
See https://www.php.net/date for format strings
|
||||
|
||||
Usage:
|
||||
>>> from datetime import datetime
|
||||
>>> d = datetime.now()
|
||||
>>> df = DateFormat(d)
|
||||
>>> print(df.format('jS F Y H:i'))
|
||||
7th October 2003 11:39
|
||||
>>>
|
||||
"""
|
||||
|
||||
import calendar
|
||||
from datetime import date, datetime, 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,
|
||||
)
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.timezone import (
|
||||
_datetime_ambiguous_or_imaginary,
|
||||
get_default_timezone,
|
||||
is_naive,
|
||||
make_aware,
|
||||
)
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
re_formatchars = _lazy_re_compile(r"(?<!\\)([aAbcdDeEfFgGhHiIjlLmMnNoOPrsStTUuwWyYzZ])")
|
||||
re_escaped = _lazy_re_compile(r"\\(.)")
|
||||
|
||||
|
||||
class Formatter:
|
||||
def format(self, formatstr):
|
||||
pieces = []
|
||||
for i, piece in enumerate(re_formatchars.split(str(formatstr))):
|
||||
if i % 2:
|
||||
if type(self.data) is date and hasattr(TimeFormat, piece):
|
||||
raise TypeError(
|
||||
"The format for date objects may not contain "
|
||||
"time-related format specifiers (found '%s')." % piece
|
||||
)
|
||||
pieces.append(str(getattr(self, piece)()))
|
||||
elif piece:
|
||||
pieces.append(re_escaped.sub(r"\1", piece))
|
||||
return "".join(pieces)
|
||||
|
||||
|
||||
class TimeFormat(Formatter):
|
||||
def __init__(self, obj):
|
||||
self.data = obj
|
||||
self.timezone = None
|
||||
|
||||
if isinstance(obj, datetime):
|
||||
# Timezone is only supported when formatting datetime objects, not
|
||||
# date objects (timezone information not appropriate), or time
|
||||
# objects (against established django policy).
|
||||
if is_naive(obj):
|
||||
timezone = get_default_timezone()
|
||||
else:
|
||||
timezone = obj.tzinfo
|
||||
if not _datetime_ambiguous_or_imaginary(obj, timezone):
|
||||
self.timezone = timezone
|
||||
|
||||
def a(self):
|
||||
"'a.m.' or 'p.m.'"
|
||||
if self.data.hour > 11:
|
||||
return _("p.m.")
|
||||
return _("a.m.")
|
||||
|
||||
def A(self):
|
||||
"'AM' or 'PM'"
|
||||
if self.data.hour > 11:
|
||||
return _("PM")
|
||||
return _("AM")
|
||||
|
||||
def e(self):
|
||||
"""
|
||||
Timezone name.
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if not self.timezone:
|
||||
return ""
|
||||
|
||||
try:
|
||||
if getattr(self.data, "tzinfo", None):
|
||||
return self.data.tzname() or ""
|
||||
except NotImplementedError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def f(self):
|
||||
"""
|
||||
Time, in 12-hour hours and minutes, with minutes left off if they're
|
||||
zero.
|
||||
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
|
||||
|
||||
def g(self):
|
||||
"Hour, 12-hour format without leading zeros; i.e. '1' to '12'"
|
||||
return self.data.hour % 12 or 12
|
||||
|
||||
def G(self):
|
||||
"Hour, 24-hour format without leading zeros; i.e. '0' to '23'"
|
||||
return self.data.hour
|
||||
|
||||
def h(self):
|
||||
"Hour, 12-hour format; i.e. '01' to '12'"
|
||||
return "%02d" % (self.data.hour % 12 or 12)
|
||||
|
||||
def H(self):
|
||||
"Hour, 24-hour format; i.e. '00' to '23'"
|
||||
return "%02d" % self.data.hour
|
||||
|
||||
def i(self):
|
||||
"Minutes; i.e. '00' to '59'"
|
||||
return "%02d" % self.data.minute
|
||||
|
||||
def O(self): # NOQA: E743, E741
|
||||
"""
|
||||
Difference to Greenwich time in hours; e.g. '+0200', '-0430'.
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if self.timezone is None:
|
||||
return ""
|
||||
|
||||
offset = self.timezone.utcoffset(self.data)
|
||||
seconds = offset.days * 86400 + offset.seconds
|
||||
sign = "-" if seconds < 0 else "+"
|
||||
seconds = abs(seconds)
|
||||
return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60)
|
||||
|
||||
def P(self):
|
||||
"""
|
||||
Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
|
||||
if they're zero and the strings 'midnight' and 'noon' if appropriate.
|
||||
Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
|
||||
Proprietary extension.
|
||||
"""
|
||||
if self.data.minute == 0 and self.data.hour == 0:
|
||||
return _("midnight")
|
||||
if self.data.minute == 0 and self.data.hour == 12:
|
||||
return _("noon")
|
||||
return "%s %s" % (self.f(), self.a())
|
||||
|
||||
def s(self):
|
||||
"Seconds; i.e. '00' to '59'"
|
||||
return "%02d" % self.data.second
|
||||
|
||||
def T(self):
|
||||
"""
|
||||
Time zone of this machine; e.g. 'EST' or 'MDT'.
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if self.timezone is None:
|
||||
return ""
|
||||
|
||||
return str(self.timezone.tzname(self.data))
|
||||
|
||||
def u(self):
|
||||
"Microseconds; i.e. '000000' to '999999'"
|
||||
return "%06d" % self.data.microsecond
|
||||
|
||||
def Z(self):
|
||||
"""
|
||||
Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
|
||||
timezones west of UTC is always negative, and for those east of UTC is
|
||||
always positive.
|
||||
|
||||
If timezone information is not available, return an empty string.
|
||||
"""
|
||||
if self.timezone is None:
|
||||
return ""
|
||||
|
||||
offset = self.timezone.utcoffset(self.data)
|
||||
|
||||
# `offset` is a datetime.timedelta. For negative values (to the west of
|
||||
# UTC) only days can be negative (days=-1) and seconds are always
|
||||
# positive. e.g. UTC-1 -> timedelta(days=-1, seconds=82800, microseconds=0)
|
||||
# Positive offsets have days=0
|
||||
return offset.days * 86400 + offset.seconds
|
||||
|
||||
|
||||
class DateFormat(TimeFormat):
|
||||
def b(self):
|
||||
"Month, textual, 3 letters, lowercase; e.g. 'jan'"
|
||||
return MONTHS_3[self.data.month]
|
||||
|
||||
def c(self):
|
||||
"""
|
||||
ISO 8601 Format
|
||||
Example : '2008-01-02T10:30:00.000123'
|
||||
"""
|
||||
return self.data.isoformat()
|
||||
|
||||
def d(self):
|
||||
"Day of the month, 2 digits with leading zeros; i.e. '01' to '31'"
|
||||
return "%02d" % self.data.day
|
||||
|
||||
def D(self):
|
||||
"Day of the week, textual, 3 letters; e.g. 'Fri'"
|
||||
return WEEKDAYS_ABBR[self.data.weekday()]
|
||||
|
||||
def E(self):
|
||||
"Alternative month names as required by some locales. Proprietary extension."
|
||||
return MONTHS_ALT[self.data.month]
|
||||
|
||||
def F(self):
|
||||
"Month, textual, long; e.g. 'January'"
|
||||
return MONTHS[self.data.month]
|
||||
|
||||
def I(self): # NOQA: E743, E741
|
||||
"'1' if daylight saving time, '0' otherwise."
|
||||
if self.timezone is None:
|
||||
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'"
|
||||
return self.data.day
|
||||
|
||||
def l(self): # NOQA: E743, E741
|
||||
"Day of the week, textual, long; e.g. 'Friday'"
|
||||
return WEEKDAYS[self.data.weekday()]
|
||||
|
||||
def L(self):
|
||||
"Boolean for whether it is a leap year; i.e. True or False"
|
||||
return calendar.isleap(self.data.year)
|
||||
|
||||
def m(self):
|
||||
"Month; i.e. '01' to '12'"
|
||||
return "%02d" % self.data.month
|
||||
|
||||
def M(self):
|
||||
"Month, textual, 3 letters; e.g. 'Jan'"
|
||||
return MONTHS_3[self.data.month].title()
|
||||
|
||||
def n(self):
|
||||
"Month without leading zeros; i.e. '1' to '12'"
|
||||
return self.data.month
|
||||
|
||||
def N(self):
|
||||
"Month abbreviation in Associated Press style. Proprietary extension."
|
||||
return MONTHS_AP[self.data.month]
|
||||
|
||||
def o(self):
|
||||
"ISO 8601 year number matching the ISO week number (W)"
|
||||
return self.data.isocalendar().year
|
||||
|
||||
def r(self):
|
||||
"RFC 5322 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'"
|
||||
value = self.data
|
||||
if not isinstance(value, datetime):
|
||||
# Assume midnight in default timezone if datetime.date provided.
|
||||
default_timezone = get_default_timezone()
|
||||
value = datetime.combine(value, time.min).replace(tzinfo=default_timezone)
|
||||
elif is_naive(value):
|
||||
value = make_aware(value, timezone=self.timezone)
|
||||
return format_datetime_rfc5322(value)
|
||||
|
||||
def S(self):
|
||||
"""
|
||||
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"
|
||||
last = self.data.day % 10
|
||||
if last == 1:
|
||||
return "st"
|
||||
if last == 2:
|
||||
return "nd"
|
||||
if last == 3:
|
||||
return "rd"
|
||||
return "th"
|
||||
|
||||
def t(self):
|
||||
"Number of days in the given month; i.e. '28' to '31'"
|
||||
return 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):
|
||||
value = datetime.combine(value, time.min)
|
||||
return int(value.timestamp())
|
||||
|
||||
def w(self):
|
||||
"Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)"
|
||||
return (self.data.weekday() + 1) % 7
|
||||
|
||||
def W(self):
|
||||
"ISO-8601 week number of year, weeks starting on Monday"
|
||||
return self.data.isocalendar().week
|
||||
|
||||
def y(self):
|
||||
"""Year, 2 digits with leading zeros; e.g. '99'."""
|
||||
return "%02d" % (self.data.year % 100)
|
||||
|
||||
def Y(self):
|
||||
"""Year, 4 digits with leading zeros; e.g. '1999'."""
|
||||
return "%04d" % self.data.year
|
||||
|
||||
def z(self):
|
||||
"""Day of the year, i.e. 1 to 366."""
|
||||
return self.data.timetuple().tm_yday
|
||||
|
||||
|
||||
def format(value, format_string):
|
||||
"Convenience function"
|
||||
df = DateFormat(value)
|
||||
return df.format(format_string)
|
||||
|
||||
|
||||
def time_format(value, format_string):
|
||||
"Convenience function"
|
||||
tf = TimeFormat(value)
|
||||
return tf.format(format_string)
|
||||
154
.venv/lib/python3.12/site-packages/django/utils/dateparse.py
Normal file
154
.venv/lib/python3.12/site-packages/django/utils/dateparse.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Functions to parse datetime objects."""
|
||||
|
||||
# We're using regular expressions rather than time.strptime because:
|
||||
# - They provide both validation and parsing.
|
||||
# - They're more flexible for datetimes.
|
||||
# - The date/datetime/time constructors produce friendlier error messages.
|
||||
|
||||
import datetime
|
||||
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.timezone import get_fixed_timezone
|
||||
|
||||
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})?)?$"
|
||||
)
|
||||
|
||||
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})?)?$"
|
||||
)
|
||||
|
||||
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"$"
|
||||
)
|
||||
|
||||
# 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"$"
|
||||
)
|
||||
|
||||
# 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")?$"
|
||||
)
|
||||
|
||||
|
||||
def parse_date(value):
|
||||
"""Parse a string and return a datetime.date.
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def parse_time(value):
|
||||
"""Parse a string and return a datetime.time.
|
||||
|
||||
This function doesn't support time zone offsets.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid time.
|
||||
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)
|
||||
|
||||
|
||||
def parse_datetime(value):
|
||||
"""Parse a string and return a datetime.datetime.
|
||||
|
||||
This function supports time zone offsets. When the input contains one,
|
||||
the output uses a timezone with a fixed offset from UTC.
|
||||
|
||||
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 = datetime.timezone.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)
|
||||
|
||||
|
||||
def parse_duration(value):
|
||||
"""Parse a duration string and return a datetime.timedelta.
|
||||
|
||||
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
|
||||
|
||||
Also supports ISO 8601 representation and PostgreSQL's day-time interval
|
||||
format.
|
||||
"""
|
||||
match = (
|
||||
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")
|
||||
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)
|
||||
if match.re == iso8601_duration_re:
|
||||
days *= sign
|
||||
return days + sign * datetime.timedelta(**kw)
|
||||
79
.venv/lib/python3.12/site-packages/django/utils/dates.py
Normal file
79
.venv/lib/python3.12/site-packages/django/utils/dates.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"Commonly-used date structures"
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
WEEKDAYS = {
|
||||
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"),
|
||||
}
|
||||
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"),
|
||||
}
|
||||
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"),
|
||||
}
|
||||
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."),
|
||||
}
|
||||
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"),
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django.utils.version import get_docs_version
|
||||
|
||||
|
||||
def deconstructible(*args, path=None):
|
||||
"""
|
||||
Class decorator that allows the decorated class to be serialized
|
||||
by the migrations subsystem.
|
||||
|
||||
The `path` kwarg specifies the import path.
|
||||
"""
|
||||
|
||||
def decorator(klass):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We capture the arguments to make returning them trivial
|
||||
obj = super(klass, cls).__new__(cls)
|
||||
obj._constructor_args = (args, kwargs)
|
||||
return obj
|
||||
|
||||
def deconstruct(obj):
|
||||
"""
|
||||
Return a 3-tuple of class import path, positional arguments,
|
||||
and keyword arguments.
|
||||
"""
|
||||
# Fallback version
|
||||
if path and type(obj) is klass:
|
||||
module_name, _, name = path.rpartition(".")
|
||||
else:
|
||||
module_name = obj.__module__
|
||||
name = obj.__class__.__name__
|
||||
# Make sure it's actually there and not an inner class
|
||||
module = import_module(module_name)
|
||||
if not hasattr(module, name):
|
||||
raise ValueError(
|
||||
"Could not find object %s in %s.\n"
|
||||
"Please note that you cannot serialize things like inner "
|
||||
"classes. Please move the object into the main module "
|
||||
"body to use migrations.\n"
|
||||
"For more information, see "
|
||||
"https://docs.djangoproject.com/en/%s/topics/migrations/"
|
||||
"#serializing-values" % (name, module_name, get_docs_version())
|
||||
)
|
||||
return (
|
||||
(
|
||||
path
|
||||
if path and type(obj) is klass
|
||||
else f"{obj.__class__.__module__}.{name}"
|
||||
),
|
||||
obj._constructor_args[0],
|
||||
obj._constructor_args[1],
|
||||
)
|
||||
|
||||
klass.__new__ = staticmethod(__new__)
|
||||
klass.deconstruct = deconstruct
|
||||
|
||||
return klass
|
||||
|
||||
if not args:
|
||||
return decorator
|
||||
return decorator(*args)
|
||||
229
.venv/lib/python3.12/site-packages/django/utils/decorators.py
Normal file
229
.venv/lib/python3.12/site-packages/django/utils/decorators.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"Functions that help with dynamically creating decorators for views."
|
||||
|
||||
from functools import partial, update_wrapper, wraps
|
||||
|
||||
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
||||
|
||||
|
||||
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."
|
||||
)
|
||||
return super().__get__(instance, cls)
|
||||
|
||||
|
||||
def _update_method_wrapper(_wrapper, decorator):
|
||||
# _multi_decorate()'s bound_method isn't available in this scope. Cheat by
|
||||
# using it on a dummy function.
|
||||
@decorator
|
||||
def dummy(*args, **kwargs):
|
||||
pass
|
||||
|
||||
update_wrapper(_wrapper, dummy)
|
||||
|
||||
|
||||
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__"):
|
||||
# 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.
|
||||
decorators = decorators[::-1]
|
||||
else:
|
||||
decorators = [decorators]
|
||||
|
||||
def _wrapper(self, *args, **kwargs):
|
||||
# bound_method has the signature that 'decorator' expects i.e. no
|
||||
# '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))))
|
||||
for dec in decorators:
|
||||
bound_method = dec(bound_method)
|
||||
return bound_method(*args, **kwargs)
|
||||
|
||||
# Copy any attributes that a decorator adds to the function it decorates.
|
||||
for dec in decorators:
|
||||
_update_method_wrapper(_wrapper, dec)
|
||||
# Preserve any existing attributes of 'method', including the name.
|
||||
update_wrapper(_wrapper, method)
|
||||
|
||||
if iscoroutinefunction(method):
|
||||
markcoroutinefunction(_wrapper)
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
def method_decorator(decorator, name=""):
|
||||
"""
|
||||
Convert a function decorator into a method decorator
|
||||
"""
|
||||
|
||||
# 'obj' can be a class or a function. If 'obj' is a function at the time it
|
||||
# is passed to _dec, it will eventually be a method of the class it is
|
||||
# defined on. If 'obj' is a class, the 'name' is required to be the name
|
||||
# of the method that will be decorated.
|
||||
def _dec(obj):
|
||||
if not isinstance(obj, type):
|
||||
return _multi_decorate(decorator, obj)
|
||||
if not (name and hasattr(obj, name)):
|
||||
raise ValueError(
|
||||
"The keyword argument `name` must be the name of a method "
|
||||
"of the decorated class: %s. Got '%s' instead." % (obj, name)
|
||||
)
|
||||
method = getattr(obj, name)
|
||||
if not callable(method):
|
||||
raise TypeError(
|
||||
"Cannot decorate '%s' as it isn't a callable attribute of "
|
||||
"%s (%s)." % (name, obj, method)
|
||||
)
|
||||
_wrapper = _multi_decorate(decorator, method)
|
||||
setattr(obj, name, _wrapper)
|
||||
return obj
|
||||
|
||||
# Don't worry about making _dec look similar to a list/tuple as it's rather
|
||||
# meaningless.
|
||||
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__
|
||||
return _dec
|
||||
|
||||
|
||||
def decorator_from_middleware_with_args(middleware_class):
|
||||
"""
|
||||
Like decorator_from_middleware, but return a function
|
||||
that accepts the arguments to be passed to the middleware_class.
|
||||
Use like::
|
||||
|
||||
cache_page = decorator_from_middleware_with_args(CacheMiddleware)
|
||||
# ...
|
||||
|
||||
@cache_page(3600)
|
||||
def my_view(request):
|
||||
# ...
|
||||
"""
|
||||
return make_middleware_decorator(middleware_class)
|
||||
|
||||
|
||||
def decorator_from_middleware(middleware_class):
|
||||
"""
|
||||
Given a middleware class (not an instance), return a view decorator. This
|
||||
lets you use middleware functionality on a per-view basis. The middleware
|
||||
is created with no params passed.
|
||||
"""
|
||||
return make_middleware_decorator(middleware_class)()
|
||||
|
||||
|
||||
def make_middleware_decorator(middleware_class):
|
||||
def _make_decorator(*m_args, **m_kwargs):
|
||||
def _decorator(view_func):
|
||||
middleware = middleware_class(view_func, *m_args, **m_kwargs)
|
||||
|
||||
def _pre_process_request(request, *args, **kwargs):
|
||||
if hasattr(middleware, "process_request"):
|
||||
result = middleware.process_request(request)
|
||||
if result is not None:
|
||||
return result
|
||||
if hasattr(middleware, "process_view"):
|
||||
result = middleware.process_view(request, view_func, args, kwargs)
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
def _process_exception(request, exception):
|
||||
if hasattr(middleware, "process_exception"):
|
||||
result = middleware.process_exception(request, exception)
|
||||
if result is not None:
|
||||
return result
|
||||
raise
|
||||
|
||||
def _post_process_request(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"):
|
||||
|
||||
def callback(response):
|
||||
return middleware.process_response(request, response)
|
||||
|
||||
response.add_post_render_callback(callback)
|
||||
else:
|
||||
if hasattr(middleware, "process_response"):
|
||||
return middleware.process_response(request, response)
|
||||
return response
|
||||
|
||||
if iscoroutinefunction(view_func):
|
||||
|
||||
async def _view_wrapper(request, *args, **kwargs):
|
||||
result = _pre_process_request(request, *args, **kwargs)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
try:
|
||||
response = await view_func(request, *args, **kwargs)
|
||||
except Exception as e:
|
||||
result = _process_exception(request, e)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return _post_process_request(request, response)
|
||||
|
||||
else:
|
||||
|
||||
def _view_wrapper(request, *args, **kwargs):
|
||||
result = _pre_process_request(request, *args, **kwargs)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
try:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
except Exception as e:
|
||||
result = _process_exception(request, e)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return _post_process_request(request, response)
|
||||
|
||||
return wraps(view_func)(_view_wrapper)
|
||||
|
||||
return _decorator
|
||||
|
||||
return _make_decorator
|
||||
|
||||
|
||||
def sync_and_async_middleware(func):
|
||||
"""
|
||||
Mark a middleware factory as returning a hybrid middleware supporting both
|
||||
types of request.
|
||||
"""
|
||||
func.sync_capable = True
|
||||
func.async_capable = True
|
||||
return func
|
||||
|
||||
|
||||
def sync_only_middleware(func):
|
||||
"""
|
||||
Mark a middleware factory as returning a sync middleware.
|
||||
This is the default.
|
||||
"""
|
||||
func.sync_capable = True
|
||||
func.async_capable = False
|
||||
return func
|
||||
|
||||
|
||||
def async_only_middleware(func):
|
||||
"""Mark a middleware factory as returning an async middleware."""
|
||||
func.sync_capable = False
|
||||
func.async_capable = True
|
||||
return func
|
||||
142
.venv/lib/python3.12/site-packages/django/utils/deprecation.py
Normal file
142
.venv/lib/python3.12/site-packages/django/utils/deprecation.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import inspect
|
||||
import warnings
|
||||
|
||||
from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
|
||||
|
||||
|
||||
class RemovedInDjango60Warning(DeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
class RemovedInDjango61Warning(PendingDeprecationWarning):
|
||||
pass
|
||||
|
||||
|
||||
RemovedInNextVersionWarning = RemovedInDjango60Warning
|
||||
RemovedAfterNextVersionWarning = RemovedInDjango61Warning
|
||||
|
||||
|
||||
class warn_about_renamed_method:
|
||||
def __init__(
|
||||
self, class_name, old_method_name, new_method_name, deprecation_warning
|
||||
):
|
||||
self.class_name = class_name
|
||||
self.old_method_name = old_method_name
|
||||
self.new_method_name = new_method_name
|
||||
self.deprecation_warning = deprecation_warning
|
||||
|
||||
def __call__(self, f):
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(
|
||||
"`%s.%s` is deprecated, use `%s` instead."
|
||||
% (self.class_name, self.old_method_name, self.new_method_name),
|
||||
self.deprecation_warning,
|
||||
2,
|
||||
)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class RenameMethodsBase(type):
|
||||
"""
|
||||
Handles the deprecation paths when renaming a method.
|
||||
|
||||
It does the following:
|
||||
1) Define the new method if missing and complain about it.
|
||||
2) Define the old method if missing.
|
||||
3) Complain whenever an old method is called.
|
||||
|
||||
See #15363 for more details.
|
||||
"""
|
||||
|
||||
renamed_methods = ()
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
new_class = super().__new__(cls, name, bases, attrs)
|
||||
|
||||
for base in inspect.getmro(new_class):
|
||||
class_name = base.__name__
|
||||
for renamed_method in cls.renamed_methods:
|
||||
old_method_name = renamed_method[0]
|
||||
old_method = base.__dict__.get(old_method_name)
|
||||
new_method_name = renamed_method[1]
|
||||
new_method = base.__dict__.get(new_method_name)
|
||||
deprecation_warning = renamed_method[2]
|
||||
wrapper = warn_about_renamed_method(class_name, *renamed_method)
|
||||
|
||||
# Define the new method if missing and complain about it
|
||||
if not new_method and old_method:
|
||||
warnings.warn(
|
||||
"`%s.%s` method should be renamed `%s`."
|
||||
% (class_name, old_method_name, new_method_name),
|
||||
deprecation_warning,
|
||||
2,
|
||||
)
|
||||
setattr(base, new_method_name, old_method)
|
||||
setattr(base, old_method_name, wrapper(old_method))
|
||||
|
||||
# Define the old method as a wrapped call to the new method.
|
||||
if not old_method and new_method:
|
||||
setattr(base, old_method_name, wrapper(new_method))
|
||||
|
||||
return new_class
|
||||
|
||||
|
||||
class MiddlewareMixin:
|
||||
sync_capable = True
|
||||
async_capable = True
|
||||
|
||||
def __init__(self, get_response):
|
||||
if get_response is None:
|
||||
raise ValueError("get_response must be provided.")
|
||||
self.get_response = get_response
|
||||
# If get_response is a coroutine function, turns us into async mode so
|
||||
# a thread is not consumed during a whole request.
|
||||
self.async_mode = iscoroutinefunction(self.get_response)
|
||||
if self.async_mode:
|
||||
# Mark the class as async-capable, but do the actual switch inside
|
||||
# __call__ to avoid swapping out dunder methods.
|
||||
markcoroutinefunction(self)
|
||||
super().__init__()
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s get_response=%s>" % (
|
||||
self.__class__.__qualname__,
|
||||
getattr(
|
||||
self.get_response,
|
||||
"__qualname__",
|
||||
self.get_response.__class__.__name__,
|
||||
),
|
||||
)
|
||||
|
||||
def __call__(self, request):
|
||||
# Exit out to async mode, if needed
|
||||
if self.async_mode:
|
||||
return self.__acall__(request)
|
||||
response = None
|
||||
if hasattr(self, "process_request"):
|
||||
response = self.process_request(request)
|
||||
response = response or self.get_response(request)
|
||||
if hasattr(self, "process_response"):
|
||||
response = self.process_response(request, response)
|
||||
return response
|
||||
|
||||
async def __acall__(self, request):
|
||||
"""
|
||||
Async version of __call__ that is swapped in when an async request
|
||||
is running.
|
||||
"""
|
||||
response = None
|
||||
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"):
|
||||
response = await sync_to_async(
|
||||
self.process_response,
|
||||
thread_sensitive=True,
|
||||
)(request, response)
|
||||
return response
|
||||
46
.venv/lib/python3.12/site-packages/django/utils/duration.py
Normal file
46
.venv/lib/python3.12/site-packages/django/utils/duration.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import datetime
|
||||
|
||||
|
||||
def _get_duration_components(duration):
|
||||
days = duration.days
|
||||
seconds = duration.seconds
|
||||
microseconds = duration.microseconds
|
||||
|
||||
minutes = seconds // 60
|
||||
seconds %= 60
|
||||
|
||||
hours = minutes // 60
|
||||
minutes %= 60
|
||||
|
||||
return days, hours, minutes, seconds, microseconds
|
||||
|
||||
|
||||
def duration_string(duration):
|
||||
"""Version of str(timedelta) which is not English specific."""
|
||||
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
|
||||
|
||||
string = "{:02d}:{:02d}:{:02d}".format(hours, minutes, seconds)
|
||||
if days:
|
||||
string = "{} ".format(days) + string
|
||||
if microseconds:
|
||||
string += ".{:06d}".format(microseconds)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
def duration_iso_string(duration):
|
||||
if duration < datetime.timedelta(0):
|
||||
sign = "-"
|
||||
duration *= -1
|
||||
else:
|
||||
sign = ""
|
||||
|
||||
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
|
||||
ms = ".{:06d}".format(microseconds) if microseconds else ""
|
||||
return "{}P{}DT{:02d}H{:02d}M{:02d}{}S".format(
|
||||
sign, days, hours, minutes, seconds, ms
|
||||
)
|
||||
|
||||
|
||||
def duration_microseconds(delta):
|
||||
return (24 * 60 * 60 * delta.days + delta.seconds) * 1000000 + delta.microseconds
|
||||
262
.venv/lib/python3.12/site-packages/django/utils/encoding.py
Normal file
262
.venv/lib/python3.12/site-packages/django/utils/encoding.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import codecs
|
||||
import datetime
|
||||
import locale
|
||||
from decimal import Decimal
|
||||
from types import NoneType
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.utils.functional import Promise
|
||||
|
||||
|
||||
class DjangoUnicodeDecodeError(UnicodeDecodeError):
|
||||
def __str__(self):
|
||||
return "%s. You passed in %r (%s)" % (
|
||||
super().__str__(),
|
||||
self.object,
|
||||
type(self.object),
|
||||
)
|
||||
|
||||
|
||||
def smart_str(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
"""
|
||||
Return a string representing 's'. Treat bytestrings using the 'encoding'
|
||||
codec.
|
||||
|
||||
If strings_only is True, don't convert (some) non-string-like objects.
|
||||
"""
|
||||
if isinstance(s, Promise):
|
||||
# The input is the result of a gettext_lazy() call.
|
||||
return s
|
||||
return force_str(s, encoding, strings_only, errors)
|
||||
|
||||
|
||||
_PROTECTED_TYPES = (
|
||||
NoneType,
|
||||
int,
|
||||
float,
|
||||
Decimal,
|
||||
datetime.datetime,
|
||||
datetime.date,
|
||||
datetime.time,
|
||||
)
|
||||
|
||||
|
||||
def is_protected_type(obj):
|
||||
"""Determine if the object instance is of a protected type.
|
||||
|
||||
Objects of protected types are preserved as-is when passed to
|
||||
force_str(strings_only=True).
|
||||
"""
|
||||
return isinstance(obj, _PROTECTED_TYPES)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
If strings_only is True, don't convert (some) non-string-like objects.
|
||||
"""
|
||||
# Handle the common case first for performance reasons.
|
||||
if issubclass(type(s), str):
|
||||
return s
|
||||
if strings_only and is_protected_type(s):
|
||||
return s
|
||||
try:
|
||||
if isinstance(s, bytes):
|
||||
s = str(s, encoding, errors)
|
||||
else:
|
||||
s = str(s)
|
||||
except UnicodeDecodeError as e:
|
||||
raise DjangoUnicodeDecodeError(*e.args) from None
|
||||
return s
|
||||
|
||||
|
||||
def smart_bytes(s, encoding="utf-8", strings_only=False, errors="strict"):
|
||||
"""
|
||||
Return a bytestring version of 's', encoded as specified in 'encoding'.
|
||||
|
||||
If strings_only is True, don't convert (some) non-string-like objects.
|
||||
"""
|
||||
if isinstance(s, Promise):
|
||||
# The input is the result of a gettext_lazy() call.
|
||||
return s
|
||||
return force_bytes(s, encoding, strings_only, errors)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
If strings_only is True, don't convert (some) non-string-like objects.
|
||||
"""
|
||||
# Handle the common case first for performance reasons.
|
||||
if isinstance(s, bytes):
|
||||
if encoding == "utf-8":
|
||||
return s
|
||||
else:
|
||||
return s.decode("utf-8", errors).encode(encoding, errors)
|
||||
if strings_only and is_protected_type(s):
|
||||
return s
|
||||
if isinstance(s, memoryview):
|
||||
return bytes(s)
|
||||
return str(s).encode(encoding, errors)
|
||||
|
||||
|
||||
def iri_to_uri(iri):
|
||||
"""
|
||||
Convert an Internationalized Resource Identifier (IRI) portion to a URI
|
||||
portion that is suitable for inclusion in a URL.
|
||||
|
||||
This is the algorithm from RFC 3987 Section 3.1, slightly simplified since
|
||||
the input is assumed to be a string rather than an arbitrary byte stream.
|
||||
|
||||
Take an IRI (string or UTF-8 bytes, e.g. '/I ♥ Django/' or
|
||||
b'/I \xe2\x99\xa5 Django/') and return a string containing the encoded
|
||||
result with ASCII chars only (e.g. '/I%20%E2%99%A5%20Django/').
|
||||
"""
|
||||
# The list of safe characters here is constructed from the "reserved" and
|
||||
# "unreserved" characters specified in RFC 3986 Sections 2.2 and 2.3:
|
||||
# reserved = gen-delims / sub-delims
|
||||
# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
|
||||
# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||
# / "*" / "+" / "," / ";" / "="
|
||||
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
# Of the unreserved characters, urllib.parse.quote() already considers all
|
||||
# but the ~ safe.
|
||||
# The % character is also added to the list of safe characters here, as the
|
||||
# end of RFC 3987 Section 3.1 specifically mentions that % must not be
|
||||
# converted.
|
||||
if iri is None:
|
||||
return iri
|
||||
elif isinstance(iri, Promise):
|
||||
iri = str(iri)
|
||||
return quote(iri, safe="/#%[]=:;$&()+,!?*@'~")
|
||||
|
||||
|
||||
# List of byte values that uri_to_iri() decodes from percent encoding.
|
||||
# First, the unreserved characters from RFC 3986:
|
||||
_ascii_ranges = [[45, 46, 95, 126], range(65, 91), range(97, 123)]
|
||||
_hextobyte = {
|
||||
(fmt % char).encode(): bytes((char,))
|
||||
for ascii_range in _ascii_ranges
|
||||
for char in ascii_range
|
||||
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}
|
||||
)
|
||||
|
||||
|
||||
def uri_to_iri(uri):
|
||||
"""
|
||||
Convert a Uniform Resource Identifier(URI) into an Internationalized
|
||||
Resource Identifier(IRI).
|
||||
|
||||
This is the algorithm from RFC 3987 Section 3.2, excluding step 4.
|
||||
|
||||
Take an URI in ASCII bytes (e.g. '/I%20%E2%99%A5%20Django/') and return
|
||||
a string containing the encoded result (e.g. '/I%20♥%20Django/').
|
||||
"""
|
||||
if uri is None:
|
||||
return uri
|
||||
uri = force_bytes(uri)
|
||||
# Fast selective unquote: 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"%")
|
||||
if len(bits) == 1:
|
||||
iri = uri
|
||||
else:
|
||||
parts = [bits[0]]
|
||||
append = parts.append
|
||||
hextobyte = _hextobyte
|
||||
for item in bits[1:]:
|
||||
hex = item[:2]
|
||||
if hex in hextobyte:
|
||||
append(hextobyte[item[:2]])
|
||||
append(item[2:])
|
||||
else:
|
||||
append(b"%")
|
||||
append(item)
|
||||
iri = b"".join(parts)
|
||||
return repercent_broken_unicode(iri).decode()
|
||||
|
||||
|
||||
def escape_uri_path(path):
|
||||
"""
|
||||
Escape the unsafe characters from the path portion of a Uniform Resource
|
||||
Identifier (URI).
|
||||
"""
|
||||
# These are the "reserved" and "unreserved" characters specified in RFC
|
||||
# 3986 Sections 2.2 and 2.3:
|
||||
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
|
||||
# unreserved = alphanum | mark
|
||||
# mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
|
||||
# The list of safe characters here is constructed subtracting ";", "=",
|
||||
# and "?" according to RFC 3986 Section 3.3.
|
||||
# The reason for not subtracting and escaping "/" is that we are escaping
|
||||
# the entire path, not a path segment.
|
||||
return quote(path, safe="/:@&+$,-_.!~*'()")
|
||||
|
||||
|
||||
def punycode(domain):
|
||||
"""Return the Punycode of the given domain if it's non-ASCII."""
|
||||
return domain.encode("idna").decode("ascii")
|
||||
|
||||
|
||||
def repercent_broken_unicode(path):
|
||||
"""
|
||||
As per RFC 3987 Section 3.2, step three of converting a URI into an IRI,
|
||||
repercent-encode any octet produced that is not part of a strictly legal
|
||||
UTF-8 octet sequence.
|
||||
"""
|
||||
changed_parts = []
|
||||
while True:
|
||||
try:
|
||||
path.decode()
|
||||
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"/#%[]=:;$&()+,!?*@'~")
|
||||
changed_parts.append(path[: e.start] + repercent.encode())
|
||||
path = path[e.end :]
|
||||
else:
|
||||
return b"".join(changed_parts) + path
|
||||
|
||||
|
||||
def filepath_to_uri(path):
|
||||
"""Convert a file system path to a URI portion that is suitable for
|
||||
inclusion in a URL.
|
||||
|
||||
Encode certain chars that would normally be recognized as special chars
|
||||
for URIs. Do not encode the ' character, as it is a valid character
|
||||
within URIs. See the encodeURIComponent() JavaScript function for details.
|
||||
"""
|
||||
if path is None:
|
||||
return path
|
||||
# I know about `os.sep` and `os.altsep` but I want to leave
|
||||
# some flexibility for hardcoding separators.
|
||||
return quote(str(path).replace("\\", "/"), safe="/~!*()'")
|
||||
|
||||
|
||||
def get_system_encoding():
|
||||
"""
|
||||
The encoding for the character type functions. Fallback to 'ascii' if the
|
||||
#encoding is unsupported by Python or could not be determined. See tickets
|
||||
#10335 and #5846.
|
||||
"""
|
||||
try:
|
||||
encoding = locale.getlocale()[1] or "ascii"
|
||||
codecs.lookup(encoding)
|
||||
except Exception:
|
||||
encoding = "ascii"
|
||||
return encoding
|
||||
|
||||
|
||||
DEFAULT_LOCALE_ENCODING = get_system_encoding()
|
||||
522
.venv/lib/python3.12/site-packages/django/utils/feedgenerator.py
Normal file
522
.venv/lib/python3.12/site-packages/django/utils/feedgenerator.py
Normal file
@@ -0,0 +1,522 @@
|
||||
"""
|
||||
Syndication feed generation library -- used for generating RSS, etc.
|
||||
|
||||
Sample usage:
|
||||
|
||||
>>> from django.utils import feedgenerator
|
||||
>>> 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.",
|
||||
... language="en",
|
||||
... )
|
||||
>>> feed.add_item(
|
||||
... title="Hello",
|
||||
... link="http://www.holovaty.com/test/",
|
||||
... description="Testing."
|
||||
... )
|
||||
>>> with open('test.rss', 'w') as fp:
|
||||
... feed.write(fp, 'utf-8')
|
||||
|
||||
For definitions of the different versions of RSS, see:
|
||||
https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import email
|
||||
import mimetypes
|
||||
from io import StringIO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.utils.encoding import iri_to_uri
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
|
||||
|
||||
def rfc2822_date(date):
|
||||
if not isinstance(date, datetime.datetime):
|
||||
date = datetime.datetime.combine(date, datetime.time())
|
||||
return email.utils.format_datetime(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 "")
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
bits = urlparse(url)
|
||||
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)
|
||||
|
||||
|
||||
def _guess_stylesheet_mimetype(url):
|
||||
"""
|
||||
Return the given stylesheet's mimetype tuple, using a slightly custom
|
||||
version of Python's mimetypes.guess_type().
|
||||
"""
|
||||
mimetypedb = mimetypes.MimeTypes()
|
||||
|
||||
# The official mimetype for XSLT files is technically `application/xslt+xml`
|
||||
# but as of 2024 almost no browser supports that (they all expect text/xsl).
|
||||
# On top of that, windows seems to assume that the type for xsl is text/xml.
|
||||
mimetypedb.readfp(StringIO("text/xsl\txsl\ntext/xsl\txslt"))
|
||||
|
||||
return mimetypedb.guess_type(url)
|
||||
|
||||
|
||||
class Stylesheet:
|
||||
"""An RSS stylesheet"""
|
||||
|
||||
def __init__(self, url, mimetype="", media="screen"):
|
||||
self._url = url
|
||||
self._mimetype = mimetype
|
||||
self.media = media
|
||||
|
||||
# Using a property to delay the evaluation of self._url as late as possible
|
||||
# in case of a lazy object (like reverse_lazy(...) for example).
|
||||
@property
|
||||
def url(self):
|
||||
return iri_to_uri(self._url)
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
if self._mimetype == "":
|
||||
return _guess_stylesheet_mimetype(self.url)[0]
|
||||
return self._mimetype
|
||||
|
||||
def __str__(self):
|
||||
data = [f'href="{self.url}"']
|
||||
if self.mimetype is not None:
|
||||
data.append(f'type="{self.mimetype}"')
|
||||
if self.media is not None:
|
||||
data.append(f'media="{self.media}"')
|
||||
return " ".join(data)
|
||||
|
||||
def __repr__(self):
|
||||
return repr((self.url, self.mimetype, self.media))
|
||||
|
||||
|
||||
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,
|
||||
stylesheets=None,
|
||||
**kwargs,
|
||||
):
|
||||
def to_str(s):
|
||||
return str(s) if s is not None else s
|
||||
|
||||
def to_stylesheet(s):
|
||||
return s if isinstance(s, Stylesheet) else Stylesheet(s)
|
||||
|
||||
categories = categories and [str(c) for c in categories]
|
||||
|
||||
if stylesheets is not None:
|
||||
if isinstance(stylesheets, (Stylesheet, str)):
|
||||
raise TypeError(
|
||||
f"stylesheets should be a list, not {stylesheets.__class__}"
|
||||
)
|
||||
stylesheets = [to_stylesheet(s) for s in stylesheets]
|
||||
|
||||
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),
|
||||
"stylesheets": stylesheets,
|
||||
**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,
|
||||
):
|
||||
"""
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
def num_items(self):
|
||||
return len(self.items)
|
||||
|
||||
def root_attributes(self):
|
||||
"""
|
||||
Return extra attributes to place on the root (i.e. feed/channel) element.
|
||||
Called from write().
|
||||
"""
|
||||
return {}
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
"""
|
||||
Add elements in the root (i.e. feed/channel) element. Called
|
||||
from write().
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_stylesheets(self, handler):
|
||||
"""
|
||||
Add stylesheet(s) to the feed. Called from write().
|
||||
"""
|
||||
pass
|
||||
|
||||
def item_attributes(self, item):
|
||||
"""
|
||||
Return extra attributes to place on each item (i.e. item/entry) element.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
"""
|
||||
Add elements on each item (i.e. item/entry) element.
|
||||
"""
|
||||
pass
|
||||
|
||||
def write(self, outfile, encoding):
|
||||
"""
|
||||
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"
|
||||
)
|
||||
|
||||
def writeString(self, encoding):
|
||||
"""
|
||||
Return the feed in the given encoding as a string.
|
||||
"""
|
||||
s = StringIO()
|
||||
self.write(s, encoding)
|
||||
return s.getvalue()
|
||||
|
||||
def latest_post_date(self):
|
||||
"""
|
||||
Return the latest item's pubdate or updateddate. If no items
|
||||
have either of these attributes this return the current UTC date/time.
|
||||
"""
|
||||
latest_date = None
|
||||
date_keys = ("updateddate", "pubdate")
|
||||
|
||||
for item in self.items:
|
||||
for date_key in date_keys:
|
||||
item_date = item.get(date_key)
|
||||
if item_date:
|
||||
if latest_date is None or item_date > latest_date:
|
||||
latest_date = item_date
|
||||
|
||||
return latest_date or datetime.datetime.now(tz=datetime.timezone.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
|
||||
self.url = iri_to_uri(url)
|
||||
|
||||
|
||||
class RssFeed(SyndicationFeed):
|
||||
content_type = "application/rss+xml; charset=utf-8"
|
||||
|
||||
def write(self, outfile, encoding):
|
||||
handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
|
||||
handler.startDocument()
|
||||
# Any stylesheet must come after the start of the document but before any tag.
|
||||
# https://www.w3.org/Style/styling-XML.en.html
|
||||
self.add_stylesheets(handler)
|
||||
handler.startElement("rss", self.rss_attributes())
|
||||
handler.startElement("channel", self.root_attributes())
|
||||
self.add_root_elements(handler)
|
||||
self.write_items(handler)
|
||||
self.endChannelElement(handler)
|
||||
handler.endElement("rss")
|
||||
|
||||
def rss_attributes(self):
|
||||
return {
|
||||
"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))
|
||||
self.add_item_elements(handler, item)
|
||||
handler.endElement("item")
|
||||
|
||||
def add_stylesheets(self, handler):
|
||||
for stylesheet in self.feed["stylesheets"] or []:
|
||||
handler.processingInstruction("xml-stylesheet", stylesheet)
|
||||
|
||||
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("category", cat)
|
||||
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"])
|
||||
|
||||
def endChannelElement(self, handler):
|
||||
handler.endElement("channel")
|
||||
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
class Rss201rev2Feed(RssFeed):
|
||||
# Spec: https://cyber.harvard.edu/rss/rss.html
|
||||
_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"])
|
||||
|
||||
# Author information.
|
||||
if item["author_name"] and item["author_email"]:
|
||||
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/"},
|
||||
)
|
||||
|
||||
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"])
|
||||
|
||||
# Enclosure.
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
# 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"
|
||||
ns = "http://www.w3.org/2005/Atom"
|
||||
|
||||
def write(self, outfile, encoding):
|
||||
handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
|
||||
handler.startDocument()
|
||||
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"]}
|
||||
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("updated", rfc3339_date(self.latest_post_date()))
|
||||
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.endElement("author")
|
||||
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"])
|
||||
|
||||
def write_items(self, handler):
|
||||
for item in self.items:
|
||||
handler.startElement("entry", self.item_attributes(item))
|
||||
self.add_item_elements(handler, item)
|
||||
handler.endElement("entry")
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
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["updateddate"] is not None:
|
||||
handler.addQuickElement("updated", rfc3339_date(item["updateddate"]))
|
||||
|
||||
# Author information.
|
||||
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.endElement("author")
|
||||
|
||||
# Unique ID.
|
||||
if item["unique_id"] is not None:
|
||||
unique_id = item["unique_id"]
|
||||
else:
|
||||
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"})
|
||||
|
||||
# Enclosures.
|
||||
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"]:
|
||||
handler.addQuickElement("category", "", {"term": cat})
|
||||
|
||||
# Rights.
|
||||
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
|
||||
# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
|
||||
DefaultFeed = Rss201rev2Feed
|
||||
305
.venv/lib/python3.12/site-packages/django/utils/formats.py
Normal file
305
.venv/lib/python3.12/site-packages/django/utils/formats.py
Normal file
@@ -0,0 +1,305 @@
|
||||
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.functional import lazy
|
||||
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
|
||||
# repeatedly.
|
||||
_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",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
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():
|
||||
"""Clear any cached formats.
|
||||
|
||||
This method is provided primarily for testing purposes,
|
||||
so that the effects of cached formats can be removed.
|
||||
"""
|
||||
global _format_cache, _format_modules_cache
|
||||
_format_cache = {}
|
||||
_format_modules_cache = {}
|
||||
|
||||
|
||||
def iter_format_modules(lang, format_module_path=None):
|
||||
"""Find format modules."""
|
||||
if not check_for_language(lang):
|
||||
return
|
||||
|
||||
if format_module_path is None:
|
||||
format_module_path = settings.FORMAT_MODULE_PATH
|
||||
|
||||
format_locations = []
|
||||
if format_module_path:
|
||||
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")
|
||||
locale = to_locale(lang)
|
||||
locales = [locale]
|
||||
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))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def get_format_modules(lang=None):
|
||||
"""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]
|
||||
|
||||
|
||||
def get_format(format_type, lang=None, use_l10n=None):
|
||||
"""
|
||||
For a specific format type, return the format for the current
|
||||
language (locale). Default to the format in the settings.
|
||||
format_type is the name of the format, e.g. 'DATE_FORMAT'.
|
||||
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), otherwise it's always localized.
|
||||
"""
|
||||
if use_l10n is None:
|
||||
use_l10n = True
|
||||
if use_l10n and lang is None:
|
||||
lang = get_language()
|
||||
format_type = str(format_type) # format_type may be lazy.
|
||||
cache_key = (format_type, lang)
|
||||
try:
|
||||
return _format_cache[cache_key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# The requested format_type has not been cached yet. Try to find it in any
|
||||
# of the format_modules for the given lang if l10n is enabled. If it's not
|
||||
# there or if l10n is disabled, fall back to the project settings.
|
||||
val = None
|
||||
if use_l10n:
|
||||
for module in get_format_modules(lang):
|
||||
val = getattr(module, format_type, None)
|
||||
if val is not None:
|
||||
break
|
||||
if val is None:
|
||||
if format_type not in FORMAT_SETTINGS:
|
||||
return format_type
|
||||
val = getattr(settings, format_type)
|
||||
elif format_type in ISO_INPUT_FORMATS:
|
||||
# If a list of input formats from one of the format_modules was
|
||||
# retrieved, make sure the ISO_INPUT_FORMATS are in this list.
|
||||
val = list(val)
|
||||
for iso_input in ISO_INPUT_FORMATS.get(format_type, ()):
|
||||
if iso_input not in val:
|
||||
val.append(iso_input)
|
||||
_format_cache[cache_key] = val
|
||||
return val
|
||||
|
||||
|
||||
get_format_lazy = lazy(get_format, str, list, tuple)
|
||||
|
||||
|
||||
def date_format(value, format=None, use_l10n=None):
|
||||
"""
|
||||
Format a datetime.date or datetime.datetime object using a
|
||||
localizable format.
|
||||
|
||||
If use_l10n is provided and is not None, that will force the value to
|
||||
be localized (or not), otherwise it's always localized.
|
||||
"""
|
||||
return dateformat.format(
|
||||
value, get_format(format or "DATE_FORMAT", use_l10n=use_l10n)
|
||||
)
|
||||
|
||||
|
||||
def time_format(value, format=None, use_l10n=None):
|
||||
"""
|
||||
Format a datetime.time object using a localizable format.
|
||||
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), otherwise it's always localized.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Format a numeric value using localization settings.
|
||||
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), otherwise it's always localized.
|
||||
"""
|
||||
if use_l10n is None:
|
||||
use_l10n = True
|
||||
lang = get_language() if use_l10n else None
|
||||
return numberformat.format(
|
||||
value,
|
||||
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),
|
||||
force_grouping=force_grouping,
|
||||
use_l10n=use_l10n,
|
||||
)
|
||||
|
||||
|
||||
def localize(value, use_l10n=None):
|
||||
"""
|
||||
Check if value is a localizable type (date, number...) and return it
|
||||
formatted as a string using current locale format.
|
||||
|
||||
If use_l10n is provided and is not None, it forces the value to
|
||||
be localized (or not), otherwise it's always localized.
|
||||
"""
|
||||
if isinstance(value, str): # Handle strings first for performance reasons.
|
||||
return value
|
||||
elif isinstance(value, bool): # Make sure booleans don't get treated as numbers
|
||||
return str(value)
|
||||
elif isinstance(value, (decimal.Decimal, float, int)):
|
||||
if use_l10n is False:
|
||||
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)
|
||||
elif isinstance(value, datetime.date):
|
||||
return date_format(value, use_l10n=use_l10n)
|
||||
elif isinstance(value, datetime.time):
|
||||
return time_format(value, use_l10n=use_l10n)
|
||||
return value
|
||||
|
||||
|
||||
def localize_input(value, default=None):
|
||||
"""
|
||||
Check if an input value is a localizable type and return it
|
||||
formatted with the appropriate formatting string of the current locale.
|
||||
"""
|
||||
if isinstance(value, str): # Handle strings first for performance reasons.
|
||||
return value
|
||||
elif isinstance(value, bool): # Don't treat booleans as numbers.
|
||||
return str(value)
|
||||
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)
|
||||
return value.strftime(format)
|
||||
elif isinstance(value, datetime.date):
|
||||
format = default or get_format("DATE_INPUT_FORMATS")[0]
|
||||
format = sanitize_strftime_format(format)
|
||||
return value.strftime(format)
|
||||
elif isinstance(value, datetime.time):
|
||||
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
|
||||
thousand separator setting. Used with form field input.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
parts = []
|
||||
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).
|
||||
pass
|
||||
else:
|
||||
for replacement in {
|
||||
thousand_sep,
|
||||
unicodedata.normalize("NFKD", thousand_sep),
|
||||
}:
|
||||
value = value.replace(replacement, "")
|
||||
parts.append(value)
|
||||
value = ".".join(reversed(parts))
|
||||
return value
|
||||
451
.venv/lib/python3.12/site-packages/django/utils/functional.py
Normal file
451
.venv/lib/python3.12/site-packages/django/utils/functional.py
Normal file
@@ -0,0 +1,451 @@
|
||||
import copy
|
||||
import itertools
|
||||
import operator
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class cached_property:
|
||||
"""
|
||||
Decorator that converts a method with a single self argument into a
|
||||
property cached on the instance.
|
||||
|
||||
A cached property can be made out of an existing method:
|
||||
(e.g. ``url = cached_property(get_absolute_url)``).
|
||||
"""
|
||||
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def func(instance):
|
||||
raise TypeError(
|
||||
"Cannot use cached_property instance without calling "
|
||||
"__set_name__() on it."
|
||||
)
|
||||
|
||||
def __init__(self, func):
|
||||
self.real_func = func
|
||||
self.__doc__ = getattr(func, "__doc__")
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if self.name is None:
|
||||
self.name = name
|
||||
self.func = self.real_func
|
||||
elif name != self.name:
|
||||
raise TypeError(
|
||||
"Cannot assign the same cached_property to two different names "
|
||||
"(%r and %r)." % (self.name, name)
|
||||
)
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Call the function and put the return value in instance.__dict__ so that
|
||||
subsequent attribute access on the instance returns the cached value
|
||||
instead of calling cached_property.__get__().
|
||||
"""
|
||||
if instance is None:
|
||||
return self
|
||||
res = instance.__dict__[self.name] = self.func(instance)
|
||||
return res
|
||||
|
||||
|
||||
class 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
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
return self.fget(cls)
|
||||
|
||||
def getter(self, method):
|
||||
self.fget = method
|
||||
return self
|
||||
|
||||
|
||||
class Promise:
|
||||
"""
|
||||
Base class for the proxy class created in the closure of the lazy function.
|
||||
It's used to recognize promises in code.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def lazy(func, *resultclasses):
|
||||
"""
|
||||
Turn any callable into a lazy evaluated callable. result classes or types
|
||||
is required -- at least one is needed so that the automatic forcing of
|
||||
the lazy evaluation code is triggered. Results are not memoized; the
|
||||
function is evaluated on every access.
|
||||
"""
|
||||
|
||||
class __proxy__(Promise):
|
||||
"""
|
||||
Encapsulate a function call and act as a proxy for methods that are
|
||||
called on the result of that function. The function is not evaluated
|
||||
until one of the methods on the result is called.
|
||||
"""
|
||||
|
||||
def __init__(self, args, kw):
|
||||
self._args = args
|
||||
self._kw = kw
|
||||
|
||||
def __reduce__(self):
|
||||
return (
|
||||
_lazy_proxy_unpickle,
|
||||
(func, self._args, self._kw) + resultclasses,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
# Instances of this class are effectively immutable. It's just a
|
||||
# collection of functions. So we don't need to do anything
|
||||
# complicated for copying.
|
||||
memo[id(self)] = self
|
||||
return self
|
||||
|
||||
def __cast(self):
|
||||
return func(*self._args, **self._kw)
|
||||
|
||||
# Explicitly wrap methods which are defined on object and hence would
|
||||
# not have been overloaded by the loop over resultclasses below.
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__cast())
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__cast())
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() == other
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() != other
|
||||
|
||||
def __lt__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() < other
|
||||
|
||||
def __le__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() <= other
|
||||
|
||||
def __gt__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() > other
|
||||
|
||||
def __ge__(self, other):
|
||||
if isinstance(other, Promise):
|
||||
other = other.__cast()
|
||||
return self.__cast() >= other
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__cast())
|
||||
|
||||
def __format__(self, format_spec):
|
||||
return format(self.__cast(), format_spec)
|
||||
|
||||
# Explicitly wrap methods which are required for certain operations on
|
||||
# int/str objects to function correctly.
|
||||
|
||||
def __add__(self, other):
|
||||
return self.__cast() + other
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + self.__cast()
|
||||
|
||||
def __mod__(self, other):
|
||||
return self.__cast() % other
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.__cast() * other
|
||||
|
||||
# Add wrappers for all methods from resultclasses which haven't been
|
||||
# wrapped explicitly above.
|
||||
for resultclass in resultclasses:
|
||||
for type_ in resultclass.mro():
|
||||
for method_name in type_.__dict__:
|
||||
# All __promise__ return the same wrapper method, they look up
|
||||
# the correct implementation when called.
|
||||
if hasattr(__proxy__, method_name):
|
||||
continue
|
||||
|
||||
# Builds a wrapper around some method. Pass method_name to
|
||||
# avoid issues due to late binding.
|
||||
def __wrapper__(self, *args, __method_name=method_name, **kw):
|
||||
# Automatically triggers the evaluation of a lazy value and
|
||||
# applies the given method of the result type.
|
||||
result = func(*self._args, **self._kw)
|
||||
return getattr(result, __method_name)(*args, **kw)
|
||||
|
||||
setattr(__proxy__, method_name, __wrapper__)
|
||||
|
||||
@wraps(func)
|
||||
def __wrapper__(*args, **kw):
|
||||
# Creates the proxy object, instead of the actual value.
|
||||
return __proxy__(args, kw)
|
||||
|
||||
return __wrapper__
|
||||
|
||||
|
||||
def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
|
||||
return lazy(func, *resultclasses)(*args, **kwargs)
|
||||
|
||||
|
||||
def lazystr(text):
|
||||
"""
|
||||
Shortcut for the common case of a lazy callable that returns str.
|
||||
"""
|
||||
return lazy(str, str)(text)
|
||||
|
||||
|
||||
def keep_lazy(*resultclasses):
|
||||
"""
|
||||
A decorator that allows a function to be called with one or more lazy
|
||||
arguments. If none of the args are lazy, the function is evaluated
|
||||
immediately, otherwise a __proxy__ is returned that will evaluate the
|
||||
function when needed.
|
||||
"""
|
||||
if not resultclasses:
|
||||
raise TypeError("You must pass at least one argument to keep_lazy().")
|
||||
|
||||
def decorator(func):
|
||||
lazy_func = lazy(func, *resultclasses)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if any(
|
||||
isinstance(arg, Promise)
|
||||
for arg in itertools.chain(args, kwargs.values())
|
||||
):
|
||||
return lazy_func(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def keep_lazy_text(func):
|
||||
"""
|
||||
A decorator for functions that accept lazy arguments and return text.
|
||||
"""
|
||||
return keep_lazy(str)(func)
|
||||
|
||||
|
||||
empty = object()
|
||||
|
||||
|
||||
def new_method_proxy(func):
|
||||
def inner(self, *args):
|
||||
if (_wrapped := self._wrapped) is empty:
|
||||
self._setup()
|
||||
_wrapped = self._wrapped
|
||||
return func(_wrapped, *args)
|
||||
|
||||
inner._mask_wrapped = False
|
||||
return inner
|
||||
|
||||
|
||||
class LazyObject:
|
||||
"""
|
||||
A wrapper for another class that can be used to delay instantiation of the
|
||||
wrapped class.
|
||||
|
||||
By subclassing, you have the opportunity to intercept and alter the
|
||||
instantiation. If you don't need to do that, use SimpleLazyObject.
|
||||
"""
|
||||
|
||||
# Avoid infinite recursion when tracing __init__ (#19456).
|
||||
_wrapped = None
|
||||
|
||||
def __init__(self):
|
||||
# Note: if a subclass overrides __init__(), it will likely need to
|
||||
# override __copy__() and __deepcopy__() as well.
|
||||
self._wrapped = empty
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name == "_wrapped":
|
||||
# Avoid recursion when getting wrapped object.
|
||||
return super().__getattribute__(name)
|
||||
value = super().__getattribute__(name)
|
||||
# If attribute is a proxy method, raise an AttributeError to call
|
||||
# __getattr__() and use the wrapped object method.
|
||||
if not getattr(value, "_mask_wrapped", True):
|
||||
raise AttributeError
|
||||
return value
|
||||
|
||||
__getattr__ = new_method_proxy(getattr)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name == "_wrapped":
|
||||
# Assign to __dict__ to avoid infinite __setattr__ loops.
|
||||
self.__dict__["_wrapped"] = value
|
||||
else:
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
setattr(self._wrapped, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
if name == "_wrapped":
|
||||
raise TypeError("can't delete _wrapped.")
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
delattr(self._wrapped, name)
|
||||
|
||||
def _setup(self):
|
||||
"""
|
||||
Must be implemented by subclasses to initialize the wrapped object.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of LazyObject must provide a _setup() method"
|
||||
)
|
||||
|
||||
# Because we have messed with __class__ below, we confuse pickle as to what
|
||||
# class we are pickling. We're going to have to initialize the wrapped
|
||||
# object to successfully pickle it, so we might as well just pickle the
|
||||
# wrapped object since they're supposed to act the same way.
|
||||
#
|
||||
# Unfortunately, if we try to simply act like the wrapped object, the ruse
|
||||
# will break down when pickle gets our id(). Thus we end up with pickle
|
||||
# thinking, in effect, that we are a distinct object from the wrapped
|
||||
# object, but with the same __dict__. This can cause problems (see #25389).
|
||||
#
|
||||
# So instead, we define our own __reduce__ method and custom unpickler. We
|
||||
# pickle the wrapped object as the unpickler's argument, so that pickle
|
||||
# will pickle it normally, and then the unpickler simply returns its
|
||||
# argument.
|
||||
def __reduce__(self):
|
||||
if self._wrapped is empty:
|
||||
self._setup()
|
||||
return (unpickle_lazyobject, (self._wrapped,))
|
||||
|
||||
def __copy__(self):
|
||||
if self._wrapped is empty:
|
||||
# If uninitialized, copy the wrapper. Use type(self), not
|
||||
# self.__class__, because the latter is proxied.
|
||||
return type(self)()
|
||||
else:
|
||||
# If initialized, return a copy of the wrapped object.
|
||||
return copy.copy(self._wrapped)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
if self._wrapped is empty:
|
||||
# We have to use type(self), not self.__class__, because the
|
||||
# latter is proxied.
|
||||
result = type(self)()
|
||||
memo[id(self)] = result
|
||||
return result
|
||||
return copy.deepcopy(self._wrapped, memo)
|
||||
|
||||
__bytes__ = new_method_proxy(bytes)
|
||||
__str__ = new_method_proxy(str)
|
||||
__bool__ = new_method_proxy(bool)
|
||||
|
||||
# Introspection support
|
||||
__dir__ = new_method_proxy(dir)
|
||||
|
||||
# Need to pretend to be the wrapped class, for the sake of objects that
|
||||
# care about this (especially in equality tests)
|
||||
__class__ = property(new_method_proxy(operator.attrgetter("__class__")))
|
||||
__eq__ = new_method_proxy(operator.eq)
|
||||
__lt__ = new_method_proxy(operator.lt)
|
||||
__gt__ = new_method_proxy(operator.gt)
|
||||
__ne__ = new_method_proxy(operator.ne)
|
||||
__hash__ = new_method_proxy(hash)
|
||||
|
||||
# List/Tuple/Dictionary methods support
|
||||
__getitem__ = new_method_proxy(operator.getitem)
|
||||
__setitem__ = new_method_proxy(operator.setitem)
|
||||
__delitem__ = new_method_proxy(operator.delitem)
|
||||
__iter__ = new_method_proxy(iter)
|
||||
__len__ = new_method_proxy(len)
|
||||
__contains__ = new_method_proxy(operator.contains)
|
||||
|
||||
|
||||
def unpickle_lazyobject(wrapped):
|
||||
"""
|
||||
Used to unpickle lazy objects. Just return its argument, which will be the
|
||||
wrapped object.
|
||||
"""
|
||||
return wrapped
|
||||
|
||||
|
||||
class SimpleLazyObject(LazyObject):
|
||||
"""
|
||||
A lazy object initialized from any function.
|
||||
|
||||
Designed for compound objects of unknown type. For builtins or objects of
|
||||
known type, use django.utils.functional.lazy.
|
||||
"""
|
||||
|
||||
def __init__(self, func):
|
||||
"""
|
||||
Pass in a callable that returns the object to be wrapped.
|
||||
|
||||
If copies are made of the resulting SimpleLazyObject, which can happen
|
||||
in various circumstances within Django, then you must ensure that the
|
||||
callable can be safely run more than once and will return the same
|
||||
value.
|
||||
"""
|
||||
self.__dict__["_setupfunc"] = func
|
||||
super().__init__()
|
||||
|
||||
def _setup(self):
|
||||
self._wrapped = self._setupfunc()
|
||||
|
||||
# Return a meaningful representation of the lazy object for debugging
|
||||
# without evaluating the wrapped object.
|
||||
def __repr__(self):
|
||||
if self._wrapped is empty:
|
||||
repr_attr = self._setupfunc
|
||||
else:
|
||||
repr_attr = self._wrapped
|
||||
return "<%s: %r>" % (type(self).__name__, repr_attr)
|
||||
|
||||
def __copy__(self):
|
||||
if self._wrapped is empty:
|
||||
# If uninitialized, copy the wrapper. Use SimpleLazyObject, not
|
||||
# self.__class__, because the latter is proxied.
|
||||
return SimpleLazyObject(self._setupfunc)
|
||||
else:
|
||||
# If initialized, return a copy of the wrapped object.
|
||||
return copy.copy(self._wrapped)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
if self._wrapped is empty:
|
||||
# We have to use SimpleLazyObject, not self.__class__, because the
|
||||
# latter is proxied.
|
||||
result = SimpleLazyObject(self._setupfunc)
|
||||
memo[id(self)] = result
|
||||
return result
|
||||
return copy.deepcopy(self._wrapped, memo)
|
||||
|
||||
__add__ = new_method_proxy(operator.add)
|
||||
|
||||
@new_method_proxy
|
||||
def __radd__(self, other):
|
||||
return other + self
|
||||
|
||||
|
||||
def partition(predicate, values):
|
||||
"""
|
||||
Split the values into two sets, based on the return value of the function
|
||||
(True/False). e.g.:
|
||||
|
||||
>>> partition(lambda x: x > 3, range(5))
|
||||
[0, 1, 2, 3], [4]
|
||||
"""
|
||||
results = ([], [])
|
||||
for item in values:
|
||||
results[predicate(item)].append(item)
|
||||
return results
|
||||
26
.venv/lib/python3.12/site-packages/django/utils/hashable.py
Normal file
26
.venv/lib/python3.12/site-packages/django/utils/hashable.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
def make_hashable(value):
|
||||
"""
|
||||
Attempt to make value hashable or raise a TypeError if it fails.
|
||||
|
||||
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())
|
||||
]
|
||||
)
|
||||
# Try hash to avoid converting a hashable iterable (e.g. string, frozenset)
|
||||
# to a tuple.
|
||||
try:
|
||||
hash(value)
|
||||
except TypeError:
|
||||
if isinstance(value, Iterable):
|
||||
return tuple(map(make_hashable, value))
|
||||
# Non-hashable, non-iterable.
|
||||
raise
|
||||
return value
|
||||
511
.venv/lib/python3.12/site-packages/django/utils/html.py
Normal file
511
.venv/lib/python3.12/site-packages/django/utils/html.py
Normal file
@@ -0,0 +1,511 @@
|
||||
"""HTML utilities suitable for global use."""
|
||||
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Mapping
|
||||
from html.parser import HTMLParser
|
||||
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation, ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.utils.deprecation import RemovedInDjango60Warning
|
||||
from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text
|
||||
from django.utils.http import MAX_URL_LENGTH, RFC3986_GENDELIMS, RFC3986_SUBDELIMS
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.safestring import SafeData, SafeString, mark_safe
|
||||
from django.utils.text import normalize_newlines
|
||||
|
||||
# https://html.spec.whatwg.org/#void-elements
|
||||
VOID_ELEMENTS = frozenset(
|
||||
(
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
# Deprecated tags.
|
||||
"frame",
|
||||
"spacer",
|
||||
)
|
||||
)
|
||||
|
||||
MAX_STRIP_TAGS_DEPTH = 50
|
||||
|
||||
# HTML tag that opens but has no closing ">" after 1k+ chars.
|
||||
long_open_tag_without_closing_re = _lazy_re_compile(r"<[a-zA-Z][^>]{1000,}")
|
||||
|
||||
|
||||
@keep_lazy(SafeString)
|
||||
def escape(text):
|
||||
"""
|
||||
Return the given text with ampersands, quotes and angle brackets encoded
|
||||
for use in HTML.
|
||||
|
||||
Always escape input, even if it's already escaped and marked as such.
|
||||
This may result in double-escaping. If this is a concern, use
|
||||
conditional_escape() instead.
|
||||
"""
|
||||
return SafeString(html.escape(str(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",
|
||||
}
|
||||
|
||||
# Escape every ASCII character with a value less than 32.
|
||||
_js_escapes.update((ord("%c" % z), "\\u%04X" % z) for z in range(32))
|
||||
|
||||
|
||||
@keep_lazy(SafeString)
|
||||
def escapejs(value):
|
||||
"""Hex encode characters for use in JavaScript strings."""
|
||||
return mark_safe(str(value).translate(_js_escapes))
|
||||
|
||||
|
||||
_json_script_escapes = {
|
||||
ord(">"): "\\u003E",
|
||||
ord("<"): "\\u003C",
|
||||
ord("&"): "\\u0026",
|
||||
}
|
||||
|
||||
|
||||
def json_script(value, element_id=None, encoder=None):
|
||||
"""
|
||||
Escape all the HTML/XML special characters with their unicode escapes, so
|
||||
value is safe to be output anywhere except for inside a tag attribute. Wrap
|
||||
the escaped JSON in a script tag.
|
||||
"""
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
json_str = json.dumps(value, cls=encoder or DjangoJSONEncoder).translate(
|
||||
_json_script_escapes
|
||||
)
|
||||
if element_id:
|
||||
template = '<script id="{}" type="application/json">{}</script>'
|
||||
args = (element_id, mark_safe(json_str))
|
||||
else:
|
||||
template = '<script type="application/json">{}</script>'
|
||||
args = (mark_safe(json_str),)
|
||||
return format_html(template, *args)
|
||||
|
||||
|
||||
def conditional_escape(text):
|
||||
"""
|
||||
Similar to escape(), except that it doesn't operate on pre-escaped strings.
|
||||
|
||||
This function relies on the __html__ convention used both by Django's
|
||||
SafeData class and by third-party libraries like markupsafe.
|
||||
"""
|
||||
if isinstance(text, Promise):
|
||||
text = str(text)
|
||||
if hasattr(text, "__html__"):
|
||||
return text.__html__()
|
||||
else:
|
||||
return escape(text)
|
||||
|
||||
|
||||
def format_html(format_string, *args, **kwargs):
|
||||
"""
|
||||
Similar to str.format, but pass all arguments through conditional_escape(),
|
||||
and call mark_safe() on the result. This function should be used instead
|
||||
of str.format or % interpolation to build up small HTML fragments.
|
||||
"""
|
||||
if not (args or kwargs):
|
||||
# RemovedInDjango60Warning: when the deprecation ends, replace with:
|
||||
# raise TypeError("args or kwargs must be provided.")
|
||||
warnings.warn(
|
||||
"Calling format_html() without passing args or kwargs is deprecated.",
|
||||
RemovedInDjango60Warning,
|
||||
stacklevel=2,
|
||||
)
|
||||
args_safe = map(conditional_escape, args)
|
||||
kwargs_safe = {k: conditional_escape(v) for (k, v) in kwargs.items()}
|
||||
return mark_safe(format_string.format(*args_safe, **kwargs_safe))
|
||||
|
||||
|
||||
def format_html_join(sep, format_string, args_generator):
|
||||
"""
|
||||
A wrapper of format_html, for the common case of a group of arguments that
|
||||
need to be formatted using the same format string, and then joined using
|
||||
'sep'. 'sep' is also passed through conditional_escape.
|
||||
|
||||
'args_generator' should be an iterator that returns the sequence of 'args'
|
||||
that will be passed to format_html.
|
||||
|
||||
Example:
|
||||
|
||||
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)
|
||||
if isinstance(args, Mapping)
|
||||
else 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))
|
||||
if autoescape:
|
||||
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)
|
||||
|
||||
|
||||
class MLStripper(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=False)
|
||||
self.reset()
|
||||
self.fed = []
|
||||
|
||||
def handle_data(self, d):
|
||||
self.fed.append(d)
|
||||
|
||||
def handle_entityref(self, name):
|
||||
self.fed.append("&%s;" % name)
|
||||
|
||||
def handle_charref(self, name):
|
||||
self.fed.append("&#%s;" % name)
|
||||
|
||||
def get_data(self):
|
||||
return "".join(self.fed)
|
||||
|
||||
|
||||
def _strip_once(value):
|
||||
"""
|
||||
Internal tag stripping utility used by strip_tags.
|
||||
"""
|
||||
s = MLStripper()
|
||||
s.feed(value)
|
||||
s.close()
|
||||
return s.get_data()
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def strip_tags(value):
|
||||
"""Return the given HTML with all tags stripped."""
|
||||
value = str(value)
|
||||
for long_open_tag in long_open_tag_without_closing_re.finditer(value):
|
||||
if long_open_tag.group().count("<") >= MAX_STRIP_TAGS_DEPTH:
|
||||
raise SuspiciousOperation
|
||||
# Note: in typical case this loop executes _strip_once twice (the second
|
||||
# execution does not remove any more tags).
|
||||
strip_tags_depth = 0
|
||||
while "<" in value and ">" in value:
|
||||
if strip_tags_depth >= MAX_STRIP_TAGS_DEPTH:
|
||||
raise SuspiciousOperation
|
||||
new_value = _strip_once(value)
|
||||
if value.count("<") == new_value.count("<"):
|
||||
# _strip_once wasn't able to detect more tags.
|
||||
break
|
||||
value = new_value
|
||||
strip_tags_depth += 1
|
||||
return 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))
|
||||
|
||||
|
||||
def smart_urlquote(url):
|
||||
"""Quote a URL if it isn't already quoted."""
|
||||
|
||||
def unquote_quote(segment):
|
||||
segment = unquote(segment)
|
||||
# Tilde is part of RFC 3986 Section 2.3 Unreserved Characters,
|
||||
# see also https://bugs.python.org/issue16285
|
||||
return quote(segment, safe=RFC3986_SUBDELIMS + RFC3986_GENDELIMS + "~")
|
||||
|
||||
try:
|
||||
scheme, netloc, path, query, fragment = urlsplit(url)
|
||||
except ValueError:
|
||||
# invalid IPv6 URL (normally square brackets in hostname part).
|
||||
return unquote_quote(url)
|
||||
|
||||
# Handle IDN as percent-encoded UTF-8 octets, per WHATWG URL Specification
|
||||
# section 3.5 and RFC 3986 section 3.2.2. Defer any IDNA to the user agent.
|
||||
# See #36013.
|
||||
netloc = unquote_quote(netloc)
|
||||
|
||||
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)
|
||||
]
|
||||
# urlencode will take care of quoting
|
||||
query = urlencode(query_parts)
|
||||
|
||||
path = unquote_quote(path)
|
||||
fragment = unquote_quote(fragment)
|
||||
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
class CountsDict(dict):
|
||||
def __init__(self, *args, word, **kwargs):
|
||||
super().__init__(*args, *kwargs)
|
||||
self.word = word
|
||||
|
||||
def __missing__(self, key):
|
||||
self[key] = self.word.count(key)
|
||||
return self[key]
|
||||
|
||||
|
||||
class Urlizer:
|
||||
"""
|
||||
Convert any URLs in text into clickable links.
|
||||
|
||||
Work on http://, https://, www. links, and also on links ending in one of
|
||||
the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org).
|
||||
Links can have trailing punctuation (periods, commas, close-parens) and
|
||||
leading punctuation (opening parens) and it'll still do the right thing.
|
||||
"""
|
||||
|
||||
trailing_punctuation_chars = ".,:;!"
|
||||
wrapping_punctuation = [("(", ")"), ("[", "]")]
|
||||
|
||||
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
|
||||
)
|
||||
word_split_re = _lazy_re_compile(r"""([\s<>"']+)""")
|
||||
|
||||
mailto_template = "mailto:{local}@{domain}"
|
||||
url_template = '<a href="{href}"{attrs}>{url}</a>'
|
||||
|
||||
def __call__(self, text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
"""
|
||||
If trim_url_limit is not None, truncate the URLs in the link text
|
||||
longer than this limit to trim_url_limit - 1 characters and append an
|
||||
ellipsis.
|
||||
|
||||
If nofollow is True, give the links a rel="nofollow" attribute.
|
||||
|
||||
If autoescape is True, autoescape the link text and URLs.
|
||||
"""
|
||||
safe_input = isinstance(text, SafeData)
|
||||
|
||||
words = self.word_split_re.split(str(text))
|
||||
local_cache = {}
|
||||
urlized_words = []
|
||||
for word in words:
|
||||
if (urlized_word := local_cache.get(word)) is None:
|
||||
urlized_word = self.handle_word(
|
||||
word,
|
||||
safe_input=safe_input,
|
||||
trim_url_limit=trim_url_limit,
|
||||
nofollow=nofollow,
|
||||
autoescape=autoescape,
|
||||
)
|
||||
local_cache[word] = urlized_word
|
||||
urlized_words.append(urlized_word)
|
||||
return "".join(urlized_words)
|
||||
|
||||
def handle_word(
|
||||
self,
|
||||
word,
|
||||
*,
|
||||
safe_input,
|
||||
trim_url_limit=None,
|
||||
nofollow=False,
|
||||
autoescape=False,
|
||||
):
|
||||
if "." in word or "@" in word or ":" in word:
|
||||
# lead: Punctuation trimmed from the beginning of the word.
|
||||
# middle: State of the word.
|
||||
# trail: Punctuation trimmed from the end of the word.
|
||||
lead, middle, trail = self.trim_punctuation(word)
|
||||
# Make URL we want to point to.
|
||||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ""
|
||||
if len(middle) <= MAX_URL_LENGTH and self.simple_url_re.match(middle):
|
||||
url = smart_urlquote(html.unescape(middle))
|
||||
elif len(middle) <= MAX_URL_LENGTH and self.simple_url_2_re.match(middle):
|
||||
url = smart_urlquote("http://%s" % html.unescape(middle))
|
||||
elif ":" not in middle and self.is_email_simple(middle):
|
||||
local, domain = middle.rsplit("@", 1)
|
||||
# Encode per RFC 6068 Section 2 (items 1, 4, 5). Defer any IDNA
|
||||
# to the user agent. See #36013.
|
||||
local = quote(local, safe="")
|
||||
domain = quote(domain, safe="")
|
||||
url = self.mailto_template.format(local=local, domain=domain)
|
||||
nofollow_attr = ""
|
||||
# Make link.
|
||||
if url:
|
||||
trimmed = self.trim_url(middle, limit=trim_url_limit)
|
||||
if autoescape and not safe_input:
|
||||
lead, trail = escape(lead), escape(trail)
|
||||
trimmed = escape(trimmed)
|
||||
middle = self.url_template.format(
|
||||
href=escape(url),
|
||||
attrs=nofollow_attr,
|
||||
url=trimmed,
|
||||
)
|
||||
return mark_safe(f"{lead}{middle}{trail}")
|
||||
else:
|
||||
if safe_input:
|
||||
return mark_safe(word)
|
||||
elif autoescape:
|
||||
return escape(word)
|
||||
elif safe_input:
|
||||
return mark_safe(word)
|
||||
elif autoescape:
|
||||
return escape(word)
|
||||
return word
|
||||
|
||||
def trim_url(self, x, *, limit):
|
||||
if limit is None or len(x) <= limit:
|
||||
return x
|
||||
return "%s…" % x[: max(0, limit - 1)]
|
||||
|
||||
@cached_property
|
||||
def wrapping_punctuation_openings(self):
|
||||
return "".join(dict(self.wrapping_punctuation).keys())
|
||||
|
||||
@cached_property
|
||||
def trailing_punctuation_chars_no_semicolon(self):
|
||||
return self.trailing_punctuation_chars.replace(";", "")
|
||||
|
||||
@cached_property
|
||||
def trailing_punctuation_chars_has_semicolon(self):
|
||||
return ";" in self.trailing_punctuation_chars
|
||||
|
||||
def trim_punctuation(self, word):
|
||||
"""
|
||||
Trim trailing and wrapping punctuation from `word`. Return the items of
|
||||
the new state.
|
||||
"""
|
||||
# Strip all opening wrapping punctuation.
|
||||
middle = word.lstrip(self.wrapping_punctuation_openings)
|
||||
lead = word[: len(word) - len(middle)]
|
||||
trail = ""
|
||||
|
||||
# Continue trimming until middle remains unchanged.
|
||||
trimmed_something = True
|
||||
counts = CountsDict(word=middle)
|
||||
while trimmed_something and middle:
|
||||
trimmed_something = False
|
||||
# Trim wrapping punctuation.
|
||||
for opening, closing in self.wrapping_punctuation:
|
||||
if counts[opening] < counts[closing]:
|
||||
rstripped = middle.rstrip(closing)
|
||||
if rstripped != middle:
|
||||
strip = counts[closing] - counts[opening]
|
||||
trail = middle[-strip:]
|
||||
middle = middle[:-strip]
|
||||
trimmed_something = True
|
||||
counts[closing] -= strip
|
||||
|
||||
amp = middle.rfind("&")
|
||||
if amp == -1:
|
||||
rstripped = middle.rstrip(self.trailing_punctuation_chars)
|
||||
else:
|
||||
rstripped = middle.rstrip(self.trailing_punctuation_chars_no_semicolon)
|
||||
if rstripped != middle:
|
||||
trail = middle[len(rstripped) :] + trail
|
||||
middle = rstripped
|
||||
trimmed_something = True
|
||||
|
||||
if self.trailing_punctuation_chars_has_semicolon and middle.endswith(";"):
|
||||
# Only strip if not part of an HTML entity.
|
||||
potential_entity = middle[amp:]
|
||||
escaped = html.unescape(potential_entity)
|
||||
if escaped == potential_entity or escaped.endswith(";"):
|
||||
rstripped = middle.rstrip(self.trailing_punctuation_chars)
|
||||
trail_start = len(rstripped)
|
||||
amount_trailing_semicolons = len(middle) - len(middle.rstrip(";"))
|
||||
if amp > -1 and amount_trailing_semicolons > 1:
|
||||
# Leave up to most recent semicolon as might be an entity.
|
||||
recent_semicolon = middle[trail_start:].index(";")
|
||||
middle_semicolon_index = recent_semicolon + trail_start + 1
|
||||
trail = middle[middle_semicolon_index:] + trail
|
||||
middle = rstripped + middle[trail_start:middle_semicolon_index]
|
||||
else:
|
||||
trail = middle[trail_start:] + trail
|
||||
middle = rstripped
|
||||
trimmed_something = True
|
||||
|
||||
return lead, middle, trail
|
||||
|
||||
@staticmethod
|
||||
def is_email_simple(value):
|
||||
"""Return True if value looks like an email address."""
|
||||
try:
|
||||
EmailValidator(allowlist=[])(value)
|
||||
except ValidationError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
urlizer = Urlizer()
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
return urlizer(
|
||||
text, trim_url_limit=trim_url_limit, nofollow=nofollow, autoescape=autoescape
|
||||
)
|
||||
|
||||
|
||||
def avoid_wrapping(value):
|
||||
"""
|
||||
Avoid text wrapping in the middle of a phrase by adding non-breaking
|
||||
spaces where there previously were normal spaces.
|
||||
"""
|
||||
return value.replace(" ", "\xa0")
|
||||
|
||||
|
||||
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__:
|
||||
raise ValueError(
|
||||
"can't apply @html_safe to %s because it defines "
|
||||
"__html__()." % klass.__name__
|
||||
)
|
||||
if "__str__" not in klass.__dict__:
|
||||
raise ValueError(
|
||||
"can't apply @html_safe to %s because it doesn't "
|
||||
"define __str__()." % klass.__name__
|
||||
)
|
||||
klass_str = klass.__str__
|
||||
klass.__str__ = lambda self: mark_safe(klass_str(self))
|
||||
klass.__html__ = lambda self: str(self)
|
||||
return klass
|
||||
387
.venv/lib/python3.12/site-packages/django/utils/http.py
Normal file
387
.venv/lib/python3.12/site-packages/django/utils/http.py
Normal file
@@ -0,0 +1,387 @@
|
||||
import base64
|
||||
import re
|
||||
import unicodedata
|
||||
from binascii import Error as BinasciiError
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import formatdate
|
||||
from urllib.parse import quote, unquote
|
||||
from urllib.parse import urlencode as original_urlencode
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
# Based on RFC 9110 Appendix A.
|
||||
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,
|
||||
)
|
||||
|
||||
MONTHS = "jan feb mar apr may jun jul aug sep oct nov dec".split()
|
||||
__D = r"(?P<day>[0-9]{2})"
|
||||
__D2 = r"(?P<day>[ 0-9][0-9])"
|
||||
__M = r"(?P<mon>\w{3})"
|
||||
__Y = r"(?P<year>[0-9]{4})"
|
||||
__Y2 = r"(?P<year>[0-9]{2})"
|
||||
__T = r"(?P<hour>[0-9]{2}):(?P<min>[0-9]{2}):(?P<sec>[0-9]{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 = "!$&'()*+,;="
|
||||
MAX_URL_LENGTH = 2048
|
||||
|
||||
|
||||
def urlencode(query, doseq=False):
|
||||
"""
|
||||
A version of Python's urllib.parse.urlencode() function that can operate on
|
||||
MultiValueDict and non-string values.
|
||||
"""
|
||||
if isinstance(query, MultiValueDict):
|
||||
query = query.lists()
|
||||
elif hasattr(query, "items"):
|
||||
query = query.items()
|
||||
query_params = []
|
||||
for key, value in query:
|
||||
if value is None:
|
||||
raise TypeError(
|
||||
"Cannot encode None for key '%s' in a query string. Did you "
|
||||
"mean to pass an empty string or omit the value?" % key
|
||||
)
|
||||
elif not doseq or isinstance(value, (str, bytes)):
|
||||
query_val = value
|
||||
else:
|
||||
try:
|
||||
itr = iter(value)
|
||||
except TypeError:
|
||||
query_val = value
|
||||
else:
|
||||
# Consume generators and iterators, when doseq=True, to
|
||||
# work around https://bugs.python.org/issue31706.
|
||||
query_val = []
|
||||
for item in itr:
|
||||
if item is None:
|
||||
raise TypeError(
|
||||
"Cannot encode None for key '%s' in a query "
|
||||
"string. Did you mean to pass an empty string or "
|
||||
"omit the value?" % key
|
||||
)
|
||||
elif not isinstance(item, bytes):
|
||||
item = str(item)
|
||||
query_val.append(item)
|
||||
query_params.append((key, query_val))
|
||||
return original_urlencode(query_params, doseq)
|
||||
|
||||
|
||||
def http_date(epoch_seconds=None):
|
||||
"""
|
||||
Format the time to match the RFC 5322 date format as specified by RFC 9110
|
||||
Section 5.6.7.
|
||||
|
||||
`epoch_seconds` is a floating point number expressed in seconds since the
|
||||
epoch, in UTC - such as that outputted by time.time(). If set to None, it
|
||||
defaults to the current time.
|
||||
|
||||
Output a string in the format 'Wdy, DD Mon YYYY HH:MM:SS GMT'.
|
||||
"""
|
||||
return formatdate(epoch_seconds, usegmt=True)
|
||||
|
||||
|
||||
def parse_http_date(date):
|
||||
"""
|
||||
Parse a date format as specified by HTTP RFC 9110 Section 5.6.7.
|
||||
|
||||
The three formats allowed by the RFC are accepted, even if only the first
|
||||
one is still in widespread use.
|
||||
|
||||
Return an integer expressed in seconds since the epoch, in UTC.
|
||||
"""
|
||||
# email.utils.parsedate() does the job for RFC 1123 dates; unfortunately
|
||||
# RFC 9110 makes it mandatory to support RFC 850 dates too. So we roll
|
||||
# our own RFC-compliant parsing.
|
||||
for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
|
||||
m = regex.match(date)
|
||||
if m is not None:
|
||||
break
|
||||
else:
|
||||
raise ValueError("%r is not in a valid HTTP date format" % date)
|
||||
try:
|
||||
year = int(m["year"])
|
||||
if year < 100:
|
||||
current_year = datetime.now(tz=timezone.utc).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
|
||||
# interpreted as representing the past.
|
||||
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(year, month, day, hour, min, sec, tzinfo=timezone.utc)
|
||||
return int(result.timestamp())
|
||||
except Exception as exc:
|
||||
raise ValueError("%r is not a valid date" % date) from exc
|
||||
|
||||
|
||||
def parse_http_date_safe(date):
|
||||
"""
|
||||
Same as parse_http_date, but return None if the input is invalid.
|
||||
"""
|
||||
try:
|
||||
return parse_http_date(date)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# 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
|
||||
into an int.
|
||||
"""
|
||||
# To prevent overconsumption of server resources, reject any
|
||||
# base36 string that is longer than 13 base36 digits (13 digits
|
||||
# is sufficient to base36-encode any 64-bit integer)
|
||||
if len(s) > 13:
|
||||
raise ValueError("Base36 input too large")
|
||||
return int(s, 36)
|
||||
|
||||
|
||||
def int_to_base36(i):
|
||||
"""Convert an integer to a base36 string."""
|
||||
char_set = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
if i < 0:
|
||||
raise ValueError("Negative base36 conversion input.")
|
||||
if i < 36:
|
||||
return char_set[i]
|
||||
b36 = ""
|
||||
while i != 0:
|
||||
i, n = divmod(i, 36)
|
||||
b36 = char_set[n] + b36
|
||||
return b36
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def urlsafe_base64_decode(s):
|
||||
"""
|
||||
Decode a base64 encoded string. Add back any trailing equal signs that
|
||||
might have been stripped.
|
||||
"""
|
||||
s = s.encode()
|
||||
try:
|
||||
return base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, b"="))
|
||||
except (LookupError, BinasciiError) as e:
|
||||
raise ValueError(e)
|
||||
|
||||
|
||||
def parse_etags(etag_str):
|
||||
"""
|
||||
Parse a string of ETags given in an If-None-Match or If-Match header as
|
||||
defined by RFC 9110. Return a list of quoted ETags, or ['*'] if all ETags
|
||||
should be matched.
|
||||
"""
|
||||
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(","))
|
||||
return [match[1] for match in etag_matches if match]
|
||||
|
||||
|
||||
def quote_etag(etag_str):
|
||||
"""
|
||||
If the provided string is already a quoted ETag, return it. Otherwise, wrap
|
||||
the string in quotes, making it a strong ETag.
|
||||
"""
|
||||
if ETAG_MATCH.match(etag_str):
|
||||
return etag_str
|
||||
else:
|
||||
return '"%s"' % etag_str
|
||||
|
||||
|
||||
def is_same_domain(host, pattern):
|
||||
"""
|
||||
Return ``True`` if the host is either an exact match or a match
|
||||
to the wildcard pattern.
|
||||
|
||||
Any pattern beginning with a period matches a domain and all of its
|
||||
subdomains. (e.g. ``.example.com`` matches ``example.com`` and
|
||||
``foo.example.com``). Anything else is an exact string match.
|
||||
"""
|
||||
if not pattern:
|
||||
return False
|
||||
|
||||
pattern = pattern.lower()
|
||||
return (
|
||||
pattern[0] == "."
|
||||
and (host.endswith(pattern) or host == pattern[1:])
|
||||
or pattern == host
|
||||
)
|
||||
|
||||
|
||||
def url_has_allowed_host_and_scheme(url, allowed_hosts, require_https=False):
|
||||
"""
|
||||
Return ``True`` if the url uses an allowed host and a safe scheme.
|
||||
|
||||
Always return ``False`` on an empty url.
|
||||
|
||||
If ``require_https`` is ``True``, only 'https' will be considered a valid
|
||||
scheme, as opposed to 'http' and 'https' with the default, ``False``.
|
||||
|
||||
Note: "True" doesn't entail that a URL is "safe". It may still be e.g.
|
||||
quoted incorrectly. Ensure to also use django.utils.encoding.iri_to_uri()
|
||||
on the path component of untrusted URLs.
|
||||
"""
|
||||
if url is not None:
|
||||
url = url.strip()
|
||||
if not url:
|
||||
return False
|
||||
if allowed_hosts is None:
|
||||
allowed_hosts = set()
|
||||
elif isinstance(allowed_hosts, str):
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
# urlsplit is not so flexible. Treat any url with three slashes as unsafe.
|
||||
if url.startswith("///") or len(url) > MAX_URL_LENGTH:
|
||||
# urlsplit does not perform validation of inputs. Unicode normalization
|
||||
# is very slow on Windows and can be a DoS attack vector.
|
||||
# https://docs.python.org/3/library/urllib.parse.html#url-parsing-security
|
||||
return False
|
||||
try:
|
||||
url_info = urlsplit(url)
|
||||
except ValueError: # e.g. invalid IPv6 addresses
|
||||
return False
|
||||
# Forbid URLs like http:///example.com - with a scheme, but without a hostname.
|
||||
# In that URL, example.com is not the hostname but, a path component. However,
|
||||
# Chrome will still consider example.com to be the hostname, so we must not
|
||||
# allow this syntax.
|
||||
if not url_info.netloc and url_info.scheme:
|
||||
return 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":
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def escape_leading_slashes(url):
|
||||
"""
|
||||
If redirecting to an absolute path (two leading slashes), a slash must be
|
||||
escaped to prevent browsers from handling the path as schemaless and
|
||||
redirecting to another host.
|
||||
"""
|
||||
if url.startswith("//"):
|
||||
url = "/%2F{}".format(url.removeprefix("//"))
|
||||
return url
|
||||
|
||||
|
||||
def _parseparam(s):
|
||||
while s[:1] == ";":
|
||||
s = s[1:]
|
||||
end = s.find(";")
|
||||
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
|
||||
end = s.find(";", end + 1)
|
||||
if end < 0:
|
||||
end = len(s)
|
||||
f = s[:end]
|
||||
yield f.strip()
|
||||
s = s[end:]
|
||||
|
||||
|
||||
def parse_header_parameters(line):
|
||||
"""
|
||||
Parse a Content-type like header.
|
||||
Return the main content-type and a dictionary of options.
|
||||
"""
|
||||
parts = _parseparam(";" + line)
|
||||
key = parts.__next__().lower()
|
||||
pdict = {}
|
||||
for p in parts:
|
||||
i = p.find("=")
|
||||
if i >= 0:
|
||||
has_encoding = False
|
||||
name = p[:i].strip().lower()
|
||||
if name.endswith("*"):
|
||||
# Lang/encoding embedded in the value (like "filename*=UTF-8''file.ext")
|
||||
# https://tools.ietf.org/html/rfc2231#section-4
|
||||
name = name[:-1]
|
||||
if p.count("'") == 2:
|
||||
has_encoding = True
|
||||
value = p[i + 1 :].strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
||||
value = value[1:-1]
|
||||
value = value.replace("\\\\", "\\").replace('\\"', '"')
|
||||
if has_encoding:
|
||||
encoding, lang, value = value.split("'")
|
||||
value = unquote(value, encoding=encoding)
|
||||
pdict[name] = value
|
||||
return key, pdict
|
||||
|
||||
|
||||
def content_disposition_header(as_attachment, filename):
|
||||
"""
|
||||
Construct a Content-Disposition HTTP header value from the given filename
|
||||
as specified by RFC 6266.
|
||||
"""
|
||||
if filename:
|
||||
disposition = "attachment" if as_attachment else "inline"
|
||||
try:
|
||||
filename.encode("ascii")
|
||||
is_ascii = True
|
||||
except UnicodeEncodeError:
|
||||
is_ascii = False
|
||||
# Quoted strings can contain horizontal tabs, space characters, and
|
||||
# characters from 0x21 to 0x7e, except 0x22 (`"`) and 0x5C (`\`) which
|
||||
# can still be expressed but must be escaped with their own `\`.
|
||||
# https://datatracker.ietf.org/doc/html/rfc9110#name-quoted-strings
|
||||
quotable_characters = r"^[\t \x21-\x7e]*$"
|
||||
if is_ascii and re.match(quotable_characters, filename):
|
||||
file_expr = 'filename="{}"'.format(
|
||||
filename.replace("\\", "\\\\").replace('"', r"\"")
|
||||
)
|
||||
else:
|
||||
file_expr = "filename*=utf-8''{}".format(quote(filename))
|
||||
return f"{disposition}; {file_expr}"
|
||||
elif as_attachment:
|
||||
return "attachment"
|
||||
else:
|
||||
return None
|
||||
90
.venv/lib/python3.12/site-packages/django/utils/inspect.py
Normal file
90
.venv/lib/python3.12/site-packages/django/utils/inspect.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
from django.utils.version import PY314
|
||||
|
||||
if PY314:
|
||||
import annotationlib
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def _get_func_parameters(func, remove_first):
|
||||
# As the annotations are not used in any case, inspect the signature with
|
||||
# FORWARDREF to leave any deferred annotations unevaluated.
|
||||
if PY314:
|
||||
signature = inspect.signature(
|
||||
func, annotation_format=annotationlib.Format.FORWARDREF
|
||||
)
|
||||
else:
|
||||
signature = inspect.signature(func)
|
||||
|
||||
parameters = tuple(signature.parameters.values())
|
||||
if remove_first:
|
||||
parameters = parameters[1:]
|
||||
return parameters
|
||||
|
||||
|
||||
def _get_callable_parameters(meth_or_func):
|
||||
is_method = inspect.ismethod(meth_or_func)
|
||||
func = meth_or_func.__func__ if is_method else meth_or_func
|
||||
return _get_func_parameters(func, remove_first=is_method)
|
||||
|
||||
|
||||
ARG_KINDS = frozenset(
|
||||
{
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
inspect.Parameter.KEYWORD_ONLY,
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_func_args(func):
|
||||
params = _get_callable_parameters(func)
|
||||
return [param.name for param in params if param.kind in ARG_KINDS]
|
||||
|
||||
|
||||
def get_func_full_args(func):
|
||||
"""
|
||||
Return a list of (argument name, default value) tuples. If the argument
|
||||
does not have a default value, omit it in the tuple. Arguments such as
|
||||
*args and **kwargs are also included.
|
||||
"""
|
||||
params = _get_callable_parameters(func)
|
||||
args = []
|
||||
for param in params:
|
||||
name = param.name
|
||||
# Ignore 'self'
|
||||
if name == "self":
|
||||
continue
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
name = "*" + name
|
||||
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
name = "**" + name
|
||||
if param.default != inspect.Parameter.empty:
|
||||
args.append((name, param.default))
|
||||
else:
|
||||
args.append((name,))
|
||||
return args
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 in ARG_KINDS])
|
||||
return count == 0 if inspect.ismethod(meth) else count == 1
|
||||
|
||||
|
||||
def func_supports_parameter(func, name):
|
||||
return any(param.name == name for param in _get_callable_parameters(func))
|
||||
64
.venv/lib/python3.12/site-packages/django/utils/ipv6.py
Normal file
64
.venv/lib/python3.12/site-packages/django/utils/ipv6.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import ipaddress
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
MAX_IPV6_ADDRESS_LENGTH = 39
|
||||
|
||||
|
||||
def _ipv6_address_from_str(ip_str, max_length=MAX_IPV6_ADDRESS_LENGTH):
|
||||
if len(ip_str) > max_length:
|
||||
raise ValueError(
|
||||
f"Unable to convert {ip_str} to an IPv6 address (value too long)."
|
||||
)
|
||||
return ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str)))
|
||||
|
||||
|
||||
def clean_ipv6_address(
|
||||
ip_str,
|
||||
unpack_ipv4=False,
|
||||
error_message=_("This is not a valid IPv6 address."),
|
||||
max_length=MAX_IPV6_ADDRESS_LENGTH,
|
||||
):
|
||||
"""
|
||||
Clean an IPv6 address string.
|
||||
|
||||
Raise ValidationError if the address is invalid.
|
||||
|
||||
Replace the longest continuous zero-sequence with "::", remove leading
|
||||
zeroes, and make sure all hextets are lowercase.
|
||||
|
||||
Args:
|
||||
ip_str: A valid IPv6 address.
|
||||
unpack_ipv4: if an IPv4-mapped address is found,
|
||||
return the plain IPv4 address (default=False).
|
||||
error_message: An error message used in the ValidationError.
|
||||
|
||||
Return a compressed IPv6 address or the same value.
|
||||
"""
|
||||
try:
|
||||
addr = _ipv6_address_from_str(ip_str, max_length)
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
error_message, code="invalid", params={"protocol": _("IPv6")}
|
||||
)
|
||||
|
||||
if unpack_ipv4 and addr.ipv4_mapped:
|
||||
return str(addr.ipv4_mapped)
|
||||
elif addr.ipv4_mapped:
|
||||
return "::ffff:%s" % str(addr.ipv4_mapped)
|
||||
|
||||
return str(addr)
|
||||
|
||||
|
||||
def is_valid_ipv6_address(ip_addr):
|
||||
"""
|
||||
Return whether the `ip_addr` object is a valid IPv6 address.
|
||||
"""
|
||||
if isinstance(ip_addr, ipaddress.IPv6Address):
|
||||
return True
|
||||
try:
|
||||
_ipv6_address_from_str(ip_addr)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,21 @@
|
||||
# RemovedInDjango60Warning: Remove this entire module.
|
||||
|
||||
import warnings
|
||||
|
||||
from django.utils.deprecation import RemovedInDjango60Warning
|
||||
|
||||
|
||||
def is_iterable(x):
|
||||
"An implementation independent way of checking for iterables"
|
||||
warnings.warn(
|
||||
"django.utils.itercompat.is_iterable() is deprecated. "
|
||||
"Use isinstance(..., collections.abc.Iterable) instead.",
|
||||
RemovedInDjango60Warning,
|
||||
stacklevel=2,
|
||||
)
|
||||
try:
|
||||
iter(x)
|
||||
except TypeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
262
.venv/lib/python3.12/site-packages/django/utils/log.py
Normal file
262
.venv/lib/python3.12/site-packages/django/utils/log.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import logging
|
||||
import logging.config # needed when logging_config doesn't start with logging.config
|
||||
from copy import copy
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
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")
|
||||
|
||||
# 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.
|
||||
DEFAULT_LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"filters": {
|
||||
"require_debug_false": {
|
||||
"()": "django.utils.log.RequireDebugFalse",
|
||||
},
|
||||
"require_debug_true": {
|
||||
"()": "django.utils.log.RequireDebugTrue",
|
||||
},
|
||||
},
|
||||
"formatters": {
|
||||
"django.server": {
|
||||
"()": "django.utils.log.ServerFormatter",
|
||||
"format": "[{server_time}] {message}",
|
||||
"style": "{",
|
||||
}
|
||||
},
|
||||
"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",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["console", "mail_admins"],
|
||||
"level": "INFO",
|
||||
},
|
||||
"django.server": {
|
||||
"handlers": ["django.server"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def configure_logging(logging_config, logging_settings):
|
||||
if logging_config:
|
||||
# First find the logging configuration function ...
|
||||
logging_config_func = import_string(logging_config)
|
||||
|
||||
logging.config.dictConfig(DEFAULT_LOGGING)
|
||||
|
||||
# ... then invoke it with the logging settings
|
||||
if logging_settings:
|
||||
logging_config_func(logging_settings)
|
||||
|
||||
|
||||
class AdminEmailHandler(logging.Handler):
|
||||
"""An exception log handler that emails log entries to site admins.
|
||||
|
||||
If the request is passed as the first argument to the log record,
|
||||
request data will be provided in the email report.
|
||||
"""
|
||||
|
||||
def __init__(self, include_html=False, email_backend=None, reporter_class=None):
|
||||
super().__init__()
|
||||
self.include_html = include_html
|
||||
self.email_backend = email_backend
|
||||
self.reporter_class = import_string(
|
||||
reporter_class or settings.DEFAULT_EXCEPTION_REPORTER
|
||||
)
|
||||
|
||||
def emit(self, record):
|
||||
# Early return when no email will be sent.
|
||||
if (
|
||||
not settings.ADMINS
|
||||
# Method not overridden.
|
||||
and self.send_mail.__func__ is AdminEmailHandler.send_mail
|
||||
):
|
||||
return
|
||||
try:
|
||||
request = record.request
|
||||
subject = "%s (%s IP): %s" % (
|
||||
record.levelname,
|
||||
(
|
||||
"internal"
|
||||
if request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS
|
||||
else "EXTERNAL"
|
||||
),
|
||||
record.getMessage(),
|
||||
)
|
||||
except Exception:
|
||||
subject = "%s: %s" % (record.levelname, record.getMessage())
|
||||
request = None
|
||||
subject = self.format_subject(subject)
|
||||
|
||||
# Since we add a nicely formatted traceback on our own, create a copy
|
||||
# of the log record without the exception data.
|
||||
no_exc_record = copy(record)
|
||||
no_exc_record.exc_info = None
|
||||
no_exc_record.exc_text = None
|
||||
|
||||
if record.exc_info:
|
||||
exc_info = record.exc_info
|
||||
else:
|
||||
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(),
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
def connection(self):
|
||||
return get_connection(backend=self.email_backend, fail_silently=True)
|
||||
|
||||
def format_subject(self, subject):
|
||||
"""
|
||||
Escape CR and LF characters.
|
||||
"""
|
||||
return subject.replace("\n", "\\n").replace("\r", "\\r")
|
||||
|
||||
|
||||
class CallbackFilter(logging.Filter):
|
||||
"""
|
||||
A logging filter that checks the return value of a given callable (which
|
||||
takes the record-to-be-logged as its only parameter) to decide whether to
|
||||
log a record.
|
||||
"""
|
||||
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
|
||||
def filter(self, record):
|
||||
if self.callback(record):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
class RequireDebugFalse(logging.Filter):
|
||||
def filter(self, record):
|
||||
return not settings.DEBUG
|
||||
|
||||
|
||||
class RequireDebugTrue(logging.Filter):
|
||||
def filter(self, record):
|
||||
return settings.DEBUG
|
||||
|
||||
|
||||
class ServerFormatter(logging.Formatter):
|
||||
default_time_format = "%d/%b/%Y %H:%M:%S"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.style = color_style()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def format(self, record):
|
||||
msg = record.msg
|
||||
status_code = getattr(record, "status_code", None)
|
||||
|
||||
if status_code:
|
||||
if 200 <= status_code < 300:
|
||||
# Put 2XX first, since it should be the common case
|
||||
msg = self.style.HTTP_SUCCESS(msg)
|
||||
elif 100 <= status_code < 200:
|
||||
msg = self.style.HTTP_INFO(msg)
|
||||
elif status_code == 304:
|
||||
msg = self.style.HTTP_NOT_MODIFIED(msg)
|
||||
elif 300 <= status_code < 400:
|
||||
msg = self.style.HTTP_REDIRECT(msg)
|
||||
elif status_code == 404:
|
||||
msg = self.style.HTTP_NOT_FOUND(msg)
|
||||
elif 400 <= status_code < 500:
|
||||
msg = self.style.HTTP_BAD_REQUEST(msg)
|
||||
else:
|
||||
# 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"):
|
||||
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
|
||||
|
||||
|
||||
def log_response(
|
||||
message,
|
||||
*args,
|
||||
response=None,
|
||||
request=None,
|
||||
logger=request_logger,
|
||||
level=None,
|
||||
exception=None,
|
||||
):
|
||||
"""
|
||||
Log errors based on HttpResponse status.
|
||||
|
||||
Log 5xx responses as errors and 4xx responses as warnings (unless a level
|
||||
is given as a keyword argument). The HttpResponse status_code and the
|
||||
request are passed to the logger's extra parameter.
|
||||
"""
|
||||
# Check if the response has already been logged. Multiple requests to log
|
||||
# the same response can be received in some cases, e.g., when the
|
||||
# response is the result of an exception and is logged when the exception
|
||||
# is caught, to record the exception.
|
||||
if getattr(response, "_has_been_logged", False):
|
||||
return
|
||||
|
||||
if level is None:
|
||||
if response.status_code >= 500:
|
||||
level = "error"
|
||||
elif response.status_code >= 400:
|
||||
level = "warning"
|
||||
else:
|
||||
level = "info"
|
||||
|
||||
escaped_args = tuple(
|
||||
a.encode("unicode_escape").decode("ascii") if isinstance(a, str) else a
|
||||
for a in args
|
||||
)
|
||||
|
||||
getattr(logger, level)(
|
||||
message,
|
||||
*escaped_args,
|
||||
extra={
|
||||
"status_code": response.status_code,
|
||||
"request": request,
|
||||
},
|
||||
exc_info=exception,
|
||||
)
|
||||
response._has_been_logged = True
|
||||
286
.venv/lib/python3.12/site-packages/django/utils/lorem_ipsum.py
Normal file
286
.venv/lib/python3.12/site-packages/django/utils/lorem_ipsum.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
Utility functions for generating "lorem ipsum" Latin text.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
COMMON_P = (
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod "
|
||||
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
|
||||
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
|
||||
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
|
||||
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
|
||||
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
|
||||
"mollit anim id est laborum."
|
||||
)
|
||||
|
||||
WORDS = (
|
||||
"exercitationem",
|
||||
"perferendis",
|
||||
"perspiciatis",
|
||||
"laborum",
|
||||
"eveniet",
|
||||
"sunt",
|
||||
"iure",
|
||||
"nam",
|
||||
"nobis",
|
||||
"eum",
|
||||
"cum",
|
||||
"officiis",
|
||||
"excepturi",
|
||||
"odio",
|
||||
"consectetur",
|
||||
"quasi",
|
||||
"aut",
|
||||
"quisquam",
|
||||
"vel",
|
||||
"eligendi",
|
||||
"itaque",
|
||||
"non",
|
||||
"odit",
|
||||
"tempore",
|
||||
"quaerat",
|
||||
"dignissimos",
|
||||
"facilis",
|
||||
"neque",
|
||||
"nihil",
|
||||
"expedita",
|
||||
"vitae",
|
||||
"vero",
|
||||
"ipsum",
|
||||
"nisi",
|
||||
"animi",
|
||||
"cumque",
|
||||
"pariatur",
|
||||
"velit",
|
||||
"modi",
|
||||
"natus",
|
||||
"iusto",
|
||||
"eaque",
|
||||
"sequi",
|
||||
"illo",
|
||||
"sed",
|
||||
"ex",
|
||||
"et",
|
||||
"voluptatibus",
|
||||
"tempora",
|
||||
"veritatis",
|
||||
"ratione",
|
||||
"assumenda",
|
||||
"incidunt",
|
||||
"nostrum",
|
||||
"placeat",
|
||||
"aliquid",
|
||||
"fuga",
|
||||
"provident",
|
||||
"praesentium",
|
||||
"rem",
|
||||
"necessitatibus",
|
||||
"suscipit",
|
||||
"adipisci",
|
||||
"quidem",
|
||||
"possimus",
|
||||
"voluptas",
|
||||
"debitis",
|
||||
"sint",
|
||||
"accusantium",
|
||||
"unde",
|
||||
"sapiente",
|
||||
"voluptate",
|
||||
"qui",
|
||||
"aspernatur",
|
||||
"laudantium",
|
||||
"soluta",
|
||||
"amet",
|
||||
"quo",
|
||||
"aliquam",
|
||||
"saepe",
|
||||
"culpa",
|
||||
"libero",
|
||||
"ipsa",
|
||||
"dicta",
|
||||
"reiciendis",
|
||||
"nesciunt",
|
||||
"doloribus",
|
||||
"autem",
|
||||
"impedit",
|
||||
"minima",
|
||||
"maiores",
|
||||
"repudiandae",
|
||||
"ipsam",
|
||||
"obcaecati",
|
||||
"ullam",
|
||||
"enim",
|
||||
"totam",
|
||||
"delectus",
|
||||
"ducimus",
|
||||
"quis",
|
||||
"voluptates",
|
||||
"dolores",
|
||||
"molestiae",
|
||||
"harum",
|
||||
"dolorem",
|
||||
"quia",
|
||||
"voluptatem",
|
||||
"molestias",
|
||||
"magni",
|
||||
"distinctio",
|
||||
"omnis",
|
||||
"illum",
|
||||
"dolorum",
|
||||
"voluptatum",
|
||||
"ea",
|
||||
"quas",
|
||||
"quam",
|
||||
"corporis",
|
||||
"quae",
|
||||
"blanditiis",
|
||||
"atque",
|
||||
"deserunt",
|
||||
"laboriosam",
|
||||
"earum",
|
||||
"consequuntur",
|
||||
"hic",
|
||||
"cupiditate",
|
||||
"quibusdam",
|
||||
"accusamus",
|
||||
"ut",
|
||||
"rerum",
|
||||
"error",
|
||||
"minus",
|
||||
"eius",
|
||||
"ab",
|
||||
"ad",
|
||||
"nemo",
|
||||
"fugit",
|
||||
"officia",
|
||||
"at",
|
||||
"in",
|
||||
"id",
|
||||
"quos",
|
||||
"reprehenderit",
|
||||
"numquam",
|
||||
"iste",
|
||||
"fugiat",
|
||||
"sit",
|
||||
"inventore",
|
||||
"beatae",
|
||||
"repellendus",
|
||||
"magnam",
|
||||
"recusandae",
|
||||
"quod",
|
||||
"explicabo",
|
||||
"doloremque",
|
||||
"aperiam",
|
||||
"consequatur",
|
||||
"asperiores",
|
||||
"commodi",
|
||||
"optio",
|
||||
"dolor",
|
||||
"labore",
|
||||
"temporibus",
|
||||
"repellat",
|
||||
"veniam",
|
||||
"architecto",
|
||||
"est",
|
||||
"esse",
|
||||
"mollitia",
|
||||
"nulla",
|
||||
"a",
|
||||
"similique",
|
||||
"eos",
|
||||
"alias",
|
||||
"dolore",
|
||||
"tenetur",
|
||||
"deleniti",
|
||||
"porro",
|
||||
"facere",
|
||||
"maxime",
|
||||
"corrupti",
|
||||
)
|
||||
|
||||
COMMON_WORDS = (
|
||||
"lorem",
|
||||
"ipsum",
|
||||
"dolor",
|
||||
"sit",
|
||||
"amet",
|
||||
"consectetur",
|
||||
"adipisicing",
|
||||
"elit",
|
||||
"sed",
|
||||
"do",
|
||||
"eiusmod",
|
||||
"tempor",
|
||||
"incididunt",
|
||||
"ut",
|
||||
"labore",
|
||||
"et",
|
||||
"dolore",
|
||||
"magna",
|
||||
"aliqua",
|
||||
)
|
||||
|
||||
|
||||
def sentence():
|
||||
"""
|
||||
Return a randomly generated sentence of lorem ipsum text.
|
||||
|
||||
The first word is capitalized, and the sentence ends in either a period or
|
||||
question mark. Commas are added at random.
|
||||
"""
|
||||
# Determine the number of comma-separated sections and number of words in
|
||||
# each section for this sentence.
|
||||
sections = [
|
||||
" ".join(random.sample(WORDS, random.randint(3, 12)))
|
||||
for i in range(random.randint(1, 5))
|
||||
]
|
||||
s = ", ".join(sections)
|
||||
# Convert to sentence case and add end punctuation.
|
||||
return "%s%s%s" % (s[0].upper(), s[1:], random.choice("?."))
|
||||
|
||||
|
||||
def paragraph():
|
||||
"""
|
||||
Return a randomly generated paragraph of lorem ipsum text.
|
||||
|
||||
The paragraph consists of between 1 and 4 sentences, inclusive.
|
||||
"""
|
||||
return " ".join(sentence() for i in range(random.randint(1, 4)))
|
||||
|
||||
|
||||
def paragraphs(count, common=True):
|
||||
"""
|
||||
Return a list of paragraphs as returned by paragraph().
|
||||
|
||||
If `common` is True, then the first paragraph will be the standard
|
||||
'lorem ipsum' paragraph. Otherwise, the first paragraph will be random
|
||||
Latin text. Either way, subsequent paragraphs will be random Latin text.
|
||||
"""
|
||||
paras = []
|
||||
for i in range(count):
|
||||
if common and i == 0:
|
||||
paras.append(COMMON_P)
|
||||
else:
|
||||
paras.append(paragraph())
|
||||
return paras
|
||||
|
||||
|
||||
def words(count, common=True):
|
||||
"""
|
||||
Return a string of `count` lorem ipsum words separated by a single space.
|
||||
|
||||
If `common` is True, then the first 19 words will be the standard
|
||||
'lorem ipsum' words. Otherwise, all words will be selected randomly.
|
||||
"""
|
||||
word_list = list(COMMON_WORDS) if common else []
|
||||
c = len(word_list)
|
||||
if count > c:
|
||||
count -= c
|
||||
while count > 0:
|
||||
c = min(count, len(WORDS))
|
||||
count -= c
|
||||
word_list += random.sample(WORDS, c)
|
||||
else:
|
||||
word_list = word_list[:count]
|
||||
return " ".join(word_list)
|
||||
@@ -0,0 +1,107 @@
|
||||
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):
|
||||
# Check whether module is loaded and fully initialized.
|
||||
if not (
|
||||
(module := sys.modules.get(module_path))
|
||||
and (spec := getattr(module, "__spec__", None))
|
||||
and getattr(spec, "_initializing", False) is False
|
||||
):
|
||||
module = import_module(module_path)
|
||||
return getattr(module, 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)
|
||||
except ValueError as err:
|
||||
raise ImportError("%s doesn't look like a module path" % dotted_path) from err
|
||||
|
||||
try:
|
||||
return cached_import(module_path, class_name)
|
||||
except AttributeError as err:
|
||||
raise ImportError(
|
||||
'Module "%s" does not define a "%s" attribute/class'
|
||||
% (module_path, class_name)
|
||||
) from err
|
||||
|
||||
|
||||
def autodiscover_modules(*args, **kwargs):
|
||||
"""
|
||||
Auto-discover INSTALLED_APPS modules and fail silently when
|
||||
not present. This forces an import on them to register any admin bits they
|
||||
may want.
|
||||
|
||||
You may provide a register_to keyword parameter as a way to access a
|
||||
registry. This register_to object must have a _registry instance variable
|
||||
to access it.
|
||||
"""
|
||||
from django.apps import apps
|
||||
|
||||
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.
|
||||
try:
|
||||
if register_to:
|
||||
before_import_registry = copy.copy(register_to._registry)
|
||||
|
||||
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
|
||||
# this could raise NotRegistered and AlreadyRegistered
|
||||
# exceptions (see #8245).
|
||||
if register_to:
|
||||
register_to._registry = before_import_registry
|
||||
|
||||
# Decide whether to bubble up this error. If the app just
|
||||
# doesn't have the module in question, we can ignore the error
|
||||
# attempting to import it, otherwise we want it to bubble up.
|
||||
if module_has_submodule(app_config.module, module_to_search):
|
||||
raise
|
||||
|
||||
|
||||
def module_has_submodule(package, module_name):
|
||||
"""See if 'module' is in 'package'."""
|
||||
try:
|
||||
package_name = package.__name__
|
||||
package_path = package.__path__
|
||||
except AttributeError:
|
||||
# package isn't a package.
|
||||
return False
|
||||
|
||||
full_module_name = package_name + "." + module_name
|
||||
try:
|
||||
return importlib_find(full_module_name, package_path) is not None
|
||||
except ModuleNotFoundError:
|
||||
# When module_name is an invalid dotted path, Python raises
|
||||
# ModuleNotFoundError.
|
||||
return False
|
||||
|
||||
|
||||
def module_dir(module):
|
||||
"""
|
||||
Find the name of the directory that contains a module, if possible.
|
||||
|
||||
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__", []))
|
||||
if len(paths) == 1:
|
||||
return paths[0]
|
||||
else:
|
||||
filename = getattr(module, "__file__", None)
|
||||
if filename is not None:
|
||||
return os.path.dirname(filename)
|
||||
raise ValueError("Cannot determine directory containing %s" % module)
|
||||
105
.venv/lib/python3.12/site-packages/django/utils/numberformat.py
Normal file
105
.venv/lib/python3.12/site-packages/django/utils/numberformat.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from decimal import Decimal
|
||||
|
||||
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,
|
||||
):
|
||||
"""
|
||||
Get a number (as a number or string), and return it as a string,
|
||||
using formats defined as arguments:
|
||||
|
||||
* decimal_sep: Decimal separator symbol (for example ".")
|
||||
* decimal_pos: Number of decimal positions
|
||||
* grouping: Number of digits in every group limited by thousand separator.
|
||||
For non-uniform digit grouping, it can be a sequence with the number
|
||||
of digit group sizes following the format used by the Python locale
|
||||
module in locale.localeconv() LC_NUMERIC grouping (e.g. (3, 2, 0)).
|
||||
* thousand_sep: Thousand separator symbol (for example ",")
|
||||
"""
|
||||
if number is None or number == "":
|
||||
return mark_safe(number)
|
||||
if use_l10n is None:
|
||||
use_l10n = True
|
||||
use_grouping = 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 = ""
|
||||
# Treat potentially very large/small floats as Decimals.
|
||||
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"))
|
||||
if abs(number) < cutoff:
|
||||
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")
|
||||
# Format the coefficient.
|
||||
coefficient = format(
|
||||
coefficient,
|
||||
decimal_sep,
|
||||
decimal_pos,
|
||||
grouping,
|
||||
thousand_sep,
|
||||
force_grouping,
|
||||
use_l10n,
|
||||
)
|
||||
return "{}e{}".format(coefficient, exponent)
|
||||
else:
|
||||
str_number = "{:f}".format(number)
|
||||
else:
|
||||
str_number = str(number)
|
||||
if str_number[0] == "-":
|
||||
sign = "-"
|
||||
str_number = str_number[1:]
|
||||
# decimal part
|
||||
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, ""
|
||||
if decimal_pos is not None:
|
||||
dec_part += "0" * (decimal_pos - len(dec_part))
|
||||
dec_part = dec_part and decimal_sep + dec_part
|
||||
# grouping
|
||||
if use_grouping:
|
||||
try:
|
||||
# if grouping is a sequence
|
||||
intervals = list(grouping)
|
||||
except TypeError:
|
||||
# grouping is a single value
|
||||
intervals = [grouping, 0]
|
||||
active_interval = intervals.pop(0)
|
||||
int_part_gd = ""
|
||||
cnt = 0
|
||||
for digit in int_part[::-1]:
|
||||
if cnt and cnt == active_interval:
|
||||
if intervals:
|
||||
active_interval = intervals.pop(0) or active_interval
|
||||
int_part_gd += thousand_sep[::-1]
|
||||
cnt = 0
|
||||
int_part_gd += digit
|
||||
cnt += 1
|
||||
int_part = int_part_gd[::-1]
|
||||
return sign + int_part + dec_part
|
||||
354
.venv/lib/python3.12/site-packages/django/utils/regex_helper.py
Normal file
354
.venv/lib/python3.12/site-packages/django/utils/regex_helper.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
Functions for reversing a regular expression (used in reverse URL resolving).
|
||||
Used internally by Django and not intended for external use.
|
||||
|
||||
This is not, and is not intended to be, a complete reg-exp decompiler. It
|
||||
should be good enough for a large class of URLS, however.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
|
||||
# Mapping of an escape character to a representative of that class. So, e.g.,
|
||||
# "\w" is replaced by "x" in a reverse URL. A value of None means to ignore
|
||||
# this sequence. Any missing key is mapped to itself.
|
||||
ESCAPE_MAPPINGS = {
|
||||
"A": None,
|
||||
"b": None,
|
||||
"B": None,
|
||||
"d": "0",
|
||||
"D": "x",
|
||||
"s": " ",
|
||||
"S": "x",
|
||||
"w": "x",
|
||||
"W": "!",
|
||||
"Z": None,
|
||||
}
|
||||
|
||||
|
||||
class Choice(list):
|
||||
"""Represent multiple possibilities at this point in a pattern string."""
|
||||
|
||||
|
||||
class Group(list):
|
||||
"""Represent a capturing group in the pattern string."""
|
||||
|
||||
|
||||
class NonCapture(list):
|
||||
"""Represent a non-capturing group in the pattern string."""
|
||||
|
||||
|
||||
def normalize(pattern):
|
||||
r"""
|
||||
Given a reg-exp pattern, normalize it to an iterable of forms that
|
||||
suffice for reverse matching. This does the following:
|
||||
|
||||
(1) For any repeating sections, keeps the minimum number of occurrences
|
||||
permitted (this means zero for optional groups).
|
||||
(2) If an optional group includes parameters, include one occurrence of
|
||||
that group (along with the zero occurrence case from step (1)).
|
||||
(3) Select the first (essentially an arbitrary) element from any character
|
||||
class. Select an arbitrary character for any unordered class (e.g. '.'
|
||||
or '\w') in the pattern.
|
||||
(4) Ignore look-ahead and look-behind assertions.
|
||||
(5) Raise an error on any disjunctive ('|') constructs.
|
||||
|
||||
Django's URLs for forward resolving are either all positional arguments or
|
||||
all keyword arguments. That is assumed here, as well. Although reverse
|
||||
resolving can be done using positional args when keyword args are
|
||||
specified, the two cannot be mixed in the same reverse() call.
|
||||
"""
|
||||
# Do a linear scan to work out the special features of this pattern. The
|
||||
# idea is that we scan once here and collect all the information we need to
|
||||
# make future decisions.
|
||||
result = []
|
||||
non_capturing_groups = []
|
||||
consume_next = True
|
||||
pattern_iter = next_char(iter(pattern))
|
||||
num_args = 0
|
||||
|
||||
# A "while" loop is used here because later on we need to be able to peek
|
||||
# at the next character and possibly go around without consuming another
|
||||
# one at the top of the loop.
|
||||
try:
|
||||
ch, escaped = next(pattern_iter)
|
||||
except StopIteration:
|
||||
return [("", [])]
|
||||
|
||||
try:
|
||||
while True:
|
||||
if escaped:
|
||||
result.append(ch)
|
||||
elif ch == ".":
|
||||
# Replace "any character" with an arbitrary representative.
|
||||
result.append(".")
|
||||
elif ch == "|":
|
||||
# FIXME: One day we'll should do this, but not in 1.0.
|
||||
raise NotImplementedError("Awaiting Implementation")
|
||||
elif ch == "^":
|
||||
pass
|
||||
elif ch == "$":
|
||||
break
|
||||
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).
|
||||
#
|
||||
# We regroup everything inside the capturing group so that it
|
||||
# can be quantified, if necessary.
|
||||
start = non_capturing_groups.pop()
|
||||
inner = NonCapture(result[start:])
|
||||
result = result[:start] + [inner]
|
||||
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 != "]":
|
||||
ch, escaped = next(pattern_iter)
|
||||
elif ch == "(":
|
||||
# Some kind of group.
|
||||
ch, escaped = next(pattern_iter)
|
||||
if ch != "?" or escaped:
|
||||
# A positional group
|
||||
name = "_%d" % num_args
|
||||
num_args += 1
|
||||
result.append(Group((("%%(%s)s" % name), name)))
|
||||
walk_to_end(ch, pattern_iter)
|
||||
else:
|
||||
ch, escaped = next(pattern_iter)
|
||||
if ch in "!=<":
|
||||
# All of these are ignorable. Walk to the end of the
|
||||
# group.
|
||||
walk_to_end(ch, pattern_iter)
|
||||
elif ch == ":":
|
||||
# Non-capturing group
|
||||
non_capturing_groups.append(len(result))
|
||||
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
|
||||
)
|
||||
# We are in a named capturing group. Extra the name and
|
||||
# then skip to the end.
|
||||
if ch == "<":
|
||||
terminal_char = ">"
|
||||
# We are in a named backreference.
|
||||
else:
|
||||
terminal_char = ")"
|
||||
name = []
|
||||
ch, escaped = next(pattern_iter)
|
||||
while ch != terminal_char:
|
||||
name.append(ch)
|
||||
ch, escaped = next(pattern_iter)
|
||||
param = "".join(name)
|
||||
# Named backreferences have already consumed the
|
||||
# parenthesis.
|
||||
if terminal_char != ")":
|
||||
result.append(Group((("%%(%s)s" % param), param)))
|
||||
walk_to_end(ch, pattern_iter)
|
||||
else:
|
||||
result.append(Group((("%%(%s)s" % param), None)))
|
||||
elif ch in "*?+{":
|
||||
# Quantifiers affect the previous item in the result list.
|
||||
count, ch = get_quantifier(ch, pattern_iter)
|
||||
if ch:
|
||||
# We had to look ahead, but it wasn't need to compute the
|
||||
# quantifier, so use this character next time around the
|
||||
# main loop.
|
||||
consume_next = False
|
||||
|
||||
if count == 0:
|
||||
if contains(result[-1], Group):
|
||||
# If we are quantifying a capturing group (or
|
||||
# something containing such a group) and the minimum is
|
||||
# zero, we must also handle the case of one occurrence
|
||||
# being present. All the quantifiers (except {0,0},
|
||||
# which we conveniently ignore) that have a 0 minimum
|
||||
# also allow a single occurrence.
|
||||
result[-1] = Choice([None, result[-1]])
|
||||
else:
|
||||
result.pop()
|
||||
elif count > 1:
|
||||
result.extend([result[-1]] * (count - 1))
|
||||
else:
|
||||
# Anything else is a literal.
|
||||
result.append(ch)
|
||||
|
||||
if consume_next:
|
||||
ch, escaped = next(pattern_iter)
|
||||
consume_next = True
|
||||
except StopIteration:
|
||||
pass
|
||||
except NotImplementedError:
|
||||
# A case of using the disjunctive form. No results for you!
|
||||
return [("", [])]
|
||||
|
||||
return list(zip(*flatten_result(result)))
|
||||
|
||||
|
||||
def next_char(input_iter):
|
||||
r"""
|
||||
An iterator that yields the next character from "pattern_iter", respecting
|
||||
escape sequences. An escaped character is replaced by a representative of
|
||||
its class (e.g. \w -> "x"). If the escaped character is one that is
|
||||
skipped, it is not returned (the next character is returned instead).
|
||||
|
||||
Yield the next character, along with a boolean indicating whether it is a
|
||||
raw (unescaped) character or not.
|
||||
"""
|
||||
for ch in input_iter:
|
||||
if ch != "\\":
|
||||
yield ch, False
|
||||
continue
|
||||
ch = next(input_iter)
|
||||
representative = ESCAPE_MAPPINGS.get(ch, ch)
|
||||
if representative is None:
|
||||
continue
|
||||
yield representative, True
|
||||
|
||||
|
||||
def walk_to_end(ch, input_iter):
|
||||
"""
|
||||
The iterator is currently inside a capturing group. Walk to the close of
|
||||
this group, skipping over any nested groups and handling escaped
|
||||
parentheses correctly.
|
||||
"""
|
||||
if ch == "(":
|
||||
nesting = 1
|
||||
else:
|
||||
nesting = 0
|
||||
for ch, escaped in input_iter:
|
||||
if escaped:
|
||||
continue
|
||||
elif ch == "(":
|
||||
nesting += 1
|
||||
elif ch == ")":
|
||||
if not nesting:
|
||||
return
|
||||
nesting -= 1
|
||||
|
||||
|
||||
def get_quantifier(ch, input_iter):
|
||||
"""
|
||||
Parse a quantifier from the input, where "ch" is the first character in the
|
||||
quantifier.
|
||||
|
||||
Return the minimum number of occurrences permitted by the quantifier and
|
||||
either None or the next character from the input_iter if the next character
|
||||
is not part of the quantifier.
|
||||
"""
|
||||
if ch in "*?+":
|
||||
try:
|
||||
ch2, escaped = next(input_iter)
|
||||
except StopIteration:
|
||||
ch2 = None
|
||||
if ch2 == "?":
|
||||
ch2 = None
|
||||
if ch == "+":
|
||||
return 1, ch2
|
||||
return 0, ch2
|
||||
|
||||
quant = []
|
||||
while ch != "}":
|
||||
ch, escaped = next(input_iter)
|
||||
quant.append(ch)
|
||||
quant = quant[:-1]
|
||||
values = "".join(quant).split(",")
|
||||
|
||||
# Consume the trailing '?', if necessary.
|
||||
try:
|
||||
ch, escaped = next(input_iter)
|
||||
except StopIteration:
|
||||
ch = None
|
||||
if ch == "?":
|
||||
ch = None
|
||||
return int(values[0]), ch
|
||||
|
||||
|
||||
def contains(source, inst):
|
||||
"""
|
||||
Return True if the "source" contains an instance of "inst". False,
|
||||
otherwise.
|
||||
"""
|
||||
if isinstance(source, inst):
|
||||
return True
|
||||
if isinstance(source, NonCapture):
|
||||
for elt in source:
|
||||
if contains(elt, inst):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def flatten_result(source):
|
||||
"""
|
||||
Turn the given source sequence into a list of reg-exp possibilities and
|
||||
their arguments. Return a list of strings and a list of argument lists.
|
||||
Each of the two lists will be of the same length.
|
||||
"""
|
||||
if source is None:
|
||||
return [""], [[]]
|
||||
if isinstance(source, Group):
|
||||
if source[1] is None:
|
||||
params = []
|
||||
else:
|
||||
params = [source[1]]
|
||||
return [source[0]], [params]
|
||||
result = [""]
|
||||
result_args = [[]]
|
||||
pos = last = 0
|
||||
for pos, elt in enumerate(source):
|
||||
if isinstance(elt, str):
|
||||
continue
|
||||
piece = "".join(source[last:pos])
|
||||
if isinstance(elt, Group):
|
||||
piece += elt[0]
|
||||
param = elt[1]
|
||||
else:
|
||||
param = None
|
||||
last = pos + 1
|
||||
for i in range(len(result)):
|
||||
result[i] += piece
|
||||
if param:
|
||||
result_args[i].append(param)
|
||||
if isinstance(elt, (Choice, NonCapture)):
|
||||
if isinstance(elt, NonCapture):
|
||||
elt = [elt]
|
||||
inner_result, inner_args = [], []
|
||||
for item in elt:
|
||||
res, args = flatten_result(item)
|
||||
inner_result.extend(res)
|
||||
inner_args.extend(args)
|
||||
new_result = []
|
||||
new_args = []
|
||||
for item, args in zip(result, result_args):
|
||||
for i_item, i_args in zip(inner_result, inner_args):
|
||||
new_result.append(item + i_item)
|
||||
new_args.append(args[:] + i_args)
|
||||
result = new_result
|
||||
result_args = new_args
|
||||
if pos >= last:
|
||||
piece = "".join(source[last:])
|
||||
for i in range(len(result)):
|
||||
result[i] += piece
|
||||
return result, result_args
|
||||
|
||||
|
||||
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"
|
||||
return regex
|
||||
|
||||
return SimpleLazyObject(_compile)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Functions for working with "safe strings": strings that can be displayed safely
|
||||
without further escaping in HTML. Marking something as a "safe string" means
|
||||
that the producer of the string has already turned characters that should not
|
||||
be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from django.utils.functional import keep_lazy
|
||||
|
||||
|
||||
class SafeData:
|
||||
__slots__ = ()
|
||||
|
||||
def __html__(self):
|
||||
"""
|
||||
Return the html representation of a string for interoperability.
|
||||
|
||||
This allows other template engines to understand Django's SafeData.
|
||||
"""
|
||||
return self
|
||||
|
||||
|
||||
class SafeString(str, SafeData):
|
||||
"""
|
||||
A str subclass that has been specifically marked as "safe" for HTML output
|
||||
purposes.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __add__(self, rhs):
|
||||
"""
|
||||
Concatenating a safe string with another safe bytestring or
|
||||
safe string is safe. Otherwise, the result is no longer safe.
|
||||
"""
|
||||
if isinstance(rhs, str):
|
||||
t = super().__add__(rhs)
|
||||
if isinstance(rhs, SafeData):
|
||||
t = SafeString(t)
|
||||
return t
|
||||
|
||||
# Give the rhs object a chance to handle the addition, for example if
|
||||
# the rhs object's class implements `__radd__`. More details:
|
||||
# https://docs.python.org/3/reference/datamodel.html#object.__radd__
|
||||
return NotImplemented
|
||||
|
||||
def __str__(self):
|
||||
return self
|
||||
|
||||
|
||||
SafeText = SafeString # For backwards compatibility since Django 2.0.
|
||||
|
||||
|
||||
def _safety_decorator(safety_marker, func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return safety_marker(func(*args, **kwargs))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@keep_lazy(SafeString)
|
||||
def mark_safe(s):
|
||||
"""
|
||||
Explicitly mark a string as safe for (HTML) output purposes. The returned
|
||||
object can be used everywhere a string is appropriate.
|
||||
|
||||
If used on a method as a decorator, mark the returned data as safe.
|
||||
|
||||
Can be called multiple times on a single string.
|
||||
"""
|
||||
if hasattr(s, "__html__"):
|
||||
return s
|
||||
if callable(s):
|
||||
return _safety_decorator(mark_safe, s)
|
||||
return SafeString(s)
|
||||
221
.venv/lib/python3.12/site-packages/django/utils/termcolors.py
Normal file
221
.venv/lib/python3.12/site-packages/django/utils/termcolors.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
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)}
|
||||
|
||||
RESET = "0"
|
||||
opt_dict = {
|
||||
"bold": "1",
|
||||
"underscore": "4",
|
||||
"blink": "5",
|
||||
"reverse": "7",
|
||||
"conceal": "8",
|
||||
}
|
||||
|
||||
|
||||
def colorize(text="", opts=(), **kwargs):
|
||||
"""
|
||||
Return your text, enclosed in ANSI graphics codes.
|
||||
|
||||
Depends on the keyword arguments 'fg' and 'bg', and the contents of
|
||||
the opts tuple/list.
|
||||
|
||||
Return the RESET code if no parameters are given.
|
||||
|
||||
Valid colors:
|
||||
'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
|
||||
|
||||
Valid options:
|
||||
'bold'
|
||||
'underscore'
|
||||
'blink'
|
||||
'reverse'
|
||||
'conceal'
|
||||
'noreset' - string will not be auto-terminated with the RESET code
|
||||
|
||||
Examples:
|
||||
colorize('hello', fg='red', bg='blue', opts=('blink',))
|
||||
colorize()
|
||||
colorize('goodbye', opts=('underscore',))
|
||||
print(colorize('first line', fg='red', opts=('noreset',)))
|
||||
print('this should be red too')
|
||||
print(colorize('and so should this'))
|
||||
print('this should not be red')
|
||||
"""
|
||||
code_list = []
|
||||
if text == "" and len(opts) == 1 and opts[0] == "reset":
|
||||
return "\x1b[%sm" % RESET
|
||||
for k, v in kwargs.items():
|
||||
if k == "fg":
|
||||
code_list.append(foreground[v])
|
||||
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 "")
|
||||
|
||||
|
||||
def make_style(opts=(), **kwargs):
|
||||
"""
|
||||
Return a function with default parameters for colorize()
|
||||
|
||||
Example:
|
||||
bold_red = make_style(opts=('bold',), fg='red')
|
||||
print(bold_red('hello'))
|
||||
KEYWORD = make_style(fg='yellow')
|
||||
COMMENT = make_style(fg='blue', opts=('bold',))
|
||||
"""
|
||||
return lambda text: colorize(text, opts, **kwargs)
|
||||
|
||||
|
||||
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": {},
|
||||
},
|
||||
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",)},
|
||||
},
|
||||
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",)},
|
||||
},
|
||||
}
|
||||
DEFAULT_PALETTE = DARK_PALETTE
|
||||
|
||||
|
||||
def parse_color_setting(config_string):
|
||||
"""Parse a DJANGO_COLORS environment variable to produce the system palette
|
||||
|
||||
The general form of a palette definition is:
|
||||
|
||||
"palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option"
|
||||
|
||||
where:
|
||||
palette is a named palette; one of 'light', 'dark', or 'nocolor'.
|
||||
role is a named style used by Django
|
||||
fg is a foreground color.
|
||||
bg is a background color.
|
||||
option is a display options.
|
||||
|
||||
Specifying a named palette is the same as manually specifying the individual
|
||||
definitions for each role. Any individual definitions following the palette
|
||||
definition will augment the base palette definition.
|
||||
|
||||
Valid roles:
|
||||
'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'
|
||||
|
||||
Valid colors:
|
||||
'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
|
||||
|
||||
Valid options:
|
||||
'bold', 'underscore', 'blink', 'reverse', 'conceal', 'noreset'
|
||||
"""
|
||||
if not config_string:
|
||||
return PALETTES[DEFAULT_PALETTE]
|
||||
|
||||
# Split the color configuration into parts
|
||||
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:
|
||||
# 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 = role.upper()
|
||||
|
||||
styles = instructions.split(",")
|
||||
styles.reverse()
|
||||
|
||||
# The first instruction can contain a slash
|
||||
# to break apart fg/bg.
|
||||
colors = styles.pop().split("/")
|
||||
colors.reverse()
|
||||
fg = colors.pop()
|
||||
if fg in color_names:
|
||||
definition["fg"] = fg
|
||||
if colors and colors[-1] in color_names:
|
||||
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
|
||||
|
||||
# The nocolor palette has all available roles.
|
||||
# Use that palette as the basis for determining
|
||||
# if the role is valid.
|
||||
if role in PALETTES[NOCOLOR_PALETTE] and definition:
|
||||
palette[role] = definition
|
||||
|
||||
# If there are no colors specified, return the empty palette.
|
||||
if palette == PALETTES[NOCOLOR_PALETTE]:
|
||||
return None
|
||||
return palette
|
||||
488
.venv/lib/python3.12/site-packages/django/utils/text.py
Normal file
488
.venv/lib/python3.12/site-packages/django/utils/text.py
Normal file
@@ -0,0 +1,488 @@
|
||||
import gzip
|
||||
import re
|
||||
import secrets
|
||||
import textwrap
|
||||
import unicodedata
|
||||
from collections import deque
|
||||
from gzip import GzipFile
|
||||
from gzip import compress as gzip_compress
|
||||
from html import escape
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.utils.functional import (
|
||||
SimpleLazyObject,
|
||||
cached_property,
|
||||
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
|
||||
|
||||
|
||||
@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:]
|
||||
|
||||
|
||||
# Set up regular expressions
|
||||
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
|
||||
def wrap(text, width):
|
||||
"""
|
||||
A word-wrap function that preserves existing line breaks. Expects that
|
||||
existing line breaks are posix newlines.
|
||||
|
||||
Preserve all white space except added line breaks consume the space on
|
||||
which they break the line.
|
||||
|
||||
Don't wrap long words, thus the output text may have lines longer than
|
||||
``width``.
|
||||
"""
|
||||
|
||||
wrapper = textwrap.TextWrapper(
|
||||
width=width,
|
||||
break_long_words=False,
|
||||
break_on_hyphens=False,
|
||||
replace_whitespace=False,
|
||||
)
|
||||
result = []
|
||||
for line in text.splitlines():
|
||||
wrapped = wrapper.wrap(line)
|
||||
if not wrapped:
|
||||
# If `line` contains only whitespaces that are dropped, restore it.
|
||||
result.append(line)
|
||||
else:
|
||||
result.extend(wrapped)
|
||||
if text.endswith("\n"):
|
||||
# If `text` ends with a newline, preserve it.
|
||||
result.append("")
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def add_truncation_text(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}
|
||||
# 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 f"{text}{truncate}"
|
||||
|
||||
|
||||
def calculate_truncate_chars_length(length, replacement):
|
||||
truncate_len = length
|
||||
for char in add_truncation_text("", replacement):
|
||||
if not unicodedata.combining(char):
|
||||
truncate_len -= 1
|
||||
if truncate_len == 0:
|
||||
break
|
||||
return truncate_len
|
||||
|
||||
|
||||
class TruncateHTMLParser(HTMLParser):
|
||||
class TruncationCompleted(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, *, length, replacement, convert_charrefs=True):
|
||||
super().__init__(convert_charrefs=convert_charrefs)
|
||||
self.tags = deque()
|
||||
self.output = ""
|
||||
self.remaining = length
|
||||
self.replacement = replacement
|
||||
|
||||
@cached_property
|
||||
def void_elements(self):
|
||||
from django.utils.html import VOID_ELEMENTS
|
||||
|
||||
return VOID_ELEMENTS
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
self.handle_starttag(tag, attrs)
|
||||
if tag not in self.void_elements:
|
||||
self.handle_endtag(tag)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self.output += self.get_starttag_text()
|
||||
if tag not in self.void_elements:
|
||||
self.tags.appendleft(tag)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag not in self.void_elements:
|
||||
self.output += f"</{tag}>"
|
||||
try:
|
||||
self.tags.remove(tag)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def handle_data(self, data):
|
||||
data, output = self.process(data)
|
||||
data_len = len(data)
|
||||
if self.remaining < data_len:
|
||||
self.remaining = 0
|
||||
self.output += add_truncation_text(output, self.replacement)
|
||||
raise self.TruncationCompleted
|
||||
self.remaining -= data_len
|
||||
self.output += output
|
||||
|
||||
def feed(self, data):
|
||||
try:
|
||||
super().feed(data)
|
||||
except self.TruncationCompleted:
|
||||
self.output += "".join([f"</{tag}>" for tag in self.tags])
|
||||
self.tags.clear()
|
||||
self.reset()
|
||||
else:
|
||||
# No data was handled.
|
||||
self.reset()
|
||||
|
||||
|
||||
class TruncateCharsHTMLParser(TruncateHTMLParser):
|
||||
def __init__(self, *, length, replacement, convert_charrefs=True):
|
||||
self.length = length
|
||||
self.processed_chars = 0
|
||||
super().__init__(
|
||||
length=calculate_truncate_chars_length(length, replacement),
|
||||
replacement=replacement,
|
||||
convert_charrefs=convert_charrefs,
|
||||
)
|
||||
|
||||
def process(self, data):
|
||||
self.processed_chars += len(data)
|
||||
if (self.processed_chars == self.length) and (
|
||||
len(self.output) + len(data) == len(self.rawdata)
|
||||
):
|
||||
self.output += data
|
||||
raise self.TruncationCompleted
|
||||
output = escape("".join(data[: self.remaining]))
|
||||
return data, output
|
||||
|
||||
|
||||
class TruncateWordsHTMLParser(TruncateHTMLParser):
|
||||
def process(self, data):
|
||||
data = re.split(r"(?<=\S)\s+(?=\S)", data)
|
||||
output = escape(" ".join(data[: self.remaining]))
|
||||
return data, output
|
||||
|
||||
|
||||
class Truncator(SimpleLazyObject):
|
||||
"""
|
||||
An object used to truncate text, either by characters or words.
|
||||
|
||||
When truncating HTML text (either chars or words), input will be limited to
|
||||
at most `MAX_LENGTH_HTML` characters.
|
||||
"""
|
||||
|
||||
# 5 million characters are approximately 4000 text pages or 3 web pages.
|
||||
MAX_LENGTH_HTML = 5_000_000
|
||||
|
||||
def __init__(self, text):
|
||||
super().__init__(lambda: str(text))
|
||||
|
||||
def chars(self, num, truncate=None, html=False):
|
||||
"""
|
||||
Return the text truncated to be no longer than the specified number
|
||||
of characters.
|
||||
|
||||
`truncate` specifies what should be used to notify that the string has
|
||||
been truncated, defaulting to a translatable string of an ellipsis.
|
||||
"""
|
||||
self._setup()
|
||||
length = int(num)
|
||||
if length <= 0:
|
||||
return ""
|
||||
text = unicodedata.normalize("NFC", self._wrapped)
|
||||
|
||||
if html:
|
||||
parser = TruncateCharsHTMLParser(length=length, replacement=truncate)
|
||||
parser.feed(text)
|
||||
parser.close()
|
||||
return parser.output
|
||||
return self._text_chars(length, truncate, text)
|
||||
|
||||
def _text_chars(self, length, truncate, text):
|
||||
"""Truncate a string after a certain number of chars."""
|
||||
truncate_len = calculate_truncate_chars_length(length, truncate)
|
||||
s_len = 0
|
||||
end_index = None
|
||||
for i, char in enumerate(text):
|
||||
if unicodedata.combining(char):
|
||||
# Don't consider combining characters
|
||||
# as adding to the string length
|
||||
continue
|
||||
s_len += 1
|
||||
if end_index is None and s_len > truncate_len:
|
||||
end_index = i
|
||||
if s_len > length:
|
||||
# Return the truncated string
|
||||
return add_truncation_text(text[: end_index or 0], truncate)
|
||||
|
||||
# Return the original string since no truncation was necessary
|
||||
return text
|
||||
|
||||
def words(self, num, truncate=None, html=False):
|
||||
"""
|
||||
Truncate a string after a certain number of words. `truncate` specifies
|
||||
what should be used to notify that the string has been truncated,
|
||||
defaulting to ellipsis.
|
||||
"""
|
||||
self._setup()
|
||||
length = int(num)
|
||||
if length <= 0:
|
||||
return ""
|
||||
if html:
|
||||
parser = TruncateWordsHTMLParser(length=length, replacement=truncate)
|
||||
parser.feed(self._wrapped)
|
||||
parser.close()
|
||||
return parser.output
|
||||
return self._text_words(length, truncate)
|
||||
|
||||
def _text_words(self, length, truncate):
|
||||
"""
|
||||
Truncate a string after a certain number of words.
|
||||
|
||||
Strip newlines in the string.
|
||||
"""
|
||||
words = self._wrapped.split()
|
||||
if len(words) > length:
|
||||
words = words[:length]
|
||||
return add_truncation_text(" ".join(words), truncate)
|
||||
return " ".join(words)
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def get_valid_filename(name):
|
||||
"""
|
||||
Return the given string converted to a string that can be used for a clean
|
||||
filename. Remove leading and trailing spaces; convert other spaces to
|
||||
underscores; and remove anything that is not an alphanumeric, dash,
|
||||
underscore, or dot.
|
||||
>>> 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 {"", ".", ".."}:
|
||||
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")):
|
||||
"""
|
||||
>>> get_text_list(['a', 'b', 'c', 'd'])
|
||||
'a, b, c or d'
|
||||
>>> get_text_list(['a', 'b', 'c'], 'and')
|
||||
'a, b and c'
|
||||
>>> get_text_list(['a', 'b'], 'and')
|
||||
'a and b'
|
||||
>>> get_text_list(['a'])
|
||||
'a'
|
||||
>>> get_text_list([])
|
||||
''
|
||||
"""
|
||||
if not list_:
|
||||
return ""
|
||||
if len(list_) == 1:
|
||||
return str(list_[0])
|
||||
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]),
|
||||
)
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def normalize_newlines(text):
|
||||
"""Normalize CRLF and CR newlines to just LF."""
|
||||
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",
|
||||
}
|
||||
return "".join(char2number.get(c, c) for c in phone.lower())
|
||||
|
||||
|
||||
def _get_random_filename(max_random_bytes):
|
||||
return b"a" * secrets.randbelow(max_random_bytes)
|
||||
|
||||
|
||||
def compress_string(s, *, max_random_bytes=None):
|
||||
compressed_data = gzip_compress(s, compresslevel=6, mtime=0)
|
||||
|
||||
if not max_random_bytes:
|
||||
return compressed_data
|
||||
|
||||
compressed_view = memoryview(compressed_data)
|
||||
header = bytearray(compressed_view[:10])
|
||||
header[3] = gzip.FNAME
|
||||
|
||||
filename = _get_random_filename(max_random_bytes) + b"\x00"
|
||||
|
||||
return bytes(header) + filename + compressed_view[10:]
|
||||
|
||||
|
||||
class StreamingBuffer(BytesIO):
|
||||
def read(self):
|
||||
ret = self.getvalue()
|
||||
self.seek(0)
|
||||
self.truncate()
|
||||
return ret
|
||||
|
||||
|
||||
# Like compress_string, but for iterators of strings.
|
||||
def compress_sequence(sequence, *, max_random_bytes=None):
|
||||
buf = StreamingBuffer()
|
||||
filename = _get_random_filename(max_random_bytes) if max_random_bytes else None
|
||||
with GzipFile(
|
||||
filename=filename, mode="wb", compresslevel=6, fileobj=buf, mtime=0
|
||||
) as zfile:
|
||||
# Output headers...
|
||||
yield buf.read()
|
||||
for item in sequence:
|
||||
zfile.write(item)
|
||||
data = buf.read()
|
||||
if data:
|
||||
yield data
|
||||
yield buf.read()
|
||||
|
||||
|
||||
# Expression to match some_token and some_token="with spaces" (and similarly
|
||||
# for single-quoted strings).
|
||||
smart_split_re = _lazy_re_compile(
|
||||
r"""
|
||||
((?:
|
||||
[^\s'"]*
|
||||
(?:
|
||||
(?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*')
|
||||
[^\s'"]*
|
||||
)+
|
||||
) | \S+)
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def smart_split(text):
|
||||
r"""
|
||||
Generator that splits a string by spaces, leaving quoted phrases together.
|
||||
Supports both single and double quotes, and supports escaping quotes with
|
||||
backslashes. In the output, strings will keep their initial and trailing
|
||||
quote marks and escaped quotes will remain escaped (the results can then
|
||||
be further processed with unescape_string_literal()).
|
||||
|
||||
>>> list(smart_split(r'This is "a person\'s" test.'))
|
||||
['This', 'is', '"a person\\\'s"', 'test.']
|
||||
>>> list(smart_split(r"Another 'person\'s' test."))
|
||||
['Another', "'person\\'s'", 'test.']
|
||||
>>> list(smart_split(r'A "\"funky\" style" test.'))
|
||||
['A', '"\\"funky\\" style"', 'test.']
|
||||
"""
|
||||
for bit in smart_split_re.finditer(str(text)):
|
||||
yield bit[0]
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def unescape_string_literal(s):
|
||||
r"""
|
||||
Convert quoted string literals to unquoted strings with escaped quotes and
|
||||
backslashes unquoted::
|
||||
|
||||
>>> unescape_string_literal('"abc"')
|
||||
'abc'
|
||||
>>> unescape_string_literal("'abc'")
|
||||
'abc'
|
||||
>>> unescape_string_literal('"a \"bc\""')
|
||||
'a "bc"'
|
||||
>>> unescape_string_literal("'\'ab\' c'")
|
||||
"'ab' c"
|
||||
"""
|
||||
if not s or 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"\\", "\\")
|
||||
|
||||
|
||||
@keep_lazy_text
|
||||
def slugify(value, allow_unicode=False):
|
||||
"""
|
||||
Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
|
||||
dashes to single dashes. Remove characters that aren't alphanumerics,
|
||||
underscores, or hyphens. Convert to lowercase. Also strip leading and
|
||||
trailing whitespace, dashes, and underscores.
|
||||
"""
|
||||
value = str(value)
|
||||
if allow_unicode:
|
||||
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("-_")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def _format_lazy(format_string, *args, **kwargs):
|
||||
"""
|
||||
Apply str.format() on 'format_string' where format_string, args,
|
||||
and/or kwargs might be lazy.
|
||||
"""
|
||||
return format_string.format(*args, **kwargs)
|
||||
|
||||
|
||||
format_lazy = lazy(_format_lazy, str)
|
||||
142
.venv/lib/python3.12/site-packages/django/utils/timesince.py
Normal file
142
.venv/lib/python3.12/site-packages/django/utils/timesince.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import datetime
|
||||
|
||||
from django.utils.html import avoid_wrapping
|
||||
from django.utils.timezone import is_aware
|
||||
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"),
|
||||
}
|
||||
|
||||
TIME_STRINGS_KEYS = list(TIME_STRINGS.keys())
|
||||
|
||||
TIME_CHUNKS = [
|
||||
60 * 60 * 24 * 7, # week
|
||||
60 * 60 * 24, # day
|
||||
60 * 60, # hour
|
||||
60, # minute
|
||||
]
|
||||
|
||||
MONTHS_DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||
|
||||
|
||||
def timesince(d, now=None, reversed=False, time_strings=None, depth=2):
|
||||
"""
|
||||
Take two datetime objects and return the time between d and now as a nicely
|
||||
formatted string, e.g. "10 minutes". If d occurs after now, return
|
||||
"0 minutes".
|
||||
|
||||
Units used are years, months, weeks, days, hours, and minutes.
|
||||
Seconds and microseconds are ignored.
|
||||
|
||||
The algorithm takes into account the varying duration of years and months.
|
||||
There is exactly "1 year, 1 month" between 2013/02/10 and 2014/03/10,
|
||||
but also between 2007/08/10 and 2008/09/10 despite the delta being 393 days
|
||||
in the former case and 397 in the latter.
|
||||
|
||||
Up to `depth` adjacent units will be displayed. For example,
|
||||
"2 weeks, 3 days" and "1 year, 3 months" are possible outputs, but
|
||||
"2 weeks, 3 hours" and "1 year, 5 days" are not.
|
||||
|
||||
`time_strings` is an optional dict of strings to replace the default
|
||||
TIME_STRINGS dict.
|
||||
|
||||
`depth` is an optional integer to control the number of adjacent time
|
||||
units returned.
|
||||
|
||||
Originally adapted from
|
||||
https://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
|
||||
Modified to improve results for years and months.
|
||||
"""
|
||||
if time_strings is None:
|
||||
time_strings = TIME_STRINGS
|
||||
if depth <= 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)
|
||||
if now and not isinstance(now, datetime.datetime):
|
||||
now = datetime.datetime(now.year, now.month, now.day)
|
||||
|
||||
# Compared datetimes must be in the same time zone.
|
||||
if not now:
|
||||
now = datetime.datetime.now(d.tzinfo if is_aware(d) else None)
|
||||
elif is_aware(now) and is_aware(d):
|
||||
now = now.astimezone(d.tzinfo)
|
||||
|
||||
if reversed:
|
||||
d, now = now, d
|
||||
delta = now - d
|
||||
|
||||
# Ignore microseconds.
|
||||
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})
|
||||
|
||||
# Get years and months.
|
||||
total_months = (now.year - d.year) * 12 + (now.month - d.month)
|
||||
if d.day > now.day or (d.day == now.day and d.time() > now.time()):
|
||||
total_months -= 1
|
||||
years, months = divmod(total_months, 12)
|
||||
|
||||
# Calculate the remaining time.
|
||||
# Create a "pivot" datetime shifted from d by years and months, then use
|
||||
# that to determine the other parts.
|
||||
if years or months:
|
||||
pivot_year = d.year + years
|
||||
pivot_month = d.month + months
|
||||
if pivot_month > 12:
|
||||
pivot_month -= 12
|
||||
pivot_year += 1
|
||||
pivot = datetime.datetime(
|
||||
pivot_year,
|
||||
pivot_month,
|
||||
min(MONTHS_DAYS[pivot_month - 1], d.day),
|
||||
d.hour,
|
||||
d.minute,
|
||||
d.second,
|
||||
tzinfo=d.tzinfo,
|
||||
)
|
||||
else:
|
||||
pivot = d
|
||||
remaining_time = (now - pivot).total_seconds()
|
||||
partials = [years, months]
|
||||
for chunk in TIME_CHUNKS:
|
||||
count = int(remaining_time // chunk)
|
||||
partials.append(count)
|
||||
remaining_time -= chunk * count
|
||||
|
||||
# Find the first non-zero part (if any) and then build the result, until
|
||||
# depth.
|
||||
i = 0
|
||||
for i, value in enumerate(partials):
|
||||
if value != 0:
|
||||
break
|
||||
else:
|
||||
return avoid_wrapping(time_strings["minute"] % {"num": 0})
|
||||
|
||||
result = []
|
||||
current_depth = 0
|
||||
while i < len(TIME_STRINGS_KEYS) and current_depth < depth:
|
||||
value = partials[i]
|
||||
if value == 0:
|
||||
break
|
||||
name = TIME_STRINGS_KEYS[i]
|
||||
result.append(avoid_wrapping(time_strings[name] % {"num": value}))
|
||||
current_depth += 1
|
||||
i += 1
|
||||
|
||||
return gettext(", ").join(result)
|
||||
|
||||
|
||||
def timeuntil(d, now=None, time_strings=None, depth=2):
|
||||
"""
|
||||
Like timesince, but return a string measuring the time until the given time.
|
||||
"""
|
||||
return timesince(d, now, reversed=True, time_strings=time_strings, depth=depth)
|
||||
259
.venv/lib/python3.12/site-packages/django/utils/timezone.py
Normal file
259
.venv/lib/python3.12/site-packages/django/utils/timezone.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Timezone-related classes and functions.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import zoneinfo
|
||||
from contextlib import ContextDecorator
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
|
||||
from asgiref.local import Local
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
__all__ = [
|
||||
"get_fixed_timezone",
|
||||
"get_default_timezone",
|
||||
"get_default_timezone_name",
|
||||
"get_current_timezone",
|
||||
"get_current_timezone_name",
|
||||
"activate",
|
||||
"deactivate",
|
||||
"override",
|
||||
"localtime",
|
||||
"localdate",
|
||||
"now",
|
||||
"is_aware",
|
||||
"is_naive",
|
||||
"make_aware",
|
||||
"make_naive",
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
name = sign + hhmm
|
||||
return timezone(timedelta(minutes=offset), name)
|
||||
|
||||
|
||||
# In order to avoid accessing settings at compile time,
|
||||
# wrap the logic in a function and cache the result.
|
||||
@functools.lru_cache
|
||||
def get_default_timezone():
|
||||
"""
|
||||
Return the default time zone as a tzinfo instance.
|
||||
|
||||
This is the time zone defined by settings.TIME_ZONE.
|
||||
"""
|
||||
return zoneinfo.ZoneInfo(settings.TIME_ZONE)
|
||||
|
||||
|
||||
# This function exists for consistency with get_current_timezone_name
|
||||
def get_default_timezone_name():
|
||||
"""Return the name of the default time zone."""
|
||||
return _get_timezone_name(get_default_timezone())
|
||||
|
||||
|
||||
_active = Local()
|
||||
|
||||
|
||||
def get_current_timezone():
|
||||
"""Return the currently active time zone as a tzinfo instance."""
|
||||
return getattr(_active, "value", get_default_timezone())
|
||||
|
||||
|
||||
def get_current_timezone_name():
|
||||
"""Return the name of the currently active time zone."""
|
||||
return _get_timezone_name(get_current_timezone())
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Timezone selection functions.
|
||||
|
||||
# These functions don't change os.environ['TZ'] and call time.tzset()
|
||||
# because it isn't thread safe.
|
||||
|
||||
|
||||
def activate(timezone):
|
||||
"""
|
||||
Set the time zone for the current thread.
|
||||
|
||||
The ``timezone`` argument must be an instance of a tzinfo subclass or a
|
||||
time zone name.
|
||||
"""
|
||||
if isinstance(timezone, tzinfo):
|
||||
_active.value = timezone
|
||||
elif isinstance(timezone, str):
|
||||
_active.value = zoneinfo.ZoneInfo(timezone)
|
||||
else:
|
||||
raise ValueError("Invalid timezone: %r" % timezone)
|
||||
|
||||
|
||||
def deactivate():
|
||||
"""
|
||||
Unset the time zone for the current thread.
|
||||
|
||||
Django will then use the time zone defined by settings.TIME_ZONE.
|
||||
"""
|
||||
if hasattr(_active, "value"):
|
||||
del _active.value
|
||||
|
||||
|
||||
class override(ContextDecorator):
|
||||
"""
|
||||
Temporarily set the time zone for the current thread.
|
||||
|
||||
This is a context manager that uses django.utils.timezone.activate()
|
||||
to set the timezone on entry and restores the previously active timezone
|
||||
on exit.
|
||||
|
||||
The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a
|
||||
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)
|
||||
if self.timezone is None:
|
||||
deactivate()
|
||||
else:
|
||||
activate(self.timezone)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.old_timezone is None:
|
||||
deactivate()
|
||||
else:
|
||||
_active.value = self.old_timezone
|
||||
|
||||
|
||||
# Templates
|
||||
|
||||
|
||||
def template_localtime(value, use_tz=None):
|
||||
"""
|
||||
Check if value is a datetime and converts it to local time if necessary.
|
||||
|
||||
If use_tz is provided and is not None, that will force the value to
|
||||
be converted (or not), overriding the value of settings.USE_TZ.
|
||||
|
||||
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)
|
||||
)
|
||||
return localtime(value) if should_convert else value
|
||||
|
||||
|
||||
# Utilities
|
||||
|
||||
|
||||
def localtime(value=None, timezone=None):
|
||||
"""
|
||||
Convert an aware datetime.datetime to local time.
|
||||
|
||||
Only aware datetimes are allowed. When value is omitted, it defaults to
|
||||
now().
|
||||
|
||||
Local time is defined by the current time zone, unless another time zone
|
||||
is specified.
|
||||
"""
|
||||
if value is None:
|
||||
value = now()
|
||||
if timezone is None:
|
||||
timezone = get_current_timezone()
|
||||
# Emulate the behavior of astimezone() on Python < 3.6.
|
||||
if is_naive(value):
|
||||
raise ValueError("localtime() cannot be applied to a naive datetime")
|
||||
return value.astimezone(timezone)
|
||||
|
||||
|
||||
def localdate(value=None, timezone=None):
|
||||
"""
|
||||
Convert an aware datetime to local time and return the value's date.
|
||||
|
||||
Only aware datetimes are allowed. When value is omitted, it defaults to
|
||||
now().
|
||||
|
||||
Local time is defined by the current time zone, unless another time zone is
|
||||
specified.
|
||||
"""
|
||||
return localtime(value, timezone).date()
|
||||
|
||||
|
||||
def now():
|
||||
"""
|
||||
Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
|
||||
"""
|
||||
return datetime.now(tz=timezone.utc if settings.USE_TZ else None)
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
The concept is defined in Python's docs:
|
||||
https://docs.python.org/library/datetime.html#datetime.tzinfo
|
||||
|
||||
Assuming value.tzinfo is either None or a proper datetime.tzinfo,
|
||||
value.utcoffset() implements the appropriate logic.
|
||||
"""
|
||||
return value.utcoffset() is not None
|
||||
|
||||
|
||||
def is_naive(value):
|
||||
"""
|
||||
Determine if a given datetime.datetime is naive.
|
||||
|
||||
The concept is defined in Python's docs:
|
||||
https://docs.python.org/library/datetime.html#datetime.tzinfo
|
||||
|
||||
Assuming value.tzinfo is either None or a proper datetime.tzinfo,
|
||||
value.utcoffset() implements the appropriate logic.
|
||||
"""
|
||||
return value.utcoffset() is None
|
||||
|
||||
|
||||
def make_aware(value, timezone=None):
|
||||
"""Make a naive datetime.datetime in a given time zone aware."""
|
||||
if timezone is None:
|
||||
timezone = get_current_timezone()
|
||||
# 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)
|
||||
# This may be wrong around DST changes!
|
||||
return value.replace(tzinfo=timezone)
|
||||
|
||||
|
||||
def make_naive(value, timezone=None):
|
||||
"""Make an aware datetime.datetime naive in a given time zone."""
|
||||
if timezone is None:
|
||||
timezone = get_current_timezone()
|
||||
# Emulate the behavior of astimezone() on Python < 3.6.
|
||||
if is_naive(value):
|
||||
raise ValueError("make_naive() cannot be applied to a naive datetime")
|
||||
return value.astimezone(timezone).replace(tzinfo=None)
|
||||
|
||||
|
||||
def _datetime_ambiguous_or_imaginary(dt, tz):
|
||||
return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
Internationalization support.
|
||||
"""
|
||||
|
||||
from contextlib import ContextDecorator
|
||||
from decimal import ROUND_UP, Decimal
|
||||
|
||||
from django.utils.autoreload import autoreload_started, file_changed
|
||||
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",
|
||||
]
|
||||
|
||||
|
||||
class TranslatorCommentWarning(SyntaxWarning):
|
||||
pass
|
||||
|
||||
|
||||
# Here be dragons, so a short explanation of the logic won't hurt:
|
||||
# We are trying to solve two problems: (1) access settings, in particular
|
||||
# settings.USE_I18N, as late as possible, so that modules can be imported
|
||||
# without having to first configure Django, and (2) if some other code creates
|
||||
# a reference to one of these functions, don't break that reference when we
|
||||
# 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
|
||||
receiving the first call to that function. After this is done, changes to
|
||||
USE_I18N will have no effect to which function is served upon request. If
|
||||
your tests rely on changing USE_I18N, you can delete all the functions
|
||||
from _trans.__dict__.
|
||||
|
||||
Note that storing the function with setattr will have a noticeable
|
||||
performance effect, as access to the function goes the normal path,
|
||||
instead of using __getattr__.
|
||||
"""
|
||||
|
||||
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"
|
||||
)
|
||||
else:
|
||||
from django.utils.translation import trans_null as trans
|
||||
setattr(self, real_name, getattr(trans, real_name))
|
||||
return getattr(trans, real_name)
|
||||
|
||||
|
||||
_trans = Trans()
|
||||
|
||||
# The Trans class is no more needed, so remove it from the namespace.
|
||||
del Trans
|
||||
|
||||
|
||||
def gettext_noop(message):
|
||||
return _trans.gettext_noop(message)
|
||||
|
||||
|
||||
def gettext(message):
|
||||
return _trans.gettext(message)
|
||||
|
||||
|
||||
def ngettext(singular, plural, number):
|
||||
return _trans.ngettext(singular, plural, number)
|
||||
|
||||
|
||||
def pgettext(context, message):
|
||||
return _trans.pgettext(context, message)
|
||||
|
||||
|
||||
def npgettext(context, singular, plural, number):
|
||||
return _trans.npgettext(context, singular, plural, number)
|
||||
|
||||
|
||||
gettext_lazy = lazy(gettext, str)
|
||||
pgettext_lazy = lazy(pgettext, str)
|
||||
|
||||
|
||||
def lazy_number(func, resultclass, number=None, **kwargs):
|
||||
if isinstance(number, int):
|
||||
kwargs["number"] = number
|
||||
proxy = lazy(func, resultclass)(**kwargs)
|
||||
else:
|
||||
original_kwargs = kwargs.copy()
|
||||
|
||||
class NumberAwareString(resultclass):
|
||||
def __bool__(self):
|
||||
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 "
|
||||
"it, because it is required to determine whether "
|
||||
"string is singular or plural." % number
|
||||
)
|
||||
|
||||
def _translate(self, 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]
|
||||
)
|
||||
return self._translate(number_value).format(*args, **kwargs)
|
||||
|
||||
def __mod__(self, rhs):
|
||||
if isinstance(rhs, dict) and number:
|
||||
number_value = self._get_number_value(rhs)
|
||||
else:
|
||||
number_value = rhs
|
||||
translated = self._translate(number_value)
|
||||
try:
|
||||
translated %= rhs
|
||||
except TypeError:
|
||||
# String doesn't contain a placeholder for the number.
|
||||
pass
|
||||
return translated
|
||||
|
||||
proxy = lazy(lambda **kwargs: NumberAwareString(), NumberAwareString)(**kwargs)
|
||||
proxy.__reduce__ = lambda: (
|
||||
_lazy_number_unpickle,
|
||||
(func, resultclass, number, original_kwargs),
|
||||
)
|
||||
return proxy
|
||||
|
||||
|
||||
def _lazy_number_unpickle(func, resultclass, number, kwargs):
|
||||
return lazy_number(func, resultclass, number=number, **kwargs)
|
||||
|
||||
|
||||
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 activate(language):
|
||||
return _trans.activate(language)
|
||||
|
||||
|
||||
def deactivate():
|
||||
return _trans.deactivate()
|
||||
|
||||
|
||||
class override(ContextDecorator):
|
||||
def __init__(self, language, deactivate=False):
|
||||
self.language = language
|
||||
self.deactivate = deactivate
|
||||
|
||||
def __enter__(self):
|
||||
self.old_language = get_language()
|
||||
if self.language is not None:
|
||||
activate(self.language)
|
||||
else:
|
||||
deactivate_all()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if self.old_language is None:
|
||||
deactivate_all()
|
||||
elif self.deactivate:
|
||||
deactivate()
|
||||
else:
|
||||
activate(self.old_language)
|
||||
|
||||
|
||||
def get_language():
|
||||
return _trans.get_language()
|
||||
|
||||
|
||||
def get_language_bidi():
|
||||
return _trans.get_language_bidi()
|
||||
|
||||
|
||||
def check_for_language(lang_code):
|
||||
return _trans.check_for_language(lang_code)
|
||||
|
||||
|
||||
def to_language(locale):
|
||||
"""Turn a locale name (en_US) into a language name (en-us)."""
|
||||
p = locale.find("_")
|
||||
if p >= 0:
|
||||
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("-")
|
||||
if not country:
|
||||
return language[:3].lower() + language[3:]
|
||||
# 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 = country.title() if len(country) > 2 else country.upper()
|
||||
if tail:
|
||||
country += "-" + tail
|
||||
return lang + "_" + country
|
||||
|
||||
|
||||
def get_language_from_request(request, check_path=False):
|
||||
return _trans.get_language_from_request(request, check_path)
|
||||
|
||||
|
||||
def get_language_from_path(path):
|
||||
return _trans.get_language_from_path(path)
|
||||
|
||||
|
||||
def get_supported_language_variant(lang_code, *, strict=False):
|
||||
return _trans.get_supported_language_variant(lang_code, strict)
|
||||
|
||||
|
||||
def templatize(src, **kwargs):
|
||||
from .template import templatize
|
||||
|
||||
return templatize(src, **kwargs)
|
||||
|
||||
|
||||
def deactivate_all():
|
||||
return _trans.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])
|
||||
else:
|
||||
info = lang_info
|
||||
except KeyError:
|
||||
if "-" not in lang_code:
|
||||
raise KeyError("Unknown language code %s." % lang_code)
|
||||
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)
|
||||
)
|
||||
|
||||
if info:
|
||||
info["name_translated"] = gettext_lazy(info["name"])
|
||||
return info
|
||||
|
||||
|
||||
trim_whitespace_re = _lazy_re_compile(r"\s*\n\s*")
|
||||
|
||||
|
||||
def trim_whitespace(s):
|
||||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
from pathlib import Path
|
||||
|
||||
from asgiref.local import Local
|
||||
|
||||
from django.apps import apps
|
||||
from django.utils.autoreload import is_django_module
|
||||
|
||||
|
||||
def watch_for_translation_changes(sender, **kwargs):
|
||||
"""Register file watchers for .mo files in potential locale paths."""
|
||||
from django.conf import settings
|
||||
|
||||
if settings.USE_I18N:
|
||||
directories = [Path("locale")]
|
||||
directories.extend(
|
||||
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")
|
||||
|
||||
|
||||
def translation_file_changed(sender, file_path, **kwargs):
|
||||
"""Clear the internal translations cache if a .mo file is modified."""
|
||||
if file_path.suffix == ".mo":
|
||||
import gettext
|
||||
|
||||
from django.utils.translation import trans_real
|
||||
|
||||
gettext._translations = {}
|
||||
trans_real._translations = {}
|
||||
trans_real._default = None
|
||||
trans_real._active = Local()
|
||||
return True
|
||||
@@ -0,0 +1,246 @@
|
||||
import warnings
|
||||
from io import StringIO
|
||||
|
||||
from django.template.base import 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")
|
||||
|
||||
|
||||
def blankout(src, char):
|
||||
"""
|
||||
Change every non-whitespace character to the given char.
|
||||
Used in the templatize function.
|
||||
"""
|
||||
return dot_re.sub(char, src)
|
||||
|
||||
|
||||
context_re = _lazy_re_compile(r"""^\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?'))\s*""")
|
||||
inline_re = _lazy_re_compile(
|
||||
# Match the trans/translate 'some text' part.
|
||||
r"""^\s*trans(?:late)?\s+((?:"[^"]*?")|(?:'[^']*?'))"""
|
||||
# Match and ignore optional filters
|
||||
r"""(?:\s*\|\s*[^\s:]+(?::(?:[^\s'":]+|(?:"[^"]*?")|(?:'[^']*?')))?)*"""
|
||||
# Match the optional context part
|
||||
r"""(\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"""_\(((?:".*?")|(?:'.*?'))\)""")
|
||||
|
||||
|
||||
def templatize(src, origin=None):
|
||||
"""
|
||||
Turn a Django template into something that is understood by xgettext. It
|
||||
does so by translating the Django translation tags into standard gettext
|
||||
function invocations.
|
||||
"""
|
||||
out = StringIO("")
|
||||
message_context = None
|
||||
intrans = False
|
||||
inplural = False
|
||||
trimmed = False
|
||||
singular = []
|
||||
plural = []
|
||||
incomment = False
|
||||
comment = []
|
||||
lineno_comment_map = {}
|
||||
comment_lineno_cache = None
|
||||
# Adding the u prefix allows gettext to recognize the string (#26093).
|
||||
raw_prefix = "u"
|
||||
|
||||
def join_tokens(tokens, trim=False):
|
||||
message = "".join(tokens)
|
||||
if trim:
|
||||
message = trim_whitespace(message)
|
||||
return message
|
||||
|
||||
for t in Lexer(src).tokenize():
|
||||
if incomment:
|
||||
if t.token_type == TokenType.BLOCK and t.contents == "endcomment":
|
||||
content = "".join(comment)
|
||||
translators_comment_start = None
|
||||
for lineno, line in enumerate(content.splitlines(True)):
|
||||
if line.lstrip().startswith(TRANSLATOR_COMMENT_MARK):
|
||||
translators_comment_start = lineno
|
||||
for lineno, line in enumerate(content.splitlines(True)):
|
||||
if (
|
||||
translators_comment_start is not None
|
||||
and lineno >= translators_comment_start
|
||||
):
|
||||
out.write(" # %s" % line)
|
||||
else:
|
||||
out.write(" #\n")
|
||||
incomment = False
|
||||
comment = []
|
||||
else:
|
||||
comment.append(t.contents)
|
||||
elif intrans:
|
||||
if t.token_type == TokenType.BLOCK:
|
||||
endbmatch = endblock_re.match(t.contents)
|
||||
pluralmatch = plural_re.match(t.contents)
|
||||
if endbmatch:
|
||||
if inplural:
|
||||
if message_context:
|
||||
out.write(
|
||||
" npgettext({p}{!r}, {p}{!r}, {p}{!r},count) ".format(
|
||||
message_context,
|
||||
join_tokens(singular, trimmed),
|
||||
join_tokens(plural, trimmed),
|
||||
p=raw_prefix,
|
||||
)
|
||||
)
|
||||
else:
|
||||
out.write(
|
||||
" ngettext({p}{!r}, {p}{!r}, count) ".format(
|
||||
join_tokens(singular, trimmed),
|
||||
join_tokens(plural, trimmed),
|
||||
p=raw_prefix,
|
||||
)
|
||||
)
|
||||
for part in singular:
|
||||
out.write(blankout(part, "S"))
|
||||
for part in plural:
|
||||
out.write(blankout(part, "P"))
|
||||
else:
|
||||
if message_context:
|
||||
out.write(
|
||||
" pgettext({p}{!r}, {p}{!r}) ".format(
|
||||
message_context,
|
||||
join_tokens(singular, trimmed),
|
||||
p=raw_prefix,
|
||||
)
|
||||
)
|
||||
else:
|
||||
out.write(
|
||||
" gettext({p}{!r}) ".format(
|
||||
join_tokens(singular, trimmed),
|
||||
p=raw_prefix,
|
||||
)
|
||||
)
|
||||
for part in singular:
|
||||
out.write(blankout(part, "S"))
|
||||
message_context = None
|
||||
intrans = False
|
||||
inplural = False
|
||||
singular = []
|
||||
plural = []
|
||||
elif pluralmatch:
|
||||
inplural = True
|
||||
else:
|
||||
filemsg = ""
|
||||
if origin:
|
||||
filemsg = "file %s, " % origin
|
||||
raise SyntaxError(
|
||||
"Translation blocks must not include other block tags: "
|
||||
"%s (%sline %d)" % (t.contents, filemsg, t.lineno)
|
||||
)
|
||||
elif t.token_type == TokenType.VAR:
|
||||
if inplural:
|
||||
plural.append("%%(%s)s" % t.contents)
|
||||
else:
|
||||
singular.append("%%(%s)s" % t.contents)
|
||||
elif t.token_type == TokenType.TEXT:
|
||||
contents = t.contents.replace("%", "%%")
|
||||
if inplural:
|
||||
plural.append(contents)
|
||||
else:
|
||||
singular.append(contents)
|
||||
else:
|
||||
# Handle comment tokens (`{# ... #}`) plus other constructs on
|
||||
# the same line:
|
||||
if comment_lineno_cache is not None:
|
||||
cur_lineno = t.lineno + t.contents.count("\n")
|
||||
if comment_lineno_cache == cur_lineno:
|
||||
if t.token_type != TokenType.COMMENT:
|
||||
for c in lineno_comment_map[comment_lineno_cache]:
|
||||
filemsg = ""
|
||||
if origin:
|
||||
filemsg = "file %s, " % origin
|
||||
warn_msg = (
|
||||
"The translator-targeted comment '%s' "
|
||||
"(%sline %d) was ignored, because it wasn't "
|
||||
"the last item on the line."
|
||||
) % (c, filemsg, comment_lineno_cache)
|
||||
warnings.warn(warn_msg, TranslatorCommentWarning)
|
||||
lineno_comment_map[comment_lineno_cache] = []
|
||||
else:
|
||||
out.write(
|
||||
"# %s" % " | ".join(lineno_comment_map[comment_lineno_cache])
|
||||
)
|
||||
comment_lineno_cache = None
|
||||
|
||||
if t.token_type == TokenType.BLOCK:
|
||||
imatch = inline_re.match(t.contents)
|
||||
bmatch = block_re.match(t.contents)
|
||||
cmatches = constant_re.findall(t.contents)
|
||||
if imatch:
|
||||
g = imatch[1]
|
||||
if g[0] == '"':
|
||||
g = g.strip('"')
|
||||
elif g[0] == "'":
|
||||
g = g.strip("'")
|
||||
g = g.replace("%", "%%")
|
||||
if imatch[2]:
|
||||
# A context is provided
|
||||
context_match = context_re.match(imatch[2])
|
||||
message_context = context_match[1]
|
||||
if message_context[0] == '"':
|
||||
message_context = message_context.strip('"')
|
||||
elif message_context[0] == "'":
|
||||
message_context = message_context.strip("'")
|
||||
out.write(
|
||||
" pgettext({p}{!r}, {p}{!r}) ".format(
|
||||
message_context, g, p=raw_prefix
|
||||
)
|
||||
)
|
||||
message_context = None
|
||||
else:
|
||||
out.write(" gettext({p}{!r}) ".format(g, p=raw_prefix))
|
||||
elif bmatch:
|
||||
for fmatch in constant_re.findall(t.contents):
|
||||
out.write(" _(%s) " % fmatch)
|
||||
if bmatch[1]:
|
||||
# A context is provided
|
||||
context_match = context_re.match(bmatch[1])
|
||||
message_context = context_match[1]
|
||||
if message_context[0] == '"':
|
||||
message_context = message_context.strip('"')
|
||||
elif message_context[0] == "'":
|
||||
message_context = message_context.strip("'")
|
||||
intrans = True
|
||||
inplural = False
|
||||
trimmed = "trimmed" in t.split_contents()
|
||||
singular = []
|
||||
plural = []
|
||||
elif cmatches:
|
||||
for cmatch in cmatches:
|
||||
out.write(" _(%s) " % cmatch)
|
||||
elif t.contents == "comment":
|
||||
incomment = True
|
||||
else:
|
||||
out.write(blankout(t.contents, "B"))
|
||||
elif t.token_type == TokenType.VAR:
|
||||
parts = t.contents.split("|")
|
||||
cmatch = constant_re.match(parts[0])
|
||||
if cmatch:
|
||||
out.write(" _(%s) " % cmatch[1])
|
||||
for p in parts[1:]:
|
||||
if p.find(":_(") >= 0:
|
||||
out.write(" %s " % p.split(":", 1)[1])
|
||||
else:
|
||||
out.write(blankout(p, "F"))
|
||||
elif t.token_type == TokenType.COMMENT:
|
||||
if t.contents.lstrip().startswith(TRANSLATOR_COMMENT_MARK):
|
||||
lineno_comment_map.setdefault(t.lineno, []).append(t.contents)
|
||||
comment_lineno_cache = t.lineno
|
||||
else:
|
||||
out.write(blankout(t.contents, "X"))
|
||||
return out.getvalue()
|
||||
@@ -0,0 +1,67 @@
|
||||
# These are versions of the functions in django.utils.translation.trans_real
|
||||
# that don't actually do anything. This is purely for performance, so that
|
||||
# settings.USE_I18N = False can use this module rather than trans_real.py.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def gettext(message):
|
||||
return message
|
||||
|
||||
|
||||
gettext_noop = gettext_lazy = _ = gettext
|
||||
|
||||
|
||||
def ngettext(singular, plural, number):
|
||||
if number == 1:
|
||||
return singular
|
||||
return plural
|
||||
|
||||
|
||||
ngettext_lazy = ngettext
|
||||
|
||||
|
||||
def pgettext(context, message):
|
||||
return gettext(message)
|
||||
|
||||
|
||||
def npgettext(context, singular, plural, number):
|
||||
return ngettext(singular, plural, number)
|
||||
|
||||
|
||||
def activate(x):
|
||||
return None
|
||||
|
||||
|
||||
def deactivate():
|
||||
return None
|
||||
|
||||
|
||||
deactivate_all = deactivate
|
||||
|
||||
|
||||
def get_language():
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
def get_language_bidi():
|
||||
return settings.LANGUAGE_CODE in settings.LANGUAGES_BIDI
|
||||
|
||||
|
||||
def check_for_language(x):
|
||||
return True
|
||||
|
||||
|
||||
def get_language_from_request(request, check_path=False):
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
def get_language_from_path(request):
|
||||
return None
|
||||
|
||||
|
||||
def get_supported_language_variant(lang_code, strict=False):
|
||||
if lang_code and lang_code.lower() == settings.LANGUAGE_CODE.lower():
|
||||
return lang_code
|
||||
else:
|
||||
raise LookupError(lang_code)
|
||||
@@ -0,0 +1,652 @@
|
||||
"""Translation helper functions."""
|
||||
|
||||
import functools
|
||||
import gettext as gettext_module
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from asgiref.local import Local
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.conf.locale import LANG_INFO
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.core.signals import setting_changed
|
||||
from django.dispatch import receiver
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
|
||||
from . import to_language, to_locale
|
||||
|
||||
# Translations are cached in a dictionary for every language.
|
||||
# The active translations are stored by threadid to make them thread local.
|
||||
_translations = {}
|
||||
_active = Local()
|
||||
|
||||
# The default translation is based on the settings file.
|
||||
_default = None
|
||||
|
||||
# magic gettext number to separate context from message
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
# Maximum number of characters that will be parsed from the Accept-Language
|
||||
# header or cookie to prevent possible denial of service or memory exhaustion
|
||||
# attacks. About 10x longer than the longest value shown on MDN’s
|
||||
# Accept-Language page.
|
||||
LANGUAGE_CODE_MAX_LENGTH = 500
|
||||
|
||||
# Format of Accept-Language header values. From RFC 9110 Sections 12.4.2 and
|
||||
# 12.5.4, and RFC 5646 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(?:\.[0-9]{,3})?|1(?:\.0{,3})?))?
|
||||
# Multiple accepts per header.
|
||||
(?:\s*,\s*|$)
|
||||
""",
|
||||
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
|
||||
)
|
||||
|
||||
language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+){0,2})(/|$)")
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
def reset_cache(*, setting, **kwargs):
|
||||
"""
|
||||
Reset global state when LANGUAGES setting has been changed, as some
|
||||
languages should no longer be accepted.
|
||||
"""
|
||||
if setting in ("LANGUAGES", "LANGUAGE_CODE"):
|
||||
check_for_language.cache_clear()
|
||||
get_languages.cache_clear()
|
||||
get_supported_language_variant.cache_clear()
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
def __getitem__(self, key):
|
||||
for cat in self._catalogs:
|
||||
try:
|
||||
return cat[key]
|
||||
except KeyError:
|
||||
pass
|
||||
raise KeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._catalogs[0][key] = value
|
||||
|
||||
def __contains__(self, key):
|
||||
return any(key in cat for cat in self._catalogs)
|
||||
|
||||
def items(self):
|
||||
for cat in self._catalogs:
|
||||
yield from cat.items()
|
||||
|
||||
def keys(self):
|
||||
for cat in self._catalogs:
|
||||
yield from cat.keys()
|
||||
|
||||
def update(self, trans):
|
||||
# Merge if plural function is the same as the top catalog, else prepend.
|
||||
if trans.plural.__code__ == self._plurals[0]:
|
||||
self._catalogs[0].update(trans._catalog)
|
||||
else:
|
||||
self._catalogs.insert(0, trans._catalog.copy())
|
||||
self._plurals.insert(0, trans.plural)
|
||||
|
||||
def get(self, key, default=None):
|
||||
missing = object()
|
||||
for cat in self._catalogs:
|
||||
result = cat.get(key, missing)
|
||||
if result is not missing:
|
||||
return result
|
||||
return default
|
||||
|
||||
def plural(self, msgid, num):
|
||||
for cat, plural in zip(self._catalogs, self._plurals):
|
||||
tmsg = cat.get((msgid, plural(num)))
|
||||
if tmsg is not None:
|
||||
return tmsg
|
||||
raise KeyError
|
||||
|
||||
|
||||
class DjangoTranslation(gettext_module.GNUTranslations):
|
||||
"""
|
||||
Set up the GNUTranslations context with regard to output charset.
|
||||
|
||||
This translation object will be constructed out of multiple GNUTranslations
|
||||
objects by merging their catalogs. It will construct an object for the
|
||||
requested language and add a fallback to the default language, if it's
|
||||
different from the requested language.
|
||||
"""
|
||||
|
||||
domain = "django"
|
||||
|
||||
def __init__(self, language, domain=None, localedirs=None):
|
||||
"""Create a GNUTranslations() using many locale directories"""
|
||||
gettext_module.GNUTranslations.__init__(self)
|
||||
if domain is not None:
|
||||
self.domain = domain
|
||||
|
||||
self.__language = language
|
||||
self.__to_language = to_language(language)
|
||||
self.__locale = to_locale(language)
|
||||
self._catalog = None
|
||||
# If a language doesn't have a catalog, use the Germanic default for
|
||||
# pluralization: anything except one is pluralized.
|
||||
self.plural = lambda n: int(n != 1)
|
||||
|
||||
if self.domain == "django":
|
||||
if localedirs is not None:
|
||||
# A module-level cache is used for caching 'django' translations
|
||||
warnings.warn(
|
||||
"localedirs is ignored when domain is 'django'.", RuntimeWarning
|
||||
)
|
||||
localedirs = None
|
||||
self._init_translation_catalog()
|
||||
|
||||
if localedirs:
|
||||
for localedir in localedirs:
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
else:
|
||||
self._add_installed_apps_translations()
|
||||
|
||||
self._add_local_translations()
|
||||
if (
|
||||
self.__language == settings.LANGUAGE_CODE
|
||||
and self.domain == "django"
|
||||
and self._catalog is None
|
||||
):
|
||||
# default lang should have at least one translation file available.
|
||||
raise 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.
|
||||
self._catalog = TranslationCatalog()
|
||||
|
||||
def __repr__(self):
|
||||
return "<DjangoTranslation lang:%s>" % self.__language
|
||||
|
||||
def _new_gnu_trans(self, localedir, use_null_fallback=True):
|
||||
"""
|
||||
Return a mergeable gettext.GNUTranslations instance.
|
||||
|
||||
A convenience wrapper. By default gettext uses 'fallback=False'.
|
||||
Using param `use_null_fallback` to avoid confusion with any other
|
||||
references to 'fallback'.
|
||||
"""
|
||||
return gettext_module.translation(
|
||||
domain=self.domain,
|
||||
localedir=localedir,
|
||||
languages=[self.__locale],
|
||||
fallback=use_null_fallback,
|
||||
)
|
||||
|
||||
def _init_translation_catalog(self):
|
||||
"""Create a base catalog using global django translations."""
|
||||
settingsfile = sys.modules[settings.__module__].__file__
|
||||
localedir = os.path.join(os.path.dirname(settingsfile), "locale")
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
def _add_installed_apps_translations(self):
|
||||
"""Merge translations from each installed app."""
|
||||
try:
|
||||
app_configs = reversed(apps.get_app_configs())
|
||||
except AppRegistryNotReady:
|
||||
raise AppRegistryNotReady(
|
||||
"The translation infrastructure cannot be initialized before the "
|
||||
"apps registry is ready. Check that you don't make non-lazy "
|
||||
"gettext calls at import time."
|
||||
)
|
||||
for app_config in app_configs:
|
||||
localedir = os.path.join(app_config.path, "locale")
|
||||
if os.path.exists(localedir):
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
def _add_local_translations(self):
|
||||
"""Merge translations defined in LOCALE_PATHS."""
|
||||
for localedir in reversed(settings.LOCALE_PATHS):
|
||||
translation = self._new_gnu_trans(localedir)
|
||||
self.merge(translation)
|
||||
|
||||
def _add_fallback(self, localedirs=None):
|
||||
"""Set the GNUTranslations() fallback with the default language."""
|
||||
# Don't set a fallback for the default language or any English variant
|
||||
# (as it's empty, so it'll ALWAYS fall back to the default language)
|
||||
if self.__language == settings.LANGUAGE_CODE or self.__language.startswith(
|
||||
"en"
|
||||
):
|
||||
return
|
||||
if self.domain == "django":
|
||||
# Get from cache
|
||||
default_translation = translation(settings.LANGUAGE_CODE)
|
||||
else:
|
||||
default_translation = DjangoTranslation(
|
||||
settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedirs
|
||||
)
|
||||
self.add_fallback(default_translation)
|
||||
|
||||
def merge(self, other):
|
||||
"""Merge another translation into this catalog."""
|
||||
if not getattr(other, "_catalog", None):
|
||||
return # NullTranslations() has no _catalog
|
||||
if self._catalog is None:
|
||||
# Take plural and _info from first catalog found (generally Django's).
|
||||
self.plural = other.plural
|
||||
self._info = other._info.copy()
|
||||
self._catalog = TranslationCatalog(other)
|
||||
else:
|
||||
self._catalog.update(other)
|
||||
if other._fallback:
|
||||
self.add_fallback(other._fallback)
|
||||
|
||||
def language(self):
|
||||
"""Return the translation language."""
|
||||
return self.__language
|
||||
|
||||
def to_language(self):
|
||||
"""Return the translation language name."""
|
||||
return self.__to_language
|
||||
|
||||
def ngettext(self, msgid1, msgid2, n):
|
||||
try:
|
||||
tmsg = self._catalog.plural(msgid1, n)
|
||||
except KeyError:
|
||||
if self._fallback:
|
||||
return self._fallback.ngettext(msgid1, msgid2, n)
|
||||
if n == 1:
|
||||
tmsg = msgid1
|
||||
else:
|
||||
tmsg = msgid2
|
||||
return tmsg
|
||||
|
||||
|
||||
def translation(language):
|
||||
"""
|
||||
Return a translation object in the default 'django' domain.
|
||||
"""
|
||||
if language not in _translations:
|
||||
_translations[language] = DjangoTranslation(language)
|
||||
return _translations[language]
|
||||
|
||||
|
||||
def activate(language):
|
||||
"""
|
||||
Fetch the translation object for a given language and install it as the
|
||||
current translation object for the current thread.
|
||||
"""
|
||||
if not language:
|
||||
return
|
||||
_active.value = translation(language)
|
||||
|
||||
|
||||
def deactivate():
|
||||
"""
|
||||
Uninstall the active translation object so that further _() calls resolve
|
||||
to the default translation object.
|
||||
"""
|
||||
if hasattr(_active, "value"):
|
||||
del _active.value
|
||||
|
||||
|
||||
def deactivate_all():
|
||||
"""
|
||||
Make the active translation object a NullTranslations() instance. This is
|
||||
useful when we want delayed translations to appear as the original string
|
||||
for some reason.
|
||||
"""
|
||||
_active.value = gettext_module.NullTranslations()
|
||||
_active.value.to_language = lambda *args: None
|
||||
|
||||
|
||||
def get_language():
|
||||
"""Return the currently selected language."""
|
||||
t = getattr(_active, "value", None)
|
||||
if t is not None:
|
||||
try:
|
||||
return t.to_language()
|
||||
except AttributeError:
|
||||
pass
|
||||
# If we don't have a real translation object, assume it's the default language.
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
def get_language_bidi():
|
||||
"""
|
||||
Return selected language's BiDi layout.
|
||||
|
||||
* False = left-to-right layout
|
||||
* True = right-to-left layout
|
||||
"""
|
||||
lang = get_language()
|
||||
if lang is None:
|
||||
return False
|
||||
else:
|
||||
base_lang = get_language().split("-")[0]
|
||||
return base_lang in settings.LANGUAGES_BIDI
|
||||
|
||||
|
||||
def catalog():
|
||||
"""
|
||||
Return the current active catalog for further processing.
|
||||
This can be used if you need to modify the catalog or want to access the
|
||||
whole message catalog instead of just translating one string.
|
||||
"""
|
||||
global _default
|
||||
|
||||
t = getattr(_active, "value", None)
|
||||
if t is not None:
|
||||
return t
|
||||
if _default is None:
|
||||
_default = translation(settings.LANGUAGE_CODE)
|
||||
return _default
|
||||
|
||||
|
||||
def gettext(message):
|
||||
"""
|
||||
Translate the 'message' string. It uses the current thread to find the
|
||||
translation object to use. If no current translation is activated, the
|
||||
message will be run through the default translation object.
|
||||
"""
|
||||
global _default
|
||||
|
||||
eol_message = message.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
if eol_message:
|
||||
_default = _default or translation(settings.LANGUAGE_CODE)
|
||||
translation_object = getattr(_active, "value", _default)
|
||||
|
||||
result = translation_object.gettext(eol_message)
|
||||
else:
|
||||
# Return an empty value of the corresponding type if an empty message
|
||||
# is given, instead of metadata, which is the default gettext behavior.
|
||||
result = type(message)("")
|
||||
|
||||
if isinstance(message, SafeData):
|
||||
return mark_safe(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def pgettext(context, message):
|
||||
msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message)
|
||||
result = gettext(msg_with_ctxt)
|
||||
if CONTEXT_SEPARATOR in result:
|
||||
# Translation not found
|
||||
result = message
|
||||
elif isinstance(message, SafeData):
|
||||
result = mark_safe(result)
|
||||
return result
|
||||
|
||||
|
||||
def gettext_noop(message):
|
||||
"""
|
||||
Mark strings for translation but don't translate them now. This can be
|
||||
used to store strings in global variables that should stay in the base
|
||||
language (because they might be used externally) and will be translated
|
||||
later.
|
||||
"""
|
||||
return message
|
||||
|
||||
|
||||
def do_ntranslate(singular, plural, number, translation_function):
|
||||
global _default
|
||||
|
||||
t = getattr(_active, "value", None)
|
||||
if t is not None:
|
||||
return getattr(t, translation_function)(singular, plural, number)
|
||||
if _default is None:
|
||||
_default = translation(settings.LANGUAGE_CODE)
|
||||
return getattr(_default, translation_function)(singular, plural, number)
|
||||
|
||||
|
||||
def ngettext(singular, plural, number):
|
||||
"""
|
||||
Return a string of the translation of either the singular or plural,
|
||||
based on the number.
|
||||
"""
|
||||
return do_ntranslate(singular, plural, number, "ngettext")
|
||||
|
||||
|
||||
def npgettext(context, singular, plural, number):
|
||||
msgs_with_ctxt = (
|
||||
"%s%s%s" % (context, CONTEXT_SEPARATOR, singular),
|
||||
"%s%s%s" % (context, CONTEXT_SEPARATOR, plural),
|
||||
number,
|
||||
)
|
||||
result = ngettext(*msgs_with_ctxt)
|
||||
if CONTEXT_SEPARATOR in result:
|
||||
# Translation not found
|
||||
result = ngettext(singular, plural, number)
|
||||
return result
|
||||
|
||||
|
||||
def all_locale_paths():
|
||||
"""
|
||||
Return a list of paths to user-provides languages files.
|
||||
"""
|
||||
globalpath = os.path.join(
|
||||
os.path.dirname(sys.modules[settings.__module__].__file__), "locale"
|
||||
)
|
||||
app_paths = []
|
||||
for app_config in apps.get_app_configs():
|
||||
locale_path = os.path.join(app_config.path, "locale")
|
||||
if os.path.exists(locale_path):
|
||||
app_paths.append(locale_path)
|
||||
return [globalpath, *settings.LOCALE_PATHS, *app_paths]
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def check_for_language(lang_code):
|
||||
"""
|
||||
Check whether there is a global language file for the given language
|
||||
code. This is used to decide whether a user-provided language is
|
||||
available.
|
||||
|
||||
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
|
||||
as the provided language codes are taken from the HTTP request. See also
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
# First, a quick check to make sure lang_code is well-formed (#21458)
|
||||
if lang_code is None or not language_code_re.search(lang_code):
|
||||
return False
|
||||
return any(
|
||||
gettext_module.find("django", path, [to_locale(lang_code)]) is not None
|
||||
for path in all_locale_paths()
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def get_languages():
|
||||
"""
|
||||
Cache of settings.LANGUAGES in a dictionary for easy lookups by key.
|
||||
Convert keys to lowercase as they should be treated as case-insensitive.
|
||||
"""
|
||||
return {key.lower(): value for key, value in dict(settings.LANGUAGES).items()}
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def get_supported_language_variant(lang_code, strict=False):
|
||||
"""
|
||||
Return the language code that's listed in supported languages, possibly
|
||||
selecting a more generic variant. Raise LookupError if nothing is found.
|
||||
|
||||
If `strict` is False (the default), look for a country-specific variant
|
||||
when neither the language code nor its generic variant is found.
|
||||
|
||||
The language code is truncated to a maximum length to avoid potential
|
||||
denial of service attacks.
|
||||
|
||||
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
|
||||
as the provided language codes are taken from the HTTP request. See also
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
if lang_code:
|
||||
# Truncate the language code to a maximum length to avoid potential
|
||||
# denial of service attacks.
|
||||
if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
|
||||
if (
|
||||
not strict
|
||||
and (index := lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0
|
||||
):
|
||||
# There is a generic variant under the maximum length accepted length.
|
||||
lang_code = lang_code[:index]
|
||||
else:
|
||||
raise LookupError(lang_code)
|
||||
# If 'zh-hant-tw' is not supported, try special fallback or subsequent
|
||||
# language codes i.e. 'zh-hant' and 'zh'.
|
||||
possible_lang_codes = [lang_code]
|
||||
try:
|
||||
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]
|
||||
supported_lang_codes = get_languages()
|
||||
|
||||
for code in possible_lang_codes:
|
||||
if code.lower() in supported_lang_codes and check_for_language(code):
|
||||
return code
|
||||
if not strict:
|
||||
# if fr-fr is not supported, try fr-ca.
|
||||
for supported_code in supported_lang_codes:
|
||||
if supported_code.startswith(generic_lang_code + "-"):
|
||||
return supported_code
|
||||
raise LookupError(lang_code)
|
||||
|
||||
|
||||
def get_language_from_path(path, strict=False):
|
||||
"""
|
||||
Return the language code if there's a valid language code found in `path`.
|
||||
|
||||
If `strict` is False (the default), look for a country-specific variant
|
||||
when neither the language code nor its generic variant is found.
|
||||
"""
|
||||
regex_match = language_code_prefix_re.match(path)
|
||||
if not regex_match:
|
||||
return None
|
||||
lang_code = regex_match[1]
|
||||
try:
|
||||
return get_supported_language_variant(lang_code, strict=strict)
|
||||
except LookupError:
|
||||
return None
|
||||
|
||||
|
||||
def get_language_from_request(request, check_path=False):
|
||||
"""
|
||||
Analyze the request to find what language the user wants the system to
|
||||
show. Only languages listed in settings.LANGUAGES are taken into account.
|
||||
If the user requests a sublanguage where we have a main language, we send
|
||||
out the main language.
|
||||
|
||||
If check_path is True, the URL path prefix will be checked for a language
|
||||
code, otherwise this is skipped for backwards compatibility.
|
||||
"""
|
||||
if check_path:
|
||||
lang_code = get_language_from_path(request.path_info)
|
||||
if lang_code is not None:
|
||||
return lang_code
|
||||
|
||||
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)
|
||||
):
|
||||
return lang_code
|
||||
|
||||
try:
|
||||
return get_supported_language_variant(lang_code)
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
|
||||
for accept_lang, unused in parse_accept_lang_header(accept):
|
||||
if accept_lang == "*":
|
||||
break
|
||||
|
||||
if not language_code_re.search(accept_lang):
|
||||
continue
|
||||
|
||||
try:
|
||||
return get_supported_language_variant(accept_lang)
|
||||
except LookupError:
|
||||
continue
|
||||
|
||||
try:
|
||||
return get_supported_language_variant(settings.LANGUAGE_CODE)
|
||||
except LookupError:
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def _parse_accept_lang_header(lang_string):
|
||||
"""
|
||||
Parse the lang_string, which is the body of an HTTP Accept-Language
|
||||
header, and return a tuple of (lang, q-value), ordered by 'q' values.
|
||||
|
||||
Return an empty tuple if there are any format errors in lang_string.
|
||||
"""
|
||||
result = []
|
||||
pieces = accept_language_re.split(lang_string.lower())
|
||||
if pieces[-1]:
|
||||
return ()
|
||||
for i in range(0, len(pieces) - 1, 3):
|
||||
first, lang, priority = pieces[i : i + 3]
|
||||
if first:
|
||||
return ()
|
||||
if priority:
|
||||
priority = float(priority)
|
||||
else:
|
||||
priority = 1.0
|
||||
result.append((lang, priority))
|
||||
result.sort(key=lambda k: k[1], reverse=True)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def parse_accept_lang_header(lang_string):
|
||||
"""
|
||||
Parse the value of the Accept-Language header up to a maximum length.
|
||||
|
||||
The value of the header is truncated to a maximum length to avoid potential
|
||||
denial of service and memory exhaustion attacks. Excessive memory could be
|
||||
used if the raw value is very large as it would be cached due to the use of
|
||||
functools.lru_cache() to avoid repetitive parsing of common header values.
|
||||
"""
|
||||
# If the header value doesn't exceed the maximum allowed length, parse it.
|
||||
if len(lang_string) <= LANGUAGE_CODE_MAX_LENGTH:
|
||||
return _parse_accept_lang_header(lang_string)
|
||||
|
||||
# If there is at least one comma in the value, parse up to the last comma
|
||||
# before the max length, skipping any truncated parts at the end of the
|
||||
# header value.
|
||||
if (index := lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0:
|
||||
return _parse_accept_lang_header(lang_string[:index])
|
||||
|
||||
# Don't attempt to parse if there is only one language-range value which is
|
||||
# longer than the maximum allowed length and so truncated.
|
||||
return ()
|
||||
126
.venv/lib/python3.12/site-packages/django/utils/tree.py
Normal file
126
.venv/lib/python3.12/site-packages/django/utils/tree.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
A class for storing a tree graph. Primarily used for filter constructs in the
|
||||
ORM.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from django.utils.hashable import make_hashable
|
||||
|
||||
|
||||
class Node:
|
||||
"""
|
||||
A single internal node in the tree graph. A Node should be viewed as a
|
||||
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"
|
||||
|
||||
def __init__(self, children=None, connector=None, negated=False):
|
||||
"""Construct a new Node. If no connector is given, use the default."""
|
||||
self.children = children[:] if children else []
|
||||
self.connector = connector or self.default
|
||||
self.negated = negated
|
||||
|
||||
@classmethod
|
||||
def create(cls, children=None, connector=None, negated=False):
|
||||
"""
|
||||
Create a new instance using Node() instead of __init__() as some
|
||||
subclasses, e.g. django.db.models.query_utils.Q, may implement a custom
|
||||
__init__() with a signature that conflicts with the one defined in
|
||||
Node.__init__().
|
||||
"""
|
||||
obj = Node(children, connector or cls.default, negated)
|
||||
obj.__class__ = cls
|
||||
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))
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: %s>" % (self.__class__.__name__, self)
|
||||
|
||||
def __copy__(self):
|
||||
obj = self.create(connector=self.connector, negated=self.negated)
|
||||
obj.children = self.children # Don't [:] as .__init__() via .create() does.
|
||||
return obj
|
||||
|
||||
copy = __copy__
|
||||
|
||||
def __deepcopy__(self, memodict):
|
||||
obj = self.create(connector=self.connector, negated=self.negated)
|
||||
obj.children = copy.deepcopy(self.children, memodict)
|
||||
return obj
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of children this node has."""
|
||||
return len(self.children)
|
||||
|
||||
def __bool__(self):
|
||||
"""Return whether or not this node has children."""
|
||||
return bool(self.children)
|
||||
|
||||
def __contains__(self, other):
|
||||
"""Return True if 'other' is a direct child of this instance."""
|
||||
return other in self.children
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.__class__ == other.__class__
|
||||
and self.connector == other.connector
|
||||
and self.negated == other.negated
|
||||
and self.children == other.children
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(
|
||||
(
|
||||
self.__class__,
|
||||
self.connector,
|
||||
self.negated,
|
||||
*make_hashable(self.children),
|
||||
)
|
||||
)
|
||||
|
||||
def add(self, data, conn_type):
|
||||
"""
|
||||
Combine this tree and the data represented by data using the
|
||||
connector conn_type. The combine is done by squashing the node other
|
||||
away if possible.
|
||||
|
||||
This tree (self) will never be pushed to a child node of the
|
||||
combined tree, nor will the connector or negated properties change.
|
||||
|
||||
Return a node which can be used in place of data regardless if the
|
||||
node other got squashed or not.
|
||||
"""
|
||||
if self.connector != conn_type:
|
||||
obj = self.copy()
|
||||
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."""
|
||||
self.negated = not self.negated
|
||||
124
.venv/lib/python3.12/site-packages/django/utils/version.py
Normal file
124
.venv/lib/python3.12/site-packages/django/utils/version.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import datetime
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
|
||||
# Private, stable API for detecting the Python implementation.
|
||||
PYPY = sys.implementation.name == "pypy"
|
||||
|
||||
# Private, stable API for detecting the Python version. PYXY means "Python X.Y
|
||||
# or later". So that third-party apps can use these values, each constant
|
||||
# should remain as long as the oldest supported Django version supports that
|
||||
# Python version.
|
||||
PY38 = sys.version_info >= (3, 8)
|
||||
PY39 = sys.version_info >= (3, 9)
|
||||
PY310 = sys.version_info >= (3, 10)
|
||||
PY311 = sys.version_info >= (3, 11)
|
||||
PY312 = sys.version_info >= (3, 12)
|
||||
PY313 = sys.version_info >= (3, 13)
|
||||
PY314 = sys.version_info >= (3, 14)
|
||||
|
||||
|
||||
def get_version(version=None):
|
||||
"""Return a PEP 440-compliant version number from VERSION."""
|
||||
version = get_complete_version(version)
|
||||
|
||||
# Now build the two parts of the version number:
|
||||
# main = X.Y[.Z]
|
||||
# sub = .devN - for pre-alpha releases
|
||||
# | {a|b|rc}N - for alpha, beta, and rc releases
|
||||
|
||||
main = get_main_version(version)
|
||||
|
||||
sub = ""
|
||||
if version[3] == "alpha" and version[4] == 0:
|
||||
git_changeset = get_git_changeset()
|
||||
if git_changeset:
|
||||
sub = ".dev%s" % git_changeset
|
||||
|
||||
elif version[3] != "final":
|
||||
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
|
||||
sub = mapping[version[3]] + str(version[4])
|
||||
|
||||
return main + sub
|
||||
|
||||
|
||||
def get_main_version(version=None):
|
||||
"""Return main version (X.Y[.Z]) from VERSION."""
|
||||
version = get_complete_version(version)
|
||||
parts = 2 if version[2] == 0 else 3
|
||||
return ".".join(str(x) for x in version[:parts])
|
||||
|
||||
|
||||
def get_complete_version(version=None):
|
||||
"""
|
||||
Return a tuple of the django version. If version argument is non-empty,
|
||||
check for correctness of the tuple provided.
|
||||
"""
|
||||
if version is None:
|
||||
from django import VERSION as version
|
||||
else:
|
||||
assert len(version) == 5
|
||||
assert version[3] in ("alpha", "beta", "rc", "final")
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def get_docs_version(version=None):
|
||||
version = get_complete_version(version)
|
||||
if version[3] != "final":
|
||||
return "dev"
|
||||
else:
|
||||
return "%d.%d" % version[:2]
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def get_git_changeset():
|
||||
"""Return a numeric identifier of the latest git changeset.
|
||||
|
||||
The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format.
|
||||
This value isn't guaranteed to be unique, but collisions are very unlikely,
|
||||
so it's sufficient for generating the development version numbers.
|
||||
"""
|
||||
# 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",
|
||||
capture_output=True,
|
||||
shell=True,
|
||||
cwd=repo_dir,
|
||||
text=True,
|
||||
)
|
||||
timestamp = git_log.stdout
|
||||
tz = datetime.timezone.utc
|
||||
try:
|
||||
timestamp = datetime.datetime.fromtimestamp(int(timestamp), tz=tz)
|
||||
except ValueError:
|
||||
return None
|
||||
return timestamp.strftime("%Y%m%d%H%M%S")
|
||||
|
||||
|
||||
version_component_re = _lazy_re_compile(r"(\d+|[a-z]+|\.)")
|
||||
|
||||
|
||||
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').
|
||||
"""
|
||||
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)
|
||||
return tuple(version_numbers)
|
||||
35
.venv/lib/python3.12/site-packages/django/utils/xmlutils.py
Normal file
35
.venv/lib/python3.12/site-packages/django/utils/xmlutils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Utilities for XML generation/parsing.
|
||||
"""
|
||||
|
||||
import re
|
||||
from xml.sax.saxutils import XMLGenerator
|
||||
|
||||
|
||||
class UnserializableContentError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class SimplerXMLGenerator(XMLGenerator):
|
||||
def addQuickElement(self, name, contents=None, attrs=None):
|
||||
"Convenience method for adding an element with no children"
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
self.startElement(name, attrs)
|
||||
if contents is not None:
|
||||
self.characters(contents)
|
||||
self.endElement(name)
|
||||
|
||||
def characters(self, content):
|
||||
if content and re.search(r"[\x00-\x08\x0B-\x0C\x0E-\x1F]", content):
|
||||
# Fail loudly when content has control chars (unsupported in XML 1.0)
|
||||
# See https://www.w3.org/International/questions/qa-controls
|
||||
raise UnserializableContentError(
|
||||
"Control characters are not supported in XML 1.0"
|
||||
)
|
||||
XMLGenerator.characters(self, content)
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
# Sort attrs for a deterministic output.
|
||||
sorted_attrs = dict(sorted(attrs.items())) if attrs else attrs
|
||||
super().startElement(name, sorted_attrs)
|
||||
Reference in New Issue
Block a user