summaryrefslogtreecommitdiffhomepage
path: root/scripts/jinja2/utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/jinja2/utils.py')
-rw-r--r--scripts/jinja2/utils.py849
1 files changed, 588 insertions, 261 deletions
diff --git a/scripts/jinja2/utils.py b/scripts/jinja2/utils.py
index 1045e3f..567185f 100644
--- a/scripts/jinja2/utils.py
+++ b/scripts/jinja2/utils.py
@@ -1,89 +1,164 @@
-# -*- coding: utf-8 -*-
-"""
- jinja2.utils
- ~~~~~~~~~~~~
-
- Utility functions.
-
- :copyright: (c) 2010 by the Jinja Team.
- :license: BSD, see LICENSE for more details.
-"""
+import enum
+import json
+import os
import re
-import errno
+import typing as t
+import warnings
+from collections import abc
from collections import deque
+from random import choice
+from random import randrange
from threading import Lock
-from jinja2._compat import text_type, string_types, implements_iterator, \
- url_quote
+from types import CodeType
+from urllib.parse import quote_from_bytes
+import markupsafe
-_word_split_re = re.compile(r'(\s+)')
-_punctuation_re = re.compile(
- '^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % (
- '|'.join(map(re.escape, ('(', '<', '&lt;'))),
- '|'.join(map(re.escape, ('.', ',', ')', '>', '\n', '&gt;')))
- )
-)
-_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
-_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
-_entity_re = re.compile(r'&([^;]+);')
-_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
-_digits = '0123456789'
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+F = t.TypeVar("F", bound=t.Callable[..., t.Any])
# special singleton representing missing values for the runtime
-missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})()
+missing: t.Any = type("MissingType", (), {"__repr__": lambda x: "missing"})()
+
+internal_code: t.MutableSet[CodeType] = set()
-# internal code
-internal_code = set()
+concat = "".join
-concat = u''.join
+def pass_context(f: F) -> F:
+ """Pass the :class:`~jinja2.runtime.Context` as the first argument
+ to the decorated function when called while rendering a template.
-def contextfunction(f):
- """This decorator can be used to mark a function or method context callable.
- A context callable is passed the active :class:`Context` as first argument when
- called from the template. This is useful if a function wants to get access
- to the context or functions provided on the context object. For example
- a function that returns a sorted list of template variables the current
- template exports could look like this::
+ Can be used on functions, filters, and tests.
- @contextfunction
- def get_exported_names(context):
- return sorted(context.exported_vars)
+ If only ``Context.eval_context`` is needed, use
+ :func:`pass_eval_context`. If only ``Context.environment`` is
+ needed, use :func:`pass_environment`.
+
+ .. versionadded:: 3.0.0
+ Replaces ``contextfunction`` and ``contextfilter``.
"""
- f.contextfunction = True
+ f.jinja_pass_arg = _PassArg.context # type: ignore
return f
-def evalcontextfunction(f):
- """This decorator can be used to mark a function or method as an eval
- context callable. This is similar to the :func:`contextfunction`
- but instead of passing the context, an evaluation context object is
- passed. For more information about the eval context, see
- :ref:`eval-context`.
+def pass_eval_context(f: F) -> F:
+ """Pass the :class:`~jinja2.nodes.EvalContext` as the first argument
+ to the decorated function when called while rendering a template.
+ See :ref:`eval-context`.
- .. versionadded:: 2.4
+ Can be used on functions, filters, and tests.
+
+ If only ``EvalContext.environment`` is needed, use
+ :func:`pass_environment`.
+
+ .. versionadded:: 3.0.0
+ Replaces ``evalcontextfunction`` and ``evalcontextfilter``.
"""
- f.evalcontextfunction = True
+ f.jinja_pass_arg = _PassArg.eval_context # type: ignore
return f
-def environmentfunction(f):
- """This decorator can be used to mark a function or method as environment
- callable. This decorator works exactly like the :func:`contextfunction`
- decorator just that the first argument is the active :class:`Environment`
- and not context.
+def pass_environment(f: F) -> F:
+ """Pass the :class:`~jinja2.Environment` as the first argument to
+ the decorated function when called while rendering a template.
+
+ Can be used on functions, filters, and tests.
+
+ .. versionadded:: 3.0.0
+ Replaces ``environmentfunction`` and ``environmentfilter``.
"""
- f.environmentfunction = True
+ f.jinja_pass_arg = _PassArg.environment # type: ignore
return f
-def internalcode(f):
+class _PassArg(enum.Enum):
+ context = enum.auto()
+ eval_context = enum.auto()
+ environment = enum.auto()
+
+ @classmethod
+ def from_obj(cls, obj: F) -> t.Optional["_PassArg"]:
+ if hasattr(obj, "jinja_pass_arg"):
+ return obj.jinja_pass_arg # type: ignore
+
+ for prefix in "context", "eval_context", "environment":
+ squashed = prefix.replace("_", "")
+
+ for name in f"{squashed}function", f"{squashed}filter":
+ if getattr(obj, name, False) is True:
+ warnings.warn(
+ f"{name!r} is deprecated and will stop working"
+ f" in Jinja 3.1. Use 'pass_{prefix}' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return cls[prefix]
+
+ return None
+
+
+def contextfunction(f: F) -> F:
+ """Pass the context as the first argument to the decorated function.
+
+ .. deprecated:: 3.0
+ Will be removed in Jinja 3.1. Use :func:`~jinja2.pass_context`
+ instead.
+ """
+ warnings.warn(
+ "'contextfunction' is renamed to 'pass_context', the old name"
+ " will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_context(f)
+
+
+def evalcontextfunction(f: F) -> F:
+ """Pass the eval context as the first argument to the decorated
+ function.
+
+ .. deprecated:: 3.0
+ Will be removed in Jinja 3.1. Use
+ :func:`~jinja2.pass_eval_context` instead.
+
+ .. versionadded:: 2.4
+ """
+ warnings.warn(
+ "'evalcontextfunction' is renamed to 'pass_eval_context', the"
+ " old name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_eval_context(f)
+
+
+def environmentfunction(f: F) -> F:
+ """Pass the environment as the first argument to the decorated
+ function.
+
+ .. deprecated:: 3.0
+ Will be removed in Jinja 3.1. Use
+ :func:`~jinja2.pass_environment` instead.
+ """
+ warnings.warn(
+ "'environmentfunction' is renamed to 'pass_environment', the"
+ " old name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return pass_environment(f)
+
+
+def internalcode(f: F) -> F:
"""Marks the function as internally used"""
internal_code.add(f.__code__)
return f
-def is_undefined(obj):
+def is_undefined(obj: t.Any) -> bool:
"""Check if the object passed is undefined. This does nothing more than
performing an instance check against :class:`Undefined` but looks nicer.
This can be used for custom filters or tests that want to react to
@@ -95,29 +170,31 @@ def is_undefined(obj):
return default
return var
"""
- from jinja2.runtime import Undefined
+ from .runtime import Undefined
+
return isinstance(obj, Undefined)
-def consume(iterable):
+def consume(iterable: t.Iterable[t.Any]) -> None:
"""Consumes an iterable without doing anything with it."""
- for event in iterable:
+ for _ in iterable:
pass
-def clear_caches():
- """Jinja2 keeps internal caches for environments and lexers. These are
- used so that Jinja2 doesn't have to recreate environments and lexers all
+def clear_caches() -> None:
+ """Jinja keeps internal caches for environments and lexers. These are
+ used so that Jinja doesn't have to recreate environments and lexers all
the time. Normally you don't have to care about that but if you are
- messuring memory consumption you may want to clean the caches.
+ measuring memory consumption you may want to clean the caches.
"""
- from jinja2.environment import _spontaneous_environments
- from jinja2.lexer import _lexer_cache
- _spontaneous_environments.clear()
+ from .environment import get_spontaneous_environment
+ from .lexer import _lexer_cache
+
+ get_spontaneous_environment.cache_clear()
_lexer_cache.clear()
-def import_string(import_name, silent=False):
+def import_string(import_name: str, silent: bool = False) -> t.Any:
"""Imports an object based on a string. This is useful if you want to
use import paths as endpoints or something similar. An import path can
be specified either in dotted notation (``xml.sax.saxutils.escape``)
@@ -129,12 +206,10 @@ def import_string(import_name, silent=False):
:return: imported object
"""
try:
- if ':' in import_name:
- module, obj = import_name.split(':', 1)
- elif '.' in import_name:
- items = import_name.split('.')
- module = '.'.join(items[:-1])
- obj = items[-1]
+ if ":" in import_name:
+ module, obj = import_name.split(":", 1)
+ elif "." in import_name:
+ module, _, obj = import_name.rpartition(".")
else:
return __import__(import_name)
return getattr(__import__(module, None, None, [obj]), obj)
@@ -143,101 +218,199 @@ def import_string(import_name, silent=False):
raise
-def open_if_exists(filename, mode='rb'):
+def open_if_exists(filename: str, mode: str = "rb") -> t.Optional[t.IO]:
"""Returns a file descriptor for the filename if that file exists,
- otherwise `None`.
+ otherwise ``None``.
"""
- try:
- return open(filename, mode)
- except IOError as e:
- if e.errno not in (errno.ENOENT, errno.EISDIR):
- raise
+ if not os.path.isfile(filename):
+ return None
+ return open(filename, mode)
-def object_type_repr(obj):
+
+def object_type_repr(obj: t.Any) -> str:
"""Returns the name of the object's type. For some recognized
singletons the name of the object is returned instead. (For
example for `None` and `Ellipsis`).
"""
if obj is None:
- return 'None'
+ return "None"
elif obj is Ellipsis:
- return 'Ellipsis'
- # __builtin__ in 2.x, builtins in 3.x
- if obj.__class__.__module__ in ('__builtin__', 'builtins'):
- name = obj.__class__.__name__
- else:
- name = obj.__class__.__module__ + '.' + obj.__class__.__name__
- return '%s object' % name
+ return "Ellipsis"
+ cls = type(obj)
-def pformat(obj, verbose=False):
- """Prettyprint an object. Either use the `pretty` library or the
- builtin `pprint`.
- """
- try:
- from pretty import pretty
- return pretty(obj, verbose=verbose)
- except ImportError:
- from pprint import pformat
- return pformat(obj)
+ if cls.__module__ == "builtins":
+ return f"{cls.__name__} object"
+ return f"{cls.__module__}.{cls.__name__} object"
-def urlize(text, trim_url_limit=None, nofollow=False, target=None):
- """Converts any URLs in text into clickable links. Works on http://,
- https:// and www. links. Links can have trailing punctuation (periods,
- commas, close-parens) and leading punctuation (opening parens) and
- it'll still do the right thing.
- If trim_url_limit is not None, the URLs in link text will be limited
- to trim_url_limit characters.
+def pformat(obj: t.Any) -> str:
+ """Format an object using :func:`pprint.pformat`."""
+ from pprint import pformat # type: ignore
- If nofollow is True, the URLs in link text will get a rel="nofollow"
- attribute.
+ return pformat(obj)
- If target is not None, a target attribute will be added to the link.
+
+_http_re = re.compile(
+ r"""
+ ^
+ (
+ (https?://|www\.) # scheme or www
+ (([\w%-]+\.)+)? # subdomain
+ (
+ [a-z]{2,63} # basic tld
+ |
+ xn--[\w%]{2,59} # idna tld
+ )
+ |
+ ([\w%-]{2,63}\.)+ # basic domain
+ (com|net|int|edu|gov|org|info|mil) # basic tld
+ |
+ (https?://) # scheme
+ (
+ (([\d]{1,3})(\.[\d]{1,3}){3}) # IPv4
+ |
+ (\[([\da-f]{0,4}:){2}([\da-f]{0,4}:?){1,6}]) # IPv6
+ )
+ )
+ (?::[\d]{1,5})? # port
+ (?:[/?#]\S*)? # path, query, and fragment
+ $
+ """,
+ re.IGNORECASE | re.VERBOSE,
+)
+_email_re = re.compile(r"^\S+@\w[\w.-]*\.\w+$")
+
+
+def urlize(
+ text: str,
+ trim_url_limit: t.Optional[int] = None,
+ rel: t.Optional[str] = None,
+ target: t.Optional[str] = None,
+ extra_schemes: t.Optional[t.Iterable[str]] = None,
+) -> str:
+ """Convert URLs in text into clickable links.
+
+ This may not recognize links in some situations. Usually, a more
+ comprehensive formatter, such as a Markdown library, is a better
+ choice.
+
+ Works on ``http://``, ``https://``, ``www.``, ``mailto:``, and email
+ addresses. Links with trailing punctuation (periods, commas, closing
+ parentheses) and leading punctuation (opening parentheses) are
+ recognized excluding the punctuation. Email addresses that include
+ header fields are not recognized (for example,
+ ``mailto:address@example.com?cc=copy@example.com``).
+
+ :param text: Original text containing URLs to link.
+ :param trim_url_limit: Shorten displayed URL values to this length.
+ :param target: Add the ``target`` attribute to links.
+ :param rel: Add the ``rel`` attribute to links.
+ :param extra_schemes: Recognize URLs that start with these schemes
+ in addition to the default behavior.
+
+ .. versionchanged:: 3.0
+ The ``extra_schemes`` parameter was added.
+
+ .. versionchanged:: 3.0
+ Generate ``https://`` links for URLs without a scheme.
+
+ .. versionchanged:: 3.0
+ The parsing rules were updated. Recognize email addresses with
+ or without the ``mailto:`` scheme. Validate IP addresses. Ignore
+ parentheses and brackets in more cases.
"""
- trim_url = lambda x, limit=trim_url_limit: limit is not None \
- and (x[:limit] + (len(x) >=limit and '...'
- or '')) or x
- words = _word_split_re.split(text_type(escape(text)))
- nofollow_attr = nofollow and ' rel="nofollow"' or ''
- if target is not None and isinstance(target, string_types):
- target_attr = ' target="%s"' % target
+ if trim_url_limit is not None:
+
+ def trim_url(x: str) -> str:
+ if len(x) > trim_url_limit: # type: ignore
+ return f"{x[:trim_url_limit]}..."
+
+ return x
+
else:
- target_attr = ''
+
+ def trim_url(x: str) -> str:
+ return x
+
+ words = re.split(r"(\s+)", str(markupsafe.escape(text)))
+ rel_attr = f' rel="{markupsafe.escape(rel)}"' if rel else ""
+ target_attr = f' target="{markupsafe.escape(target)}"' if target else ""
+
for i, word in enumerate(words):
- match = _punctuation_re.match(word)
+ head, middle, tail = "", word, ""
+ match = re.match(r"^([(<]|&lt;)+", middle)
+
if match:
- lead, middle, trail = match.groups()
- if middle.startswith('www.') or (
- '@' not in middle and
- not middle.startswith('http://') and
- not middle.startswith('https://') and
- len(middle) > 0 and
- middle[0] in _letters + _digits and (
- middle.endswith('.org') or
- middle.endswith('.net') or
- middle.endswith('.com')
- )):
- middle = '<a href="http://%s"%s%s>%s</a>' % (middle,
- nofollow_attr, target_attr, trim_url(middle))
- if middle.startswith('http://') or \
- middle.startswith('https://'):
- middle = '<a href="%s"%s%s>%s</a>' % (middle,
- nofollow_attr, target_attr, trim_url(middle))
- if '@' in middle and not middle.startswith('www.') and \
- not ':' in middle and _simple_email_re.match(middle):
- middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
- if lead + middle + trail != word:
- words[i] = lead + middle + trail
- return u''.join(words)
-
-
-def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
- """Generate some lorem impsum for the template."""
- from jinja2.constants import LOREM_IPSUM_WORDS
- from random import choice, randrange
+ head = match.group()
+ middle = middle[match.end() :]
+
+ # Unlike lead, which is anchored to the start of the string,
+ # need to check that the string ends with any of the characters
+ # before trying to match all of them, to avoid backtracking.
+ if middle.endswith((")", ">", ".", ",", "\n", "&gt;")):
+ match = re.search(r"([)>.,\n]|&gt;)+$", middle)
+
+ if match:
+ tail = match.group()
+ middle = middle[: match.start()]
+
+ # Prefer balancing parentheses in URLs instead of ignoring a
+ # trailing character.
+ for start_char, end_char in ("(", ")"), ("<", ">"), ("&lt;", "&gt;"):
+ start_count = middle.count(start_char)
+
+ if start_count <= middle.count(end_char):
+ # Balanced, or lighter on the left
+ continue
+
+ # Move as many as possible from the tail to balance
+ for _ in range(min(start_count, tail.count(end_char))):
+ end_index = tail.index(end_char) + len(end_char)
+ # Move anything in the tail before the end char too
+ middle += tail[:end_index]
+ tail = tail[end_index:]
+
+ if _http_re.match(middle):
+ if middle.startswith("https://") or middle.startswith("http://"):
+ middle = (
+ f'<a href="{middle}"{rel_attr}{target_attr}>{trim_url(middle)}</a>'
+ )
+ else:
+ middle = (
+ f'<a href="https://{middle}"{rel_attr}{target_attr}>'
+ f"{trim_url(middle)}</a>"
+ )
+
+ elif middle.startswith("mailto:") and _email_re.match(middle[7:]):
+ middle = f'<a href="{middle}">{middle[7:]}</a>'
+
+ elif (
+ "@" in middle
+ and not middle.startswith("www.")
+ and ":" not in middle
+ and _email_re.match(middle)
+ ):
+ middle = f'<a href="mailto:{middle}">{middle}</a>'
+
+ elif extra_schemes is not None:
+ for scheme in extra_schemes:
+ if middle != scheme and middle.startswith(scheme):
+ middle = f'<a href="{middle}"{rel_attr}{target_attr}>{middle}</a>'
+
+ words[i] = f"{head}{middle}{tail}"
+
+ return "".join(words)
+
+
+def generate_lorem_ipsum(
+ n: int = 5, html: bool = True, min: int = 20, max: int = 100
+) -> str:
+ """Generate some lorem ipsum for the template."""
+ from .constants import LOREM_IPSUM_WORDS
+
words = LOREM_IPSUM_WORDS.split()
result = []
@@ -262,56 +435,81 @@ def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
if idx - randrange(3, 8) > last_comma:
last_comma = idx
last_fullstop += 2
- word += ','
+ word += ","
# add end of sentences
if idx - randrange(10, 20) > last_fullstop:
last_comma = last_fullstop = idx
- word += '.'
+ word += "."
next_capitalized = True
p.append(word)
# ensure that the paragraph ends with a dot.
- p = u' '.join(p)
- if p.endswith(','):
- p = p[:-1] + '.'
- elif not p.endswith('.'):
- p += '.'
- result.append(p)
+ p_str = " ".join(p)
+
+ if p_str.endswith(","):
+ p_str = p_str[:-1] + "."
+ elif not p_str.endswith("."):
+ p_str += "."
+
+ result.append(p_str)
if not html:
- return u'\n\n'.join(result)
- return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result))
+ return "\n\n".join(result)
+ return markupsafe.Markup(
+ "\n".join(f"<p>{markupsafe.escape(x)}</p>" for x in result)
+ )
-def unicode_urlencode(obj, charset='utf-8'):
- """URL escapes a single bytestring or unicode string with the
- given charset if applicable to URL safe quoting under all rules
- that need to be considered under all supported Python versions.
+def url_quote(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str:
+ """Quote a string for use in a URL using the given charset.
- If non strings are provided they are converted to their unicode
- representation first.
+ :param obj: String or bytes to quote. Other types are converted to
+ string then encoded to bytes using the given charset.
+ :param charset: Encode text to bytes using this charset.
+ :param for_qs: Quote "/" and use "+" for spaces.
"""
- if not isinstance(obj, string_types):
- obj = text_type(obj)
- if isinstance(obj, text_type):
+ if not isinstance(obj, bytes):
+ if not isinstance(obj, str):
+ obj = str(obj)
+
obj = obj.encode(charset)
- return text_type(url_quote(obj))
+ safe = b"" if for_qs else b"/"
+ rv = quote_from_bytes(obj, safe)
+
+ if for_qs:
+ rv = rv.replace("%20", "+")
-class LRUCache(object):
+ return rv
+
+
+def unicode_urlencode(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str:
+ import warnings
+
+ warnings.warn(
+ "'unicode_urlencode' has been renamed to 'url_quote'. The old"
+ " name will be removed in Jinja 3.1.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return url_quote(obj, charset=charset, for_qs=for_qs)
+
+
+@abc.MutableMapping.register
+class LRUCache:
"""A simple LRU Cache implementation."""
# this is fast for small capacities (something below 1000) but doesn't
# scale. But as long as it's only used as storage for templates this
# won't do any harm.
- def __init__(self, capacity):
+ def __init__(self, capacity: int) -> None:
self.capacity = capacity
- self._mapping = {}
- self._queue = deque()
+ self._mapping: t.Dict[t.Any, t.Any] = {}
+ self._queue: "te.Deque[t.Any]" = deque()
self._postinit()
- def _postinit(self):
+ def _postinit(self) -> None:
# alias all queue methods for faster lookup
self._popleft = self._queue.popleft
self._pop = self._queue.pop
@@ -319,80 +517,70 @@ class LRUCache(object):
self._wlock = Lock()
self._append = self._queue.append
- def __getstate__(self):
+ def __getstate__(self) -> t.Mapping[str, t.Any]:
return {
- 'capacity': self.capacity,
- '_mapping': self._mapping,
- '_queue': self._queue
+ "capacity": self.capacity,
+ "_mapping": self._mapping,
+ "_queue": self._queue,
}
- def __setstate__(self, d):
+ def __setstate__(self, d: t.Mapping[str, t.Any]) -> None:
self.__dict__.update(d)
self._postinit()
- def __getnewargs__(self):
+ def __getnewargs__(self) -> t.Tuple:
return (self.capacity,)
- def copy(self):
+ def copy(self) -> "LRUCache":
"""Return a shallow copy of the instance."""
rv = self.__class__(self.capacity)
rv._mapping.update(self._mapping)
- rv._queue = deque(self._queue)
+ rv._queue.extend(self._queue)
return rv
- def get(self, key, default=None):
+ def get(self, key: t.Any, default: t.Any = None) -> t.Any:
"""Return an item from the cache dict or `default`"""
try:
return self[key]
except KeyError:
return default
- def setdefault(self, key, default=None):
+ def setdefault(self, key: t.Any, default: t.Any = None) -> t.Any:
"""Set `default` if the key is not in the cache otherwise
leave unchanged. Return the value of this key.
"""
- self._wlock.acquire()
try:
- try:
- return self[key]
- except KeyError:
- self[key] = default
- return default
- finally:
- self._wlock.release()
+ return self[key]
+ except KeyError:
+ self[key] = default
+ return default
- def clear(self):
+ def clear(self) -> None:
"""Clear the cache."""
- self._wlock.acquire()
- try:
+ with self._wlock:
self._mapping.clear()
self._queue.clear()
- finally:
- self._wlock.release()
- def __contains__(self, key):
+ def __contains__(self, key: t.Any) -> bool:
"""Check if a key exists in this cache."""
return key in self._mapping
- def __len__(self):
+ def __len__(self) -> int:
"""Return the current size of the cache."""
return len(self._mapping)
- def __repr__(self):
- return '<%s %r>' % (
- self.__class__.__name__,
- self._mapping
- )
+ def __repr__(self) -> str:
+ return f"<{type(self).__name__} {self._mapping!r}>"
- def __getitem__(self, key):
+ def __getitem__(self, key: t.Any) -> t.Any:
"""Get an item from the cache. Moves the item up so that it has the
highest priority then.
Raise a `KeyError` if it does not exist.
"""
- self._wlock.acquire()
- try:
+ with self._wlock:
rv = self._mapping[key]
+
if self._queue[-1] != key:
try:
self._remove(key)
@@ -401,73 +589,55 @@ class LRUCache(object):
# when we read, ignore the ValueError that we would
# get otherwise.
pass
+
self._append(key)
+
return rv
- finally:
- self._wlock.release()
- def __setitem__(self, key, value):
+ def __setitem__(self, key: t.Any, value: t.Any) -> None:
"""Sets the value for an item. Moves the item up so that it
has the highest priority then.
"""
- self._wlock.acquire()
- try:
+ with self._wlock:
if key in self._mapping:
self._remove(key)
elif len(self._mapping) == self.capacity:
del self._mapping[self._popleft()]
+
self._append(key)
self._mapping[key] = value
- finally:
- self._wlock.release()
- def __delitem__(self, key):
+ def __delitem__(self, key: t.Any) -> None:
"""Remove an item from the cache dict.
Raise a `KeyError` if it does not exist.
"""
- self._wlock.acquire()
- try:
+ with self._wlock:
del self._mapping[key]
+
try:
self._remove(key)
except ValueError:
- # __getitem__ is not locked, it might happen
pass
- finally:
- self._wlock.release()
- def items(self):
+ def items(self) -> t.Iterable[t.Tuple[t.Any, t.Any]]:
"""Return a list of items."""
result = [(key, self._mapping[key]) for key in list(self._queue)]
result.reverse()
return result
- def iteritems(self):
- """Iterate over all items."""
- return iter(self.items())
-
- def values(self):
+ def values(self) -> t.Iterable[t.Any]:
"""Return a list of all values."""
return [x[1] for x in self.items()]
- def itervalue(self):
- """Iterate over all values."""
- return iter(self.values())
-
- def keys(self):
+ def keys(self) -> t.Iterable[t.Any]:
"""Return a list of all keys ordered by most recent usage."""
return list(self)
- def iterkeys(self):
- """Iterate over all keys in the cache dict, ordered by
- the most recent usage.
- """
+ def __iter__(self) -> t.Iterator[t.Any]:
return reversed(tuple(self._queue))
- __iter__ = iterkeys
-
- def __reversed__(self):
- """Iterate over the values in the cache dict, oldest items
+ def __reversed__(self) -> t.Iterator[t.Any]:
+ """Iterate over the keys in the cache dict, oldest items
coming first.
"""
return iter(tuple(self._queue))
@@ -475,53 +645,210 @@ class LRUCache(object):
__copy__ = copy
-# register the LRU cache as mutable mapping if possible
-try:
- from collections import MutableMapping
- MutableMapping.register(LRUCache)
-except ImportError:
- pass
+def select_autoescape(
+ enabled_extensions: t.Collection[str] = ("html", "htm", "xml"),
+ disabled_extensions: t.Collection[str] = (),
+ default_for_string: bool = True,
+ default: bool = False,
+) -> t.Callable[[t.Optional[str]], bool]:
+ """Intelligently sets the initial value of autoescaping based on the
+ filename of the template. This is the recommended way to configure
+ autoescaping if you do not want to write a custom function yourself.
+
+ If you want to enable it for all templates created from strings or
+ for all templates with `.html` and `.xml` extensions::
+
+ from jinja2 import Environment, select_autoescape
+ env = Environment(autoescape=select_autoescape(
+ enabled_extensions=('html', 'xml'),
+ default_for_string=True,
+ ))
+
+ Example configuration to turn it on at all times except if the template
+ ends with `.txt`::
+
+ from jinja2 import Environment, select_autoescape
+ env = Environment(autoescape=select_autoescape(
+ disabled_extensions=('txt',),
+ default_for_string=True,
+ default=True,
+ ))
+
+ The `enabled_extensions` is an iterable of all the extensions that
+ autoescaping should be enabled for. Likewise `disabled_extensions` is
+ a list of all templates it should be disabled for. If a template is
+ loaded from a string then the default from `default_for_string` is used.
+ If nothing matches then the initial value of autoescaping is set to the
+ value of `default`.
+
+ For security reasons this function operates case insensitive.
+
+ .. versionadded:: 2.9
+ """
+ enabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in enabled_extensions)
+ disabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in disabled_extensions)
+
+ def autoescape(template_name: t.Optional[str]) -> bool:
+ if template_name is None:
+ return default_for_string
+ template_name = template_name.lower()
+ if template_name.endswith(enabled_patterns):
+ return True
+ if template_name.endswith(disabled_patterns):
+ return False
+ return default
+
+ return autoescape
+
+
+def htmlsafe_json_dumps(
+ obj: t.Any, dumps: t.Optional[t.Callable[..., str]] = None, **kwargs: t.Any
+) -> markupsafe.Markup:
+ """Serialize an object to a string of JSON with :func:`json.dumps`,
+ then replace HTML-unsafe characters with Unicode escapes and mark
+ the result safe with :class:`~markupsafe.Markup`.
+ This is available in templates as the ``|tojson`` filter.
-@implements_iterator
-class Cycler(object):
- """A cycle helper for templates."""
+ The following characters are escaped: ``<``, ``>``, ``&``, ``'``.
- def __init__(self, *items):
+ The returned string is safe to render in HTML documents and
+ ``<script>`` tags. The exception is in HTML attributes that are
+ double quoted; either use single quotes or the ``|forceescape``
+ filter.
+
+ :param obj: The object to serialize to JSON.
+ :param dumps: The ``dumps`` function to use. Defaults to
+ ``env.policies["json.dumps_function"]``, which defaults to
+ :func:`json.dumps`.
+ :param kwargs: Extra arguments to pass to ``dumps``. Merged onto
+ ``env.policies["json.dumps_kwargs"]``.
+
+ .. versionchanged:: 3.0
+ The ``dumper`` parameter is renamed to ``dumps``.
+
+ .. versionadded:: 2.9
+ """
+ if dumps is None:
+ dumps = json.dumps
+
+ return markupsafe.Markup(
+ dumps(obj, **kwargs)
+ .replace("<", "\\u003c")
+ .replace(">", "\\u003e")
+ .replace("&", "\\u0026")
+ .replace("'", "\\u0027")
+ )
+
+
+class Cycler:
+ """Cycle through values by yield them one at a time, then restarting
+ once the end is reached. Available as ``cycler`` in templates.
+
+ Similar to ``loop.cycle``, but can be used outside loops or across
+ multiple loops. For example, render a list of folders and files in a
+ list, alternating giving them "odd" and "even" classes.
+
+ .. code-block:: html+jinja
+
+ {% set row_class = cycler("odd", "even") %}
+ <ul class="browser">
+ {% for folder in folders %}
+ <li class="folder {{ row_class.next() }}">{{ folder }}
+ {% endfor %}
+ {% for file in files %}
+ <li class="file {{ row_class.next() }}">{{ file }}
+ {% endfor %}
+ </ul>
+
+ :param items: Each positional argument will be yielded in the order
+ given for each cycle.
+
+ .. versionadded:: 2.1
+ """
+
+ def __init__(self, *items: t.Any) -> None:
if not items:
- raise RuntimeError('at least one item has to be provided')
+ raise RuntimeError("at least one item has to be provided")
self.items = items
- self.reset()
+ self.pos = 0
- def reset(self):
- """Resets the cycle."""
+ def reset(self) -> None:
+ """Resets the current item to the first item."""
self.pos = 0
@property
- def current(self):
- """Returns the current item."""
+ def current(self) -> t.Any:
+ """Return the current item. Equivalent to the item that will be
+ returned next time :meth:`next` is called.
+ """
return self.items[self.pos]
- def __next__(self):
- """Goes one item ahead and returns it."""
+ def next(self) -> t.Any:
+ """Return the current item, then advance :attr:`current` to the
+ next item.
+ """
rv = self.current
self.pos = (self.pos + 1) % len(self.items)
return rv
+ __next__ = next
-class Joiner(object):
+
+class Joiner:
"""A joining helper for templates."""
- def __init__(self, sep=u', '):
+ def __init__(self, sep: str = ", ") -> None:
self.sep = sep
self.used = False
- def __call__(self):
+ def __call__(self) -> str:
if not self.used:
self.used = True
- return u''
+ return ""
return self.sep
-# Imported here because that's where it was in the past
-from markupsafe import Markup, escape, soft_unicode
+class Namespace:
+ """A namespace object that can hold arbitrary attributes. It may be
+ initialized from a dictionary or with keyword arguments."""
+
+ def __init__(*args: t.Any, **kwargs: t.Any) -> None: # noqa: B902
+ self, args = args[0], args[1:]
+ self.__attrs = dict(*args, **kwargs)
+
+ def __getattribute__(self, name: str) -> t.Any:
+ # __class__ is needed for the awaitable check in async mode
+ if name in {"_Namespace__attrs", "__class__"}:
+ return object.__getattribute__(self, name)
+ try:
+ return self.__attrs[name]
+ except KeyError:
+ raise AttributeError(name) from None
+
+ def __setitem__(self, name: str, value: t.Any) -> None:
+ self.__attrs[name] = value
+
+ def __repr__(self) -> str:
+ return f"<Namespace {self.__attrs!r}>"
+
+
+class Markup(markupsafe.Markup):
+ def __new__(cls, base="", encoding=None, errors="strict"): # type: ignore
+ warnings.warn(
+ "'jinja2.Markup' is deprecated and will be removed in Jinja"
+ " 3.1. Import 'markupsafe.Markup' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return super().__new__(cls, base, encoding, errors)
+
+
+def escape(s: t.Any) -> str:
+ warnings.warn(
+ "'jinja2.escape' is deprecated and will be removed in Jinja"
+ " 3.1. Import 'markupsafe.escape' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return markupsafe.escape(s)