测试gitnore

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