diff options
Diffstat (limited to 'django/utils')
45 files changed, 1974 insertions, 1304 deletions
diff --git a/django/utils/_os.py b/django/utils/_os.py index b2a2a9d426..f85fe09248 100644 --- a/django/utils/_os.py +++ b/django/utils/_os.py @@ -23,12 +23,15 @@ 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 @@ -39,8 +42,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) @@ -55,5 +58,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) diff --git a/django/utils/archive.py b/django/utils/archive.py index d5a0cf0446..71ec2d0015 100644 --- a/django/utils/archive.py +++ b/django/utils/archive.py @@ -55,6 +55,7 @@ class Archive: """ The external API class that encapsulates an archive implementation. """ + def __init__(self, file): self._archive = self._archive_cls(file)(file) @@ -68,7 +69,8 @@ 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: @@ -76,7 +78,8 @@ 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): @@ -99,6 +102,7 @@ class BaseArchive: """ Base Archive class. Implementations should inherit this class. """ + @staticmethod def _copy_permissions(mode, filename): """ @@ -111,13 +115,15 @@ 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): """ @@ -143,14 +149,17 @@ 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) @@ -174,13 +183,15 @@ 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: @@ -192,7 +203,6 @@ class TarArchive(BaseArchive): class ZipArchive(BaseArchive): - def __init__(self, file): self._archive = zipfile.ZipFile(file) @@ -210,14 +220,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 @@ -227,11 +237,21 @@ 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 diff --git a/django/utils/asyncio.py b/django/utils/asyncio.py index b8e14f1f68..7e0b439db2 100644 --- a/django/utils/asyncio.py +++ b/django/utils/asyncio.py @@ -10,6 +10,7 @@ def async_unsafe(message): Decorator to mark functions as async-unsafe. Someone trying to access the function while in an async context will get an error message. """ + def decorator(func): @wraps(func) def inner(*args, **kwargs): @@ -19,15 +20,17 @@ def async_unsafe(message): except RuntimeError: pass else: - if not os.environ.get('DJANGO_ALLOW_ASYNC_UNSAFE'): + if not os.environ.get("DJANGO_ALLOW_ASYNC_UNSAFE"): raise SynchronousOnlyOperation(message) # Pass onward. return func(*args, **kwargs) + return inner + # If the message is actually a function, then be a no-arguments decorator. if callable(message): func = message - message = 'You cannot call this from an async context - use a thread or sync_to_async.' + message = "You cannot call this from an async context - use a thread or sync_to_async." return decorator(func) else: return decorator diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 583c2be647..7b9219f4c1 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -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,7 +112,11 @@ 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)) @@ -126,21 +130,25 @@ 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() @@ -217,49 +225,50 @@ 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 = [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}' + f"-X{key}" if value is True else f"-X{key}={value}" for key, value in sys._xoptions.items() ) # __spec__ is set when the server was started with the `-m` option, # see https://docs.python.org/3/reference/import.html#main-spec # __spec__ may not exist, e.g. when running in a Conda env. - if getattr(__main__, '__spec__', None) is not None: + if getattr(__main__, "__spec__", None) is not None: spec = __main__.__spec__ - if (spec.name == '__main__' or spec.name.endswith('.__main__')) and spec.parent: + if (spec.name == "__main__" or spec.name.endswith(".__main__")) and spec.parent: name = spec.parent else: name = spec.name - args += ['-m', name] + args += ["-m", name] args += sys.argv[1:] elif not py_script.exists(): # sys.argv[0] may not exist for several reasons on Windows. # It may exist with a .exe extension or have a -script.py suffix. - 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) + 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) + 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) @@ -279,12 +288,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): @@ -314,11 +323,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 @@ -330,7 +339,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() @@ -351,15 +360,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) @@ -382,10 +391,15 @@ 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) @@ -418,7 +432,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 @@ -437,52 +451,63 @@ 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): """ @@ -493,19 +518,22 @@ 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() @@ -516,8 +544,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) @@ -527,7 +555,9 @@ 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: @@ -541,19 +571,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): @@ -568,7 +598,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()): @@ -584,7 +614,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 @@ -592,19 +622,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(): @@ -620,7 +650,9 @@ 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 = threading.Thread( + target=main_func, args=args, kwargs=kwargs, name="django-main-thread" + ) django_main_thread.daemon = True django_main_thread.start() @@ -631,16 +663,20 @@ 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() diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py index 21f1fb3b91..fcaab23f53 100644 --- a/django/utils/baseconv.py +++ b/django/utils/baseconv.py @@ -42,33 +42,37 @@ import warnings from django.utils.deprecation import RemovedInDjango50Warning warnings.warn( - 'The django.utils.baseconv module is deprecated.', + "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 @@ -76,7 +80,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): @@ -95,7 +99,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 @@ -108,4 +112,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="$") diff --git a/django/utils/cache.py b/django/utils/cache.py index c0e47e0e42..90292ce4da 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -23,15 +23,13 @@ from django.conf import settings from django.core.cache import caches from django.http import HttpResponse, HttpResponseNotModified from django.utils.crypto import md5 -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): @@ -46,8 +44,9 @@ 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: @@ -57,13 +56,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: @@ -72,18 +71,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: @@ -98,8 +97,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): @@ -107,18 +106,20 @@ 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( + response.headers["ETag"] = quote_etag( md5(response.content, usedforsecurity=False).hexdigest(), ) return response @@ -127,7 +128,8 @@ def set_response_etag(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,7 +141,15 @@ 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] @@ -158,11 +168,13 @@ 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. @@ -170,23 +182,26 @@ 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) @@ -202,12 +217,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: @@ -231,15 +246,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 @@ -264,8 +279,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) @@ -274,7 +289,9 @@ 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): @@ -287,28 +304,31 @@ 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 @@ -319,9 +339,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 @@ -332,21 +352,27 @@ def _generate_cache_key(request, method, headerlist, key_prefix): value = request.META.get(header) if value is not None: ctx.update(value.encode()) - url = md5(request.build_absolute_uri().encode('ascii'), usedforsecurity=False) - cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % ( - key_prefix, method, url.hexdigest(), ctx.hexdigest()) + url = md5(request.build_absolute_uri().encode("ascii"), usedforsecurity=False) + cache_key = "views.decorators.cache.cache_page.%s.%s.%s.%s" % ( + key_prefix, + method, + url.hexdigest(), + ctx.hexdigest(), + ) return _i18n_cache_key_suffix(request, cache_key) def _generate_cache_header_key(key_prefix, request): """Return a cache key for the header cache.""" - url = md5(request.build_absolute_uri().encode('ascii'), usedforsecurity=False) - cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( - key_prefix, url.hexdigest()) + url = md5(request.build_absolute_uri().encode("ascii"), usedforsecurity=False) + cache_key = "views.decorators.cache.cache_header.%s.%s" % ( + key_prefix, + url.hexdigest(), + ) return _i18n_cache_key_suffix(request, cache_key) -def get_cache_key(request, key_prefix=None, method='GET', cache=None): +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 @@ -388,17 +414,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) @@ -410,7 +436,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 diff --git a/django/utils/connection.py b/django/utils/connection.py index 72a02143fe..1b5895e8d3 100644 --- a/django/utils/connection.py +++ b/django/utils/connection.py @@ -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: diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 2af58fda6e..341cb742c1 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -12,10 +12,11 @@ from django.utils.inspect import func_supports_parameter 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,8 +33,7 @@ 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. @@ -45,7 +45,7 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm='sha1'): return hmac.new(key, msg=force_bytes(value), digestmod=hasher) -RANDOM_STRING_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' +RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS): @@ -59,7 +59,7 @@ 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)) + return "".join(secrets.choice(allowed_chars) for i in range(length)) def constant_time_compare(val1, val2): @@ -81,11 +81,12 @@ def pbkdf2(password, salt, iterations, dklen=0, digest=None): # detect whether the usedforsecurity argument is available as this fix may also # have been applied by downstream package maintainers to other versions in # their repositories. -if func_supports_parameter(hashlib.md5, 'usedforsecurity'): +if func_supports_parameter(hashlib.md5, "usedforsecurity"): md5 = hashlib.md5 new_hash = hashlib.new else: - def md5(data=b'', *, usedforsecurity=True): + + def md5(data=b"", *, usedforsecurity=True): return hashlib.md5(data) def new_hash(hash_algorithm, *, usedforsecurity=True): diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 18c91aa5e8..b5858b8076 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -38,8 +38,8 @@ class OrderedSet: return len(self.dict) def __repr__(self): - data = repr(list(self.dict)) if self.dict else '' - return f'{self.__class__.__qualname__}({data})' + data = repr(list(self.dict)) if self.dict else "" + return f"{self.__class__.__qualname__}({data})" class MultiValueDictKeyError(KeyError): @@ -68,6 +68,7 @@ class MultiValueDict(dict): 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) @@ -92,24 +93,22 @@ 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) @@ -231,7 +230,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 @@ -264,6 +263,7 @@ 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 +277,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) @@ -314,9 +314,7 @@ 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()) @@ -335,11 +333,11 @@ class CaseInsensitiveMapping(Mapping): 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] + "Element key %r invalid, only strings are allowed" % elem[0] ) yield elem diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 70bfb32e01..541b8f8688 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -15,17 +15,24 @@ import datetime 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_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: @@ -40,12 +47,11 @@ 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 @@ -61,22 +67,21 @@ class TimeFormat(Formatter): @property def _no_timezone_or_datetime_is_ambiguous_or_imaginary(self): - return ( - not self.timezone or - _datetime_ambiguous_or_imaginary(self.data, self.timezone) + 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): """ @@ -88,8 +93,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 "" @@ -103,7 +108,7 @@ class TimeFormat(Formatter): """ hour = self.data.hour % 12 or 12 minute = self.data.minute - return '%d:%02d' % (hour, minute) if minute else hour + return "%d:%02d" % (hour, minute) if minute else hour def g(self): "Hour, 12-hour format without leading zeros; i.e. '1' to '12'" @@ -115,15 +120,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.data.hour % 12 or 12) def H(self): "Hour, 24-hour format; i.e. '00' to '23'" - return '%02d' % self.data.hour + return "%02d" % self.data.hour 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 """ @@ -135,7 +140,7 @@ class TimeFormat(Formatter): return "" seconds = self.Z() - sign = '-' if seconds < 0 else '+' + sign = "-" if seconds < 0 else "+" seconds = abs(seconds) return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60) @@ -147,14 +152,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): """ @@ -169,7 +174,7 @@ class TimeFormat(Formatter): def u(self): "Microseconds; i.e. '000000' to '999999'" - return '%06d' % self.data.microsecond + return "%06d" % self.data.microsecond def Z(self): """ @@ -205,7 +210,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'" @@ -222,8 +227,8 @@ class DateFormat(TimeFormat): 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' + 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'" @@ -239,7 +244,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'" @@ -273,19 +278,19 @@ class DateFormat(TimeFormat): def S(self): "English ordinal suffix for the day of the month, 2 characters; i.e. 'st', 'nd', 'rd' or 'th'" if self.data.day in (11, 12, 13): # Special case - return 'th' + 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)" @@ -304,11 +309,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 + return "%04d" % self.data.year def z(self): """Day of the year, i.e. 1 to 366.""" diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py index a137031b3f..2e6a260a4f 100644 --- a/django/utils/dateparse.py +++ b/django/utils/dateparse.py @@ -10,59 +10,57 @@ 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"\s*(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$" ) standard_duration_re = _lazy_re_compile( - r'^' - r'(?:(?P<days>-?\d+) (days?, )?)?' - r'(?P<sign>-?)' - r'((?:(?P<hours>\d+):)(?=\d+:\d+))?' - r'(?:(?P<minutes>\d+):)?' - r'(?P<seconds>\d+)' - r'(?:[\.,](?P<microseconds>\d{1,6})\d{0,6})?' - r'$' + 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")?$" ) @@ -98,7 +96,7 @@ def parse_time(value): except ValueError: if match := time_re.match(value): kw = match.groupdict() - kw['microsecond'] = kw['microsecond'] and kw['microsecond'].ljust(6, '0') + 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) @@ -117,14 +115,14 @@ def parse_datetime(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': + 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] == '-': + 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} @@ -140,17 +138,17 @@ 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') - 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) + sign = -1 if kw.pop("sign", "+") == "-" else 1 + if kw.get("microseconds"): + kw["microseconds"] = kw["microseconds"].ljust(6, "0") + kw = {k: float(v.replace(",", ".")) for k, v in kw.items() if v is not None} + days = datetime.timedelta(kw.pop("days", 0.0) or 0.0) if match.re == iso8601_duration_re: days *= sign return days + sign * datetime.timedelta(**kw) diff --git a/django/utils/dates.py b/django/utils/dates.py index d2e769db84..05ac3293df 100644 --- a/django/utils/dates.py +++ b/django/utils/dates.py @@ -1,49 +1,79 @@ "Commonly-used date structures" -from django.utils.translation import gettext_lazy as _, pgettext_lazy +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import pgettext_lazy WEEKDAYS = { - 0: _('Monday'), 1: _('Tuesday'), 2: _('Wednesday'), 3: _('Thursday'), 4: _('Friday'), - 5: _('Saturday'), 6: _('Sunday') + 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"), } diff --git a/django/utils/datetime_safe.py b/django/utils/datetime_safe.py index e06887b706..817ddcf0fa 100644 --- a/django/utils/datetime_safe.py +++ b/django/utils/datetime_safe.py @@ -9,13 +9,14 @@ import time import warnings -from datetime import date as real_date, datetime as real_datetime +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.', + "The django.utils.datetime_safe module is deprecated.", category=RemovedInDjango50Warning, stacklevel=2, ) @@ -32,9 +33,16 @@ 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) @@ -78,7 +86,9 @@ 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 @@ -104,5 +114,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 diff --git a/django/utils/deconstruct.py b/django/utils/deconstruct.py index 9d073902f3..ca87f7ecc9 100644 --- a/django/utils/deconstruct.py +++ b/django/utils/deconstruct.py @@ -10,6 +10,7 @@ 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 @@ -24,7 +25,7 @@ def deconstructible(*args, path=None): """ # Fallback version if path and type(obj) is klass: - module_name, _, name = path.rpartition('.') + module_name, _, name = path.rpartition(".") else: module_name = obj.__module__ name = obj.__class__.__name__ @@ -38,11 +39,12 @@ def deconstructible(*args, path=None): "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())) + % (name, module_name, get_docs_version()) + ) return ( path if path and type(obj) is klass - else f'{obj.__class__.__module__}.{name}', + else f"{obj.__class__.__module__}.{name}", obj._constructor_args[0], obj._constructor_args[1], ) diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 69aca10a4d..e412bb15e1 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -6,7 +6,9 @@ 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) @@ -16,6 +18,7 @@ def _update_method_wrapper(_wrapper, decorator): @decorator def dummy(*args, **kwargs): pass + update_wrapper(_wrapper, dummy) @@ -24,7 +27,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. @@ -50,7 +53,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 """ @@ -78,11 +81,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 @@ -118,37 +121,44 @@ 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 diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py index 48209bcdf1..528783a5c1 100644 --- a/django/utils/deprecation.py +++ b/django/utils/deprecation.py @@ -14,7 +14,9 @@ class RemovedInDjango50Warning(PendingDeprecationWarning): 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 @@ -23,10 +25,13 @@ 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 @@ -60,9 +65,11 @@ 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)) @@ -77,7 +84,8 @@ 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) @@ -88,17 +96,17 @@ class MiddlewareMixin: def __init__(self, get_response): if get_response is None: - raise ValueError('get_response must be provided.') + raise ValueError("get_response must be provided.") self.get_response = get_response self._async_check() super().__init__() def __repr__(self): - return '<%s get_response=%s>' % ( + return "<%s get_response=%s>" % ( self.__class__.__qualname__, getattr( self.get_response, - '__qualname__', + "__qualname__", self.get_response.__class__.__name__, ), ) @@ -120,10 +128,10 @@ class MiddlewareMixin: if self._is_coroutine: 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 @@ -133,13 +141,13 @@ 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, diff --git a/django/utils/duration.py b/django/utils/duration.py index 466603d46c..8495af3fa8 100644 --- a/django/utils/duration.py +++ b/django/utils/duration.py @@ -19,25 +19,27 @@ 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): diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 19eb150ad7..89eac79dd4 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -13,10 +13,14 @@ 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. @@ -30,7 +34,13 @@ 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, ) @@ -43,7 +53,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. @@ -65,7 +75,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'. @@ -77,7 +87,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. @@ -86,10 +96,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): @@ -136,15 +146,14 @@ _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): @@ -164,7 +173,7 @@ def uri_to_iri(uri): # 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: @@ -177,9 +186,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() @@ -202,7 +211,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): @@ -217,8 +226,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 @@ -245,10 +254,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 diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index 857beff13a..a8f6bc5c06 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -40,7 +40,7 @@ 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): @@ -50,68 +50,103 @@ def get_tag_uri(url, date): 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) @@ -147,7 +182,9 @@ 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): """ @@ -163,7 +200,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: @@ -177,6 +214,7 @@ class SyndicationFeed: 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 @@ -184,7 +222,7 @@ 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) @@ -198,31 +236,33 @@ 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") @@ -232,10 +272,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): @@ -243,93 +283,105 @@ 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.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: @@ -338,52 +390,56 @@ 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 diff --git a/django/utils/formats.py b/django/utils/formats.py index 3aef3bc23c..50b58445e4 100644 --- a/django/utils/formats.py +++ b/django/utils/formats.py @@ -8,9 +8,7 @@ from importlib import import_module from django.conf import settings from django.utils import dateformat, numberformat from django.utils.functional import lazy -from django.utils.translation import ( - check_for_language, get_language, to_locale, -) +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 @@ -19,33 +17,35 @@ _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,16 +72,16 @@ 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 @@ -91,7 +91,9 @@ def get_format_modules(lang=None): 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)) + _format_modules_cache[lang] = list( + iter_format_modules(lang, settings.FORMAT_MODULE_PATH) + ) return _format_modules_cache[lang] @@ -104,11 +106,14 @@ 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_INTERNAL + if hasattr(settings, "_USE_L10N_INTERNAL") + else settings.USE_L10N + ) + ) if use_l10n and lang is None: lang = get_language() cache_key = (format_type, lang) @@ -152,7 +157,9 @@ 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): @@ -162,7 +169,9 @@ 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): @@ -172,18 +181,21 @@ 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 - )) + 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 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, ) @@ -206,11 +218,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 @@ -226,15 +238,15 @@ 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 = default or get_format("DATETIME_INPUT_FORMATS")[0] format = sanitize_strftime_format(format) return value.strftime(format) elif isinstance(value, datetime.date): - format = default or get_format('DATE_INPUT_FORMATS')[0] + format = default or get_format("DATE_INPUT_FORMATS")[0] format = sanitize_strftime_format(format) return value.strftime(format) elif isinstance(value, datetime.time): - format = default or get_format('TIME_INPUT_FORMATS')[0] + format = default or get_format("TIME_INPUT_FORMATS")[0] return value.strftime(format) return value @@ -262,12 +274,12 @@ def sanitize_strftime_format(fmt): See https://bugs.python.org/issue13305 for more details. """ - if datetime.date(1, 1, 1).strftime('%Y') == '0001': + if datetime.date(1, 1, 1).strftime("%Y") == "0001": return fmt - mapping = {'C': 2, 'F': 10, 'G': 4, 'Y': 4} + 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]), + r"((?:^|[^%])(?:%%)*)%([CFGY])", + lambda m: r"%s%%0%s%s" % (m[1], mapping[m[2]], m[2]), fmt, ) @@ -279,19 +291,25 @@ 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: + 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 diff --git a/django/utils/functional.py b/django/utils/functional.py index ea46cff20f..9e1be0fe0f 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -13,13 +13,14 @@ class cached_property: A cached property can be made out of an existing method: (e.g. ``url = cached_property(get_absolute_url)``). """ + name = None @staticmethod def func(instance): raise TypeError( - 'Cannot use cached_property instance without calling ' - '__set_name__() on it.' + "Cannot use cached_property instance without calling " + "__set_name__() on it." ) def __init__(self, func, name=None): @@ -33,7 +34,7 @@ class cached_property: stacklevel=2, ) 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: @@ -62,6 +63,7 @@ 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 @@ -78,6 +80,7 @@ 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 @@ -96,6 +99,7 @@ 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): @@ -108,7 +112,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): @@ -129,7 +133,7 @@ def lazy(func, *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.' + "Cannot call lazy() with both bytes and text return types." ) if cls._delegate_text: cls.__str__ = cls.__text_cast @@ -144,6 +148,7 @@ 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): @@ -233,10 +238,15 @@ 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 @@ -255,6 +265,7 @@ def new_method_proxy(func): if self._wrapped is empty: self._setup() return func(self._wrapped, *args) + return inner @@ -297,7 +308,9 @@ 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 @@ -376,6 +389,7 @@ 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. @@ -385,7 +399,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): @@ -398,7 +412,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: diff --git a/django/utils/hashable.py b/django/utils/hashable.py index 7d137ccc2f..042e1a4373 100644 --- a/django/utils/hashable.py +++ b/django/utils/hashable.py @@ -8,10 +8,12 @@ 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: diff --git a/django/utils/html.py b/django/utils/html.py index be9f22312e..d228e4c7bc 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -4,9 +4,7 @@ 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 @@ -30,22 +28,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) @@ -55,9 +53,9 @@ def escapejs(value): _json_script_escapes = { - ord('>'): '\\u003E', - ord('<'): '\\u003C', - ord('&'): '\\u0026', + ord(">"): "\\u003E", + ord("<"): "\\u003C", + ord("&"): "\\u0026", } @@ -68,6 +66,7 @@ def json_script(value, element_id=None): 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) if element_id: template = '<script id="{}" type="application/json">{}</script>' @@ -87,7 +86,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) @@ -118,22 +117,23 @@ 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): @@ -146,13 +146,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): @@ -171,9 +171,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 @@ -183,17 +183,18 @@ 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: @@ -210,8 +211,10 @@ 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) @@ -230,17 +233,17 @@ class Urlizer: Links can have trailing punctuation (periods, commas, close-parens) and leading punctuation (opening parens) and it'll still do the right thing. """ - trailing_punctuation_chars = '.,:;!' - wrapping_punctuation = [('(', ')'), ('[', ']')] - simple_url_re = _lazy_re_compile(r'^https?://\[?\w', re.IGNORECASE) + trailing_punctuation_chars = ".,:;!" + wrapping_punctuation = [("(", ")"), ("[", "]")] + + simple_url_re = _lazy_re_compile(r"^https?://\[?\w", re.IGNORECASE) simple_url_2_re = _lazy_re_compile( - r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)($|/.*)$', - re.IGNORECASE + r"^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)($|/.*)$", re.IGNORECASE ) - word_split_re = _lazy_re_compile(r'''([\s<>"']+)''') + word_split_re = _lazy_re_compile(r"""([\s<>"']+)""") - mailto_template = 'mailto:{local}@{domain}' + mailto_template = "mailto:{local}@{domain}" url_template = '<a href="{href}"{attrs}>{url}</a>' def __call__(self, text, trim_url_limit=None, nofollow=False, autoescape=False): @@ -256,39 +259,48 @@ class Urlizer: safe_input = isinstance(text, SafeData) words = self.word_split_re.split(str(text)) - return ''.join([ - self.handle_word( - word, - safe_input=safe_input, - trim_url_limit=trim_url_limit, - nofollow=nofollow, - autoescape=autoescape, - ) for word in words - ]) + return "".join( + [ + self.handle_word( + word, + safe_input=safe_input, + trim_url_limit=trim_url_limit, + nofollow=nofollow, + autoescape=autoescape, + ) + for word in words + ] + ) def handle_word( - self, word, *, safe_input, trim_url_limit=None, nofollow=False, autoescape=False, + self, + word, + *, + safe_input, + trim_url_limit=None, + nofollow=False, + autoescape=False, ): - if '.' in word or '@' in word or ':' in word: + if "." in word or "@" in word or ":" in word: # lead: Punctuation trimmed from the beginning of the word. # middle: State of the word. # trail: Punctuation trimmed from the end of the word. lead, middle, trail = self.trim_punctuation(word) # Make URL we want to point to. url = None - nofollow_attr = ' rel="nofollow"' if nofollow else '' + nofollow_attr = ' rel="nofollow"' if nofollow else "" if self.simple_url_re.match(middle): url = smart_urlquote(html.unescape(middle)) elif self.simple_url_2_re.match(middle): - url = smart_urlquote('http://%s' % html.unescape(middle)) - elif ':' not in middle and self.is_email_simple(middle): - local, domain = middle.rsplit('@', 1) + url = smart_urlquote("http://%s" % html.unescape(middle)) + elif ":" not in middle and self.is_email_simple(middle): + local, domain = middle.rsplit("@", 1) try: domain = punycode(domain) except UnicodeError: return word url = self.mailto_template.format(local=local, domain=domain) - nofollow_attr = '' + nofollow_attr = "" # Make link. if url: trimmed = self.trim_url(middle, limit=trim_url_limit) @@ -300,7 +312,7 @@ class Urlizer: attrs=nofollow_attr, url=trimmed, ) - return mark_safe(f'{lead}{middle}{trail}') + return mark_safe(f"{lead}{middle}{trail}") else: if safe_input: return mark_safe(word) @@ -315,14 +327,14 @@ class Urlizer: def trim_url(self, x, *, 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(self, word): """ Trim trailing and wrapping punctuation from `word`. Return the items of the new state. """ - lead, middle, trail = '', word, '' + lead, middle, trail = "", word, "" # Continue trimming until middle remains unchanged. trimmed_something = True while trimmed_something: @@ -330,15 +342,15 @@ class Urlizer: # Trim wrapping punctuation. for opening, closing in self.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.endswith(closing) + and middle.count(closing) == middle.count(opening) + 1 ): - middle = middle[:-len(closing)] + middle = middle[: -len(closing)] trail = closing + trail trimmed_something = True # Trim trailing punctuation (after trimming wrapping punctuation, @@ -357,15 +369,15 @@ class Urlizer: 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 @@ -375,7 +387,9 @@ urlizer = Urlizer() @keep_lazy_text def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): - return urlizer(text, trim_url_limit=trim_url_limit, nofollow=nofollow, autoescape=autoescape) + return urlizer( + text, trim_url_limit=trim_url_limit, nofollow=nofollow, autoescape=autoescape + ) def avoid_wrapping(value): @@ -391,12 +405,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__ diff --git a/django/utils/http.py b/django/utils/http.py index ab90f1e377..0292713235 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -5,33 +5,42 @@ import unicodedata from binascii import Error as BinasciiError from email.utils import formatdate from urllib.parse import ( - ParseResult, SplitResult, _coerce_args, _splitnetloc, _splitparams, - scheme_chars, urlencode as original_urlencode, uses_params, + ParseResult, + SplitResult, + _coerce_args, + _splitnetloc, + _splitparams, + scheme_chars, ) +from urllib.parse import urlencode as original_urlencode +from urllib.parse import uses_params from django.utils.datastructures import MultiValueDict 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>[0-9]{2})' -__D2 = r'(?P<day>[ 0-9][0-9])' -__M = r'(?P<mon>\w{3})' -__Y = r'(?P<year>[0-9]{4})' -__Y2 = r'(?P<year>[0-9]{2})' -__T = r'(?P<hour>[0-9]{2}):(?P<min>[0-9]{2}):(?P<sec>[0-9]{2})' -RFC1123_DATE = _lazy_re_compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T)) -RFC850_DATE = _lazy_re_compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T)) -ASCTIME_DATE = _lazy_re_compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y)) +MONTHS = "jan feb mar apr may jun jul aug sep oct nov dec".split() +__D = r"(?P<day>[0-9]{2})" +__D2 = r"(?P<day>[ 0-9][0-9])" +__M = r"(?P<mon>\w{3})" +__Y = r"(?P<year>[0-9]{4})" +__Y2 = r"(?P<year>[0-9]{2})" +__T = r"(?P<hour>[0-9]{2}):(?P<min>[0-9]{2}):(?P<sec>[0-9]{2})" +RFC1123_DATE = _lazy_re_compile(r"^\w{3}, %s %s %s %s GMT$" % (__D, __M, __Y, __T)) +RFC850_DATE = _lazy_re_compile(r"^\w{6,9}, %s-%s-%s %s GMT$" % (__D, __M, __Y2, __T)) +ASCTIME_DATE = _lazy_re_compile(r"^\w{3} %s %s %s %s$" % (__M, __D2, __T, __Y)) RFC3986_GENDELIMS = ":/?#[]@" RFC3986_SUBDELIMS = "!$&'()*+,;=" @@ -44,7 +53,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: @@ -112,7 +121,7 @@ def parse_http_date(date): 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_century = current_year - (current_year % 100) @@ -122,11 +131,11 @@ 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']) + 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()) except Exception as exc: @@ -145,6 +154,7 @@ 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 @@ -160,12 +170,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 @@ -177,7 +187,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): @@ -187,7 +197,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) @@ -198,11 +208,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] @@ -231,8 +241,9 @@ 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 ) @@ -259,14 +270,15 @@ 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 ) # 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). @@ -275,41 +287,42 @@ 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) @@ -317,7 +330,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) @@ -332,15 +345,16 @@ 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 + ) def escape_leading_slashes(url): @@ -349,6 +363,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 diff --git a/django/utils/inspect.py b/django/utils/inspect.py index 7e062244e5..28418f7312 100644 --- a/django/utils/inspect.py +++ b/django/utils/inspect.py @@ -19,7 +19,8 @@ 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 ] @@ -35,12 +36,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: @@ -50,28 +51,21 @@ 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 diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py index ddb8c8091d..88dd6ecb4b 100644 --- a/django/utils/ipv6.py +++ b/django/utils/ipv6.py @@ -4,8 +4,9 @@ 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. @@ -25,12 +26,12 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False, 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) diff --git a/django/utils/jslex.py b/django/utils/jslex.py index 8abf5f1126..93a1a2e972 100644 --- a/django/utils/jslex.py +++ b/django/utils/jslex.py @@ -7,6 +7,7 @@ class Tok: """ A specification for a token class. """ + num = 0 def __init__(self, name, regex, next=None): @@ -101,21 +102,34 @@ 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 @@ -128,15 +142,23 @@ 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 = [ @@ -145,13 +167,16 @@ 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.. @@ -174,12 +199,15 @@ 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): @@ -190,31 +218,32 @@ def prepare_js_for_gettext(js): 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) diff --git a/django/utils/log.py b/django/utils/log.py index 5a5decd531..fd0cc1bdc1 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -8,7 +8,7 @@ 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 @@ -16,51 +16,51 @@ request_logger = logging.getLogger('django.request') # require_debug_true filter. This configuration is quoted in # docs/ref/logging.txt; please amend it there if edited here. DEFAULT_LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', + "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', + "django.server": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "django.server", + }, + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + "class": "django.utils.log.AdminEmailHandler", }, - '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,22 +87,24 @@ 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) @@ -118,12 +120,17 @@ 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) @@ -132,7 +139,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): @@ -141,6 +148,7 @@ 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 @@ -161,7 +169,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() @@ -169,7 +177,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: @@ -189,17 +197,25 @@ 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, exception=None): +def log_response( + message, + *args, + response=None, + request=None, + logger=request_logger, + level=None, + exception=None, +): """ Log errors based on HttpResponse status. @@ -211,22 +227,23 @@ def log_response(message, *args, response=None, request=None, logger=request_log # the same response can be received in some cases, e.g., when the # response is the result of an exception and is logged when the exception # is caught, to record the exception. - if getattr(response, '_has_been_logged', False): + 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=exception, ) diff --git a/django/utils/lorem_ipsum.py b/django/utils/lorem_ipsum.py index cfa675d70a..5cbc4e5a60 100644 --- a/django/utils/lorem_ipsum.py +++ b/django/utils/lorem_ipsum.py @@ -5,51 +5,220 @@ 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", ) @@ -62,10 +231,13 @@ 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(): @@ -74,7 +246,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): @@ -111,4 +283,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) diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index bf099cba96..cb579e7f8c 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -8,9 +8,9 @@ from importlib.util import find_spec as importlib_find def cached_import(module_path, class_name): # Check whether module is loaded and fully initialized. if not ( - (module := sys.modules.get(module_path)) and - (spec := getattr(module, '__spec__', None)) and - getattr(spec, '_initializing', False) is False + (module := sys.modules.get(module_path)) + and (spec := getattr(module, "__spec__", None)) + and getattr(spec, "_initializing", False) is False ): module = import_module(module_path) return getattr(module, class_name) @@ -22,15 +22,16 @@ def import_string(dotted_path): 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 try: return cached_import(module_path, class_name) except AttributeError as err: - raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( - module_path, class_name) + raise ImportError( + 'Module "%s" does not define a "%s" attribute/class' + % (module_path, class_name) ) from err @@ -46,7 +47,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. @@ -54,7 +55,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 @@ -79,7 +80,7 @@ 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: @@ -96,11 +97,11 @@ def module_dir(module): over several directories. """ # Convert to list because __path__ may not support indexing. - paths = list(getattr(module, '__path__', [])) + 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) diff --git a/django/utils/numberformat.py b/django/utils/numberformat.py index 3bfdb2ea52..488d6a77cd 100644 --- a/django/utils/numberformat.py +++ b/django/utils/numberformat.py @@ -4,8 +4,15 @@ 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: @@ -18,54 +25,61 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', 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: @@ -76,7 +90,7 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', # 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: diff --git a/django/utils/regex_helper.py b/django/utils/regex_helper.py index 8612475b96..9ee82e1a9b 100644 --- a/django/utils/regex_helper.py +++ b/django/utils/regex_helper.py @@ -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,37 +117,39 @@ 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: @@ -185,7 +187,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))) @@ -201,7 +203,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) @@ -217,16 +219,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 @@ -241,30 +243,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 @@ -290,20 +292,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] @@ -331,7 +333,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 @@ -339,13 +341,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) diff --git a/django/utils/safestring.py b/django/utils/safestring.py index c061717889..b7d1adff62 100644 --- a/django/utils/safestring.py +++ b/django/utils/safestring.py @@ -49,6 +49,7 @@ def _safety_decorator(safety_marker, func): @wraps(func) def wrapped(*args, **kwargs): return safety_marker(func(*args, **kwargs)) + return wrapped @@ -61,7 +62,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) diff --git a/django/utils/termcolors.py b/django/utils/termcolors.py index 089ae63ef0..3d1eee6e41 100644 --- a/django/utils/termcolors.py +++ b/django/utils/termcolors.py @@ -2,15 +2,21 @@ 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. @@ -40,19 +46,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): @@ -68,68 +74,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 @@ -169,39 +175,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 diff --git a/django/utils/text.py b/django/utils/text.py index 915da50483..dcfe3fba0e 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -1,12 +1,14 @@ import re import unicodedata -from gzip import GzipFile, compress as gzip_compress +from gzip import GzipFile +from gzip import compress as gzip_compress from io import BytesIO from django.core.exceptions import SuspiciousFileOperation 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 _, gettext_lazy, pgettext +from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy, pgettext @keep_lazy_text @@ -20,11 +22,11 @@ def capfirst(x): # 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 @@ -39,46 +41,49 @@ 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): """ @@ -90,11 +95,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: @@ -117,8 +122,7 @@ 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 @@ -144,8 +148,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): """ @@ -156,11 +160,18 @@ 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 @@ -202,7 +213,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) @@ -210,12 +221,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 @@ -230,15 +241,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' @@ -252,31 +263,55 @@ 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()) def compress_string(s): @@ -294,7 +329,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: @@ -307,7 +342,8 @@ 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'"]* (?: @@ -315,7 +351,9 @@ smart_split_re = _lazy_re_compile(r""" [^\s'"]* )+ ) | \S+) -""", re.VERBOSE) +""", + re.VERBOSE, +) def smart_split(text): @@ -355,7 +393,7 @@ def unescape_string_literal(s): if not s or s[0] not in "\"'" or s[-1] != s[0]: raise ValueError("Not a string literal: %r" % s) quote = s[0] - return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\') + return s[1:-1].replace(r"\%s" % quote, quote).replace(r"\\", "\\") @keep_lazy_text @@ -368,18 +406,22 @@ 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): diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 157dcb72c2..8a8ffb8151 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -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("%(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"), } 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"] % {"num": 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"] % {"num": 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] % {"num": 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): diff --git a/django/utils/timezone.py b/django/utils/timezone.py index 9572c99bac..71b160448e 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -20,12 +20,21 @@ 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. @@ -39,8 +48,8 @@ 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) @@ -56,6 +65,7 @@ def get_default_timezone(): """ if settings.USE_DEPRECATED_PYTZ: import pytz + return pytz.timezone(settings.TIME_ZONE) return zoneinfo.ZoneInfo(settings.TIME_ZONE) @@ -86,6 +96,7 @@ def _get_timezone_name(timezone): """ return timezone.tzname(None) or str(timezone) + # Timezone selection functions. # These functions don't change os.environ['TZ'] and call time.tzset() @@ -104,6 +115,7 @@ def activate(timezone): elif isinstance(timezone, str): if settings.USE_DEPRECATED_PYTZ: import pytz + _active.value = pytz.timezone(timezone) else: _active.value = zoneinfo.ZoneInfo(timezone) @@ -133,11 +145,12 @@ 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: @@ -152,6 +165,7 @@ 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. @@ -162,16 +176,17 @@ 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. @@ -215,6 +230,7 @@ def 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. @@ -247,9 +263,9 @@ def make_aware(value, timezone=None, is_dst=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.', + "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: @@ -260,8 +276,7 @@ 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) @@ -315,6 +330,7 @@ def _is_pytz_zone(tz): def _datetime_ambiguous_or_imaginary(dt, tz): if _is_pytz_zone(tz): import pytz + try: tz.utcoffset(dt) except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError): diff --git a/django/utils/topological_sort.py b/django/utils/topological_sort.py index f7ce0e0d1d..66b6866ec8 100644 --- a/django/utils/topological_sort.py +++ b/django/utils/topological_sort.py @@ -17,14 +17,20 @@ 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): diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index 29ac60ad1d..6b8cc73d5c 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -9,14 +9,27 @@ 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", + "ngettext", + "ngettext_lazy", + "pgettext", + "pgettext_lazy", + "npgettext", + "npgettext_lazy", ] @@ -32,6 +45,7 @@ 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 @@ -47,13 +61,20 @@ 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, + 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" ) - 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)) @@ -92,31 +113,33 @@ pgettext_lazy = lazy(pgettext, str) 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): @@ -133,7 +156,10 @@ 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 @@ -146,7 +172,9 @@ def ngettext_lazy(singular, plural, number=None): def npgettext_lazy(context, singular, plural, number=None): - return lazy_number(npgettext, str, context=context, singular=singular, plural=plural, number=number) + return lazy_number( + npgettext, str, context=context, singular=singular, plural=plural, number=number + ) def activate(language): @@ -192,27 +220,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('-') + lang, _, country = language.lower().partition("-") if not country: return language[:3].lower() + language[3:] # A language with > 2 characters after the dash only has its first # character after the dash capitalized; e.g. sr-latn becomes sr_Latn. # A language with 2 characters after the dash has both characters # capitalized; e.g. en-us becomes en_US. - country, _, tail = country.partition('-') + country, _, tail = country.partition("-") country = country.title() if len(country) > 2 else country.upper() if tail: - country += '-' + tail - return lang + '_' + country + country += "-" + tail + return lang + "_" + country def get_language_from_request(request, check_path=False): @@ -229,6 +257,7 @@ def get_supported_language_variant(lang_code, *, strict=False): def templatize(src, **kwargs): from .template import templatize + return templatize(src, **kwargs) @@ -238,32 +267,35 @@ 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 diff --git a/django/utils/translation/reloader.py b/django/utils/translation/reloader.py index d8afa89270..be05ccc860 100644 --- a/django/utils/translation/reloader.py +++ b/django/utils/translation/reloader.py @@ -11,23 +11,24 @@ 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 diff --git a/django/utils/translation/template.py b/django/utils/translation/template.py index 588f538cb2..d7353a3028 100644 --- a/django/utils/translation/template.py +++ b/django/utils/translation/template.py @@ -6,9 +6,9 @@ from django.utils.regex_helper import _lazy_re_compile from . import TranslatorCommentWarning, trim_whitespace -TRANSLATOR_COMMENT_MARK = 'Translators' +TRANSLATOR_COMMENT_MARK = "Translators" -dot_re = _lazy_re_compile(r'\S') +dot_re = _lazy_re_compile(r"\S") def blankout(src, char): @@ -28,7 +28,9 @@ 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"""_\(((?:".*?")|(?:'.*?'))\)""") @@ -40,7 +42,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 @@ -52,27 +54,30 @@ 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: @@ -84,36 +89,44 @@ 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 @@ -122,20 +135,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: @@ -144,13 +157,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 " @@ -159,7 +172,9 @@ 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: @@ -172,7 +187,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]) @@ -181,15 +196,17 @@ 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]) @@ -200,30 +217,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() diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 97efd40c6d..52110af83a 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -32,18 +32,20 @@ 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''' +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(?:\.[0-9]{,3})?|1(?:\.0{,3})?))? # Optional "q=1.00", "q=0.8" (?:\s*,\s*|$) # Multiple accepts per header. - ''', re.VERBOSE) + """, + 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+){0,2})(/|$)') +language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+){0,2})(/|$)") @receiver(setting_changed) @@ -52,7 +54,7 @@ def reset_cache(*, setting, **kwargs): Reset global state when LANGUAGES setting has been changed, as some languages should no longer be accepted. """ - if setting in ('LANGUAGES', 'LANGUAGE_CODE'): + if setting in ("LANGUAGES", "LANGUAGE_CODE"): check_for_language.cache_clear() get_languages.cache_clear() get_supported_language_variant.cache_clear() @@ -63,6 +65,7 @@ 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)] @@ -124,7 +127,8 @@ 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""" @@ -140,10 +144,12 @@ 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() @@ -155,9 +161,16 @@ 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. @@ -184,7 +197,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) @@ -196,9 +209,10 @@ 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) @@ -213,9 +227,11 @@ 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: @@ -226,7 +242,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). @@ -321,7 +337,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 @@ -349,7 +365,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) @@ -359,7 +375,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) @@ -404,13 +420,15 @@ 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 @@ -423,10 +441,11 @@ 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] @@ -447,7 +466,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() ) @@ -478,11 +497,11 @@ def get_supported_language_variant(lang_code, strict=False): # language codes i.e. 'zh-hant' and 'zh'. 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: + while (i := lang_code.rfind("-", 0, i)) > -1: possible_lang_codes.append(lang_code[:i]) generic_lang_code = possible_lang_codes[-1] supported_lang_codes = get_languages() @@ -493,7 +512,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) @@ -531,7 +550,11 @@ 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: @@ -539,9 +562,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): @@ -571,7 +594,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: diff --git a/django/utils/tree.py b/django/utils/tree.py index a56442c32d..f67c90eae4 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -14,9 +14,10 @@ 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.""" @@ -41,8 +42,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) @@ -67,14 +68,21 @@ 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 == other.connector + and self.negated == other.negated + and self.children == other.children ) def __hash__(self): - return hash((self.__class__, self.connector, self.negated, *make_hashable(self.children))) + return hash( + ( + self.__class__, + self.connector, + self.negated, + *make_hashable(self.children), + ) + ) def add(self, data, conn_type): """ @@ -94,9 +102,9 @@ class Node: self.children = [obj, data] return data elif ( - isinstance(data, Node) and - not data.negated and - (data.connector == conn_type or len(data) == 1) + 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 diff --git a/django/utils/version.py b/django/utils/version.py index 7f4e9e3cce..1e20a86563 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -28,14 +28,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 +45,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 +57,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 @@ -80,12 +80,15 @@ def get_git_changeset(): """ # Repository may not be found if __file__ is undefined, e.g. in a frozen # module. - if '__file__' not in globals(): + if "__file__" not in globals(): return None repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) git_log = subprocess.run( - 'git log --pretty=format:%ct --quiet -1 HEAD', - capture_output=True, shell=True, cwd=repo_dir, text=True, + "git log --pretty=format:%ct --quiet -1 HEAD", + capture_output=True, + shell=True, + cwd=repo_dir, + text=True, ) timestamp = git_log.stdout tz = datetime.timezone.utc @@ -93,10 +96,10 @@ def get_git_changeset(): timestamp = datetime.datetime.fromtimestamp(int(timestamp), tz=tz) except ValueError: return None - return timestamp.strftime('%Y%m%d%H%M%S') + return timestamp.strftime("%Y%m%d%H%M%S") -version_component_re = _lazy_re_compile(r'(\d+|[a-z]+|\.)') +version_component_re = _lazy_re_compile(r"(\d+|[a-z]+|\.)") def get_version_tuple(version): @@ -106,7 +109,7 @@ def get_version_tuple(version): """ version_numbers = [] for item in version_component_re.split(version): - if item and item != '.': + if item and item != ".": try: component = int(item) except ValueError: diff --git a/django/utils/xmlutils.py b/django/utils/xmlutils.py index e4607b9865..c3eb3ba6a3 100644 --- a/django/utils/xmlutils.py +++ b/django/utils/xmlutils.py @@ -21,10 +21,12 @@ 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): |
