diff options
Diffstat (limited to 'deps/v8/third_party/jinja2/runtime.py')
-rw-r--r-- | deps/v8/third_party/jinja2/runtime.py | 816 |
1 files changed, 429 insertions, 387 deletions
diff --git a/deps/v8/third_party/jinja2/runtime.py b/deps/v8/third_party/jinja2/runtime.py index 3ad7968624..985842b284 100644 --- a/deps/v8/third_party/jinja2/runtime.py +++ b/deps/v8/third_party/jinja2/runtime.py @@ -1,32 +1,45 @@ -# -*- coding: utf-8 -*- """The runtime functions and state used by compiled templates.""" +import functools import sys +import typing as t +from collections import abc from itertools import chain -from types import MethodType from markupsafe import escape # noqa: F401 from markupsafe import Markup -from markupsafe import soft_unicode - -from ._compat import abc -from ._compat import imap -from ._compat import implements_iterator -from ._compat import implements_to_string -from ._compat import iteritems -from ._compat import PY2 -from ._compat import string_types -from ._compat import text_type -from ._compat import with_metaclass +from markupsafe import soft_str + +from .async_utils import auto_aiter +from .async_utils import auto_await # noqa: F401 from .exceptions import TemplateNotFound # noqa: F401 from .exceptions import TemplateRuntimeError # noqa: F401 from .exceptions import UndefinedError from .nodes import EvalContext +from .utils import _PassArg from .utils import concat -from .utils import evalcontextfunction from .utils import internalcode from .utils import missing from .utils import Namespace # noqa: F401 from .utils import object_type_repr +from .utils import pass_eval_context + +V = t.TypeVar("V") +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +if t.TYPE_CHECKING: + import logging + import typing_extensions as te + from .environment import Environment + + class LoopRenderFunc(te.Protocol): + def __call__( + self, + reciter: t.Iterable[V], + loop_render_func: "LoopRenderFunc", + depth: int = 0, + ) -> str: + ... + # these variables are exported to the template runtime exported = [ @@ -36,54 +49,54 @@ exported = [ "Markup", "TemplateRuntimeError", "missing", - "concat", "escape", "markup_join", - "unicode_join", - "to_string", + "str_join", "identity", "TemplateNotFound", "Namespace", "Undefined", + "internalcode", +] +async_exported = [ + "AsyncLoopContext", + "auto_aiter", + "auto_await", ] - -#: the name of the function that is used to convert something into -#: a string. We can just use the text type here. -to_string = text_type -def identity(x): +def identity(x: V) -> V: """Returns its argument. Useful for certain things in the environment. """ return x -def markup_join(seq): - """Concatenation that escapes if necessary and converts to unicode.""" +def markup_join(seq: t.Iterable[t.Any]) -> str: + """Concatenation that escapes if necessary and converts to string.""" buf = [] - iterator = imap(soft_unicode, seq) + iterator = map(soft_str, seq) for arg in iterator: buf.append(arg) if hasattr(arg, "__html__"): - return Markup(u"").join(chain(buf, iterator)) + return Markup("").join(chain(buf, iterator)) return concat(buf) -def unicode_join(seq): - """Simple args to unicode conversion and concatenation.""" - return concat(imap(text_type, seq)) +def str_join(seq: t.Iterable[t.Any]) -> str: + """Simple args to string conversion and concatenation.""" + return concat(map(str, seq)) def new_context( - environment, - template_name, - blocks, - vars=None, - shared=None, - globals=None, - locals=None, -): + environment: "Environment", + template_name: t.Optional[str], + blocks: t.Dict[str, t.Callable[["Context"], t.Iterator[str]]], + vars: t.Optional[t.Dict[str, t.Any]] = None, + shared: bool = False, + globals: t.Optional[t.MutableMapping[str, t.Any]] = None, + locals: t.Optional[t.Mapping[str, t.Any]] = None, +) -> "Context": """Internal helper for context creation.""" if vars is None: vars = {} @@ -96,66 +109,38 @@ def new_context( # we don't want to modify the dict passed if shared: parent = dict(parent) - for key, value in iteritems(locals): + for key, value in locals.items(): if value is not missing: parent[key] = value - return environment.context_class(environment, parent, template_name, blocks) + return environment.context_class( + environment, parent, template_name, blocks, globals=globals + ) -class TemplateReference(object): +class TemplateReference: """The `self` in templates.""" - def __init__(self, context): + def __init__(self, context: "Context") -> None: self.__context = context - def __getitem__(self, name): + def __getitem__(self, name: str) -> t.Any: blocks = self.__context.blocks[name] return BlockReference(name, self.__context, blocks, 0) - def __repr__(self): - return "<%s %r>" % (self.__class__.__name__, self.__context.name) - + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.__context.name!r}>" -def _get_func(x): - return getattr(x, "__func__", x) - - -class ContextMeta(type): - def __new__(mcs, name, bases, d): - rv = type.__new__(mcs, name, bases, d) - if bases == (): - return rv - - resolve = _get_func(rv.resolve) - default_resolve = _get_func(Context.resolve) - resolve_or_missing = _get_func(rv.resolve_or_missing) - default_resolve_or_missing = _get_func(Context.resolve_or_missing) - - # If we have a changed resolve but no changed default or missing - # resolve we invert the call logic. - if ( - resolve is not default_resolve - and resolve_or_missing is default_resolve_or_missing - ): - rv._legacy_resolve_mode = True - elif ( - resolve is default_resolve - and resolve_or_missing is default_resolve_or_missing - ): - rv._fast_resolve_mode = True - - return rv +def _dict_method_all(dict_method: F) -> F: + @functools.wraps(dict_method) + def f_all(self: "Context") -> t.Any: + return dict_method(self.get_all()) -def resolve_or_missing(context, key, missing=missing): - if key in context.vars: - return context.vars[key] - if key in context.parent: - return context.parent[key] - return missing + return t.cast(F, f_all) -class Context(with_metaclass(ContextMeta)): +@abc.Mapping.register +class Context: """The template context holds the variables of a template. It stores the values passed to the template and also the names the template exports. Creating instances is neither supported nor useful as it's created @@ -165,7 +150,7 @@ class Context(with_metaclass(ContextMeta)): The context is immutable. Modifications on :attr:`parent` **must not** happen and modifications on :attr:`vars` are allowed from generated template code only. Template filters and global functions marked as - :func:`contextfunction`\\s get the active context passed as first argument + :func:`pass_context` get the active context passed as first argument and are allowed to access the context read-only. The template context supports read only dict operations (`get`, @@ -175,30 +160,30 @@ class Context(with_metaclass(ContextMeta)): :class:`Undefined` object for missing variables. """ - # XXX: we want to eventually make this be a deprecation warning and - # remove it. - _legacy_resolve_mode = False - _fast_resolve_mode = False - - def __init__(self, environment, parent, name, blocks): + def __init__( + self, + environment: "Environment", + parent: t.Dict[str, t.Any], + name: t.Optional[str], + blocks: t.Dict[str, t.Callable[["Context"], t.Iterator[str]]], + globals: t.Optional[t.MutableMapping[str, t.Any]] = None, + ): self.parent = parent - self.vars = {} - self.environment = environment + self.vars: t.Dict[str, t.Any] = {} + self.environment: "Environment" = environment self.eval_ctx = EvalContext(self.environment, name) - self.exported_vars = set() + self.exported_vars: t.Set[str] = set() self.name = name + self.globals_keys = set() if globals is None else set(globals) # create the initial mapping of blocks. Whenever template inheritance # takes place the runtime will update this mapping with the new blocks # from the template. - self.blocks = dict((k, [v]) for k, v in iteritems(blocks)) - - # In case we detect the fast resolve mode we can set up an alias - # here that bypasses the legacy code logic. - if self._fast_resolve_mode: - self.resolve_or_missing = MethodType(resolve_or_missing, self) + self.blocks = {k: [v] for k, v in blocks.items()} - def super(self, name, current): + def super( + self, name: str, current: t.Callable[["Context"], t.Iterator[str]] + ) -> t.Union["BlockReference", "Undefined"]: """Render a parent block.""" try: blocks = self.blocks[name] @@ -206,47 +191,62 @@ class Context(with_metaclass(ContextMeta)): blocks[index] except LookupError: return self.environment.undefined( - "there is no parent block called %r." % name, name="super" + f"there is no parent block called {name!r}.", name="super" ) return BlockReference(name, self, blocks, index) - def get(self, key, default=None): - """Returns an item from the template context, if it doesn't exist - `default` is returned. + def get(self, key: str, default: t.Any = None) -> t.Any: + """Look up a variable by name, or return a default if the key is + not found. + + :param key: The variable name to look up. + :param default: The value to return if the key is not found. """ try: return self[key] except KeyError: return default - def resolve(self, key): - """Looks up a variable like `__getitem__` or `get` but returns an - :class:`Undefined` object with the name of the name looked up. + def resolve(self, key: str) -> t.Union[t.Any, "Undefined"]: + """Look up a variable by name, or return an :class:`Undefined` + object if the key is not found. + + If you need to add custom behavior, override + :meth:`resolve_or_missing`, not this method. The various lookup + functions use that method, not this one. + + :param key: The variable name to look up. """ - if self._legacy_resolve_mode: - rv = resolve_or_missing(self, key) - else: - rv = self.resolve_or_missing(key) + rv = self.resolve_or_missing(key) + if rv is missing: return self.environment.undefined(name=key) + return rv - def resolve_or_missing(self, key): - """Resolves a variable like :meth:`resolve` but returns the - special `missing` value if it cannot be found. + def resolve_or_missing(self, key: str) -> t.Any: + """Look up a variable by name, or return a ``missing`` sentinel + if the key is not found. + + Override this method to add custom lookup behavior. + :meth:`resolve`, :meth:`get`, and :meth:`__getitem__` use this + method. Don't call this method directly. + + :param key: The variable name to look up. """ - if self._legacy_resolve_mode: - rv = self.resolve(key) - if isinstance(rv, Undefined): - rv = missing - return rv - return resolve_or_missing(self, key) - - def get_exported(self): + if key in self.vars: + return self.vars[key] + + if key in self.parent: + return self.parent[key] + + return missing + + def get_exported(self) -> t.Dict[str, t.Any]: """Get a new dict with the exported variables.""" - return dict((k, self.vars[k]) for k in self.exported_vars) + return {k: self.vars[k] for k in self.exported_vars} - def get_all(self): + def get_all(self) -> t.Dict[str, t.Any]: """Return the complete context as dict including the exported variables. For optimizations reasons this might not return an actual copy so be careful with using it. @@ -258,44 +258,51 @@ class Context(with_metaclass(ContextMeta)): return dict(self.parent, **self.vars) @internalcode - def call(__self, __obj, *args, **kwargs): # noqa: B902 + def call( + __self, __obj: t.Callable, *args: t.Any, **kwargs: t.Any # noqa: B902 + ) -> t.Union[t.Any, "Undefined"]: """Call the callable with the arguments and keyword arguments provided but inject the active context or environment as first - argument if the callable is a :func:`contextfunction` or - :func:`environmentfunction`. + argument if the callable has :func:`pass_context` or + :func:`pass_environment`. """ if __debug__: __traceback_hide__ = True # noqa # Allow callable classes to take a context - if hasattr(__obj, "__call__"): # noqa: B004 - fn = __obj.__call__ - for fn_type in ( - "contextfunction", - "evalcontextfunction", - "environmentfunction", - ): - if hasattr(fn, fn_type): - __obj = fn - break - - if callable(__obj): - if getattr(__obj, "contextfunction", False) is True: - args = (__self,) + args - elif getattr(__obj, "evalcontextfunction", False) is True: - args = (__self.eval_ctx,) + args - elif getattr(__obj, "environmentfunction", False) is True: - args = (__self.environment,) + args + if ( + hasattr(__obj, "__call__") # noqa: B004 + and _PassArg.from_obj(__obj.__call__) is not None # type: ignore + ): + __obj = __obj.__call__ # type: ignore + + pass_arg = _PassArg.from_obj(__obj) + + if pass_arg is _PassArg.context: + # the active context should have access to variables set in + # loops and blocks without mutating the context itself + if kwargs.get("_loop_vars"): + __self = __self.derived(kwargs["_loop_vars"]) + if kwargs.get("_block_vars"): + __self = __self.derived(kwargs["_block_vars"]) + args = (__self,) + args + elif pass_arg is _PassArg.eval_context: + args = (__self.eval_ctx,) + args + elif pass_arg is _PassArg.environment: + args = (__self.environment,) + args + + kwargs.pop("_block_vars", None) + kwargs.pop("_loop_vars", None) + try: return __obj(*args, **kwargs) except StopIteration: return __self.environment.undefined( - "value was undefined because " - "a callable raised a " - "StopIteration exception" + "value was undefined because a callable raised a" + " StopIteration exception" ) - def derived(self, locals=None): + def derived(self, locals: t.Optional[t.Dict[str, t.Any]] = None) -> "Context": """Internal helper function to create a derived context. This is used in situations where the system needs a new context in the same template that is independent. @@ -304,78 +311,79 @@ class Context(with_metaclass(ContextMeta)): self.environment, self.name, {}, self.get_all(), True, None, locals ) context.eval_ctx = self.eval_ctx - context.blocks.update((k, list(v)) for k, v in iteritems(self.blocks)) + context.blocks.update((k, list(v)) for k, v in self.blocks.items()) return context - def _all(meth): # noqa: B902 - def proxy(self): - return getattr(self.get_all(), meth)() - - proxy.__doc__ = getattr(dict, meth).__doc__ - proxy.__name__ = meth - return proxy - - keys = _all("keys") - values = _all("values") - items = _all("items") + keys = _dict_method_all(dict.keys) + values = _dict_method_all(dict.values) + items = _dict_method_all(dict.items) - # not available on python 3 - if PY2: - iterkeys = _all("iterkeys") - itervalues = _all("itervalues") - iteritems = _all("iteritems") - del _all - - def __contains__(self, name): + def __contains__(self, name: str) -> bool: return name in self.vars or name in self.parent - def __getitem__(self, key): - """Lookup a variable or raise `KeyError` if the variable is - undefined. + def __getitem__(self, key: str) -> t.Any: + """Look up a variable by name with ``[]`` syntax, or raise a + ``KeyError`` if the key is not found. """ item = self.resolve_or_missing(key) + if item is missing: raise KeyError(key) - return item - - def __repr__(self): - return "<%s %s of %r>" % ( - self.__class__.__name__, - repr(self.get_all()), - self.name, - ) + return item -abc.Mapping.register(Context) + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.get_all()!r} of {self.name!r}>" -class BlockReference(object): +class BlockReference: """One block on a template reference.""" - def __init__(self, name, context, stack, depth): + def __init__( + self, + name: str, + context: "Context", + stack: t.List[t.Callable[["Context"], t.Iterator[str]]], + depth: int, + ) -> None: self.name = name self._context = context self._stack = stack self._depth = depth @property - def super(self): + def super(self) -> t.Union["BlockReference", "Undefined"]: """Super the block.""" if self._depth + 1 >= len(self._stack): return self._context.environment.undefined( - "there is no parent block called %r." % self.name, name="super" + f"there is no parent block called {self.name!r}.", name="super" ) return BlockReference(self.name, self._context, self._stack, self._depth + 1) @internalcode - def __call__(self): + async def _async_call(self) -> str: + rv = concat( + [x async for x in self._stack[self._depth](self._context)] # type: ignore + ) + + if self._context.eval_ctx.autoescape: + return Markup(rv) + + return rv + + @internalcode + def __call__(self) -> str: + if self._context.environment.is_async: + return self._async_call() # type: ignore + rv = concat(self._stack[self._depth](self._context)) + if self._context.eval_ctx.autoescape: - rv = Markup(rv) + return Markup(rv) + return rv -@implements_iterator class LoopContext: """A wrapper iterable for dynamic ``for`` loops, with information about the loop and iteration. @@ -384,13 +392,19 @@ class LoopContext: #: Current iteration of the loop, starting at 0. index0 = -1 - _length = None - _after = missing - _current = missing - _before = missing - _last_changed_value = missing + _length: t.Optional[int] = None + _after: t.Any = missing + _current: t.Any = missing + _before: t.Any = missing + _last_changed_value: t.Any = missing - def __init__(self, iterable, undefined, recurse=None, depth0=0): + def __init__( + self, + iterable: t.Iterable[V], + undefined: t.Type["Undefined"], + recurse: t.Optional["LoopRenderFunc"] = None, + depth0: int = 0, + ) -> None: """ :param iterable: Iterable to wrap. :param undefined: :class:`Undefined` class to use for next and @@ -407,11 +421,11 @@ class LoopContext: self.depth0 = depth0 @staticmethod - def _to_iterator(iterable): + def _to_iterator(iterable: t.Iterable[V]) -> t.Iterator[V]: return iter(iterable) @property - def length(self): + def length(self) -> int: """Length of the iterable. If the iterable is a generator or otherwise does not have a @@ -421,7 +435,7 @@ class LoopContext: return self._length try: - self._length = len(self._iterable) + self._length = len(self._iterable) # type: ignore except TypeError: iterable = list(self._iterator) self._iterator = self._to_iterator(iterable) @@ -429,21 +443,21 @@ class LoopContext: return self._length - def __len__(self): + def __len__(self) -> int: return self.length @property - def depth(self): + def depth(self) -> int: """How many levels deep a recursive loop currently is, starting at 1.""" return self.depth0 + 1 @property - def index(self): + def index(self) -> int: """Current iteration of the loop, starting at 1.""" return self.index0 + 1 @property - def revindex0(self): + def revindex0(self) -> int: """Number of iterations from the end of the loop, ending at 0. Requires calculating :attr:`length`. @@ -451,7 +465,7 @@ class LoopContext: return self.length - self.index @property - def revindex(self): + def revindex(self) -> int: """Number of iterations from the end of the loop, ending at 1. Requires calculating :attr:`length`. @@ -459,11 +473,11 @@ class LoopContext: return self.length - self.index0 @property - def first(self): + def first(self) -> bool: """Whether this is the first iteration of the loop.""" return self.index0 == 0 - def _peek_next(self): + def _peek_next(self) -> t.Any: """Return the next element in the iterable, or :data:`missing` if the iterable is exhausted. Only peeks one item ahead, caching the result in :attr:`_last` for use in subsequent checks. The @@ -476,7 +490,7 @@ class LoopContext: return self._after @property - def last(self): + def last(self) -> bool: """Whether this is the last iteration of the loop. Causes the iterable to advance early. See @@ -486,7 +500,7 @@ class LoopContext: return self._peek_next() is missing @property - def previtem(self): + def previtem(self) -> t.Union[t.Any, "Undefined"]: """The item in the previous iteration. Undefined during the first iteration. """ @@ -496,13 +510,13 @@ class LoopContext: return self._before @property - def nextitem(self): + def nextitem(self) -> t.Union[t.Any, "Undefined"]: """The item in the next iteration. Undefined during the last iteration. Causes the iterable to advance early. See :func:`itertools.groupby` for issues this can cause. - The :func:`groupby` filter avoids that issue. + The :func:`jinja-filters.groupby` filter avoids that issue. """ rv = self._peek_next() @@ -511,7 +525,7 @@ class LoopContext: return rv - def cycle(self, *args): + def cycle(self, *args: V) -> V: """Return a value from the given args, cycling through based on the current :attr:`index0`. @@ -522,7 +536,7 @@ class LoopContext: return args[self.index0 % len(args)] - def changed(self, *value): + def changed(self, *value: t.Any) -> bool: """Return ``True`` if previously called with a different value (including when called for the first time). @@ -534,10 +548,10 @@ class LoopContext: return False - def __iter__(self): + def __iter__(self) -> "LoopContext": return self - def __next__(self): + def __next__(self) -> t.Tuple[t.Any, "LoopContext"]: if self._after is not missing: rv = self._after self._after = missing @@ -550,7 +564,7 @@ class LoopContext: return rv, self @internalcode - def __call__(self, iterable): + def __call__(self, iterable: t.Iterable[V]) -> str: """When iterating over nested data, render the body of the loop recursively with the given inner iterable data. @@ -563,23 +577,94 @@ class LoopContext: return self._recurse(iterable, self._recurse, depth=self.depth) - def __repr__(self): - return "<%s %d/%d>" % (self.__class__.__name__, self.index, self.length) + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.index}/{self.length}>" + + +class AsyncLoopContext(LoopContext): + _iterator: t.AsyncIterator[t.Any] # type: ignore + @staticmethod + def _to_iterator( # type: ignore + iterable: t.Union[t.Iterable[V], t.AsyncIterable[V]] + ) -> t.AsyncIterator[V]: + return auto_aiter(iterable) + + @property + async def length(self) -> int: # type: ignore + if self._length is not None: + return self._length -class Macro(object): + try: + self._length = len(self._iterable) # type: ignore + except TypeError: + iterable = [x async for x in self._iterator] + self._iterator = self._to_iterator(iterable) + self._length = len(iterable) + self.index + (self._after is not missing) + + return self._length + + @property + async def revindex0(self) -> int: # type: ignore + return await self.length - self.index + + @property + async def revindex(self) -> int: # type: ignore + return await self.length - self.index0 + + async def _peek_next(self) -> t.Any: + if self._after is not missing: + return self._after + + try: + self._after = await self._iterator.__anext__() + except StopAsyncIteration: + self._after = missing + + return self._after + + @property + async def last(self) -> bool: # type: ignore + return await self._peek_next() is missing + + @property + async def nextitem(self) -> t.Union[t.Any, "Undefined"]: + rv = await self._peek_next() + + if rv is missing: + return self._undefined("there is no next item") + + return rv + + def __aiter__(self) -> "AsyncLoopContext": + return self + + async def __anext__(self) -> t.Tuple[t.Any, "AsyncLoopContext"]: + if self._after is not missing: + rv = self._after + self._after = missing + else: + rv = await self._iterator.__anext__() + + self.index0 += 1 + self._before = self._current + self._current = rv + return rv, self + + +class Macro: """Wraps a macro function.""" def __init__( self, - environment, - func, - name, - arguments, - catch_kwargs, - catch_varargs, - caller, - default_autoescape=None, + environment: "Environment", + func: t.Callable[..., str], + name: str, + arguments: t.List[str], + catch_kwargs: bool, + catch_varargs: bool, + caller: bool, + default_autoescape: t.Optional[bool] = None, ): self._environment = environment self._func = func @@ -590,13 +675,18 @@ class Macro(object): self.catch_varargs = catch_varargs self.caller = caller self.explicit_caller = "caller" in arguments + if default_autoescape is None: - default_autoescape = environment.autoescape + if callable(environment.autoescape): + default_autoescape = environment.autoescape(None) + else: + default_autoescape = environment.autoescape + self._default_autoescape = default_autoescape @internalcode - @evalcontextfunction - def __call__(self, *args, **kwargs): + @pass_eval_context + def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: # This requires a bit of explanation, In the past we used to # decide largely based on compile-time information if a macro is # safe or unsafe. While there was a volatile mode it was largely @@ -656,40 +746,47 @@ class Macro(object): elif kwargs: if "caller" in kwargs: raise TypeError( - "macro %r was invoked with two values for " - "the special caller argument. This is " - "most likely a bug." % self.name + f"macro {self.name!r} was invoked with two values for the special" + " caller argument. This is most likely a bug." ) raise TypeError( - "macro %r takes no keyword argument %r" - % (self.name, next(iter(kwargs))) + f"macro {self.name!r} takes no keyword argument {next(iter(kwargs))!r}" ) if self.catch_varargs: arguments.append(args[self._argument_count :]) elif len(args) > self._argument_count: raise TypeError( - "macro %r takes not more than %d argument(s)" - % (self.name, len(self.arguments)) + f"macro {self.name!r} takes not more than" + f" {len(self.arguments)} argument(s)" ) return self._invoke(arguments, autoescape) - def _invoke(self, arguments, autoescape): - """This method is being swapped out by the async implementation.""" + async def _async_invoke(self, arguments: t.List[t.Any], autoescape: bool) -> str: + rv = await self._func(*arguments) # type: ignore + + if autoescape: + return Markup(rv) + + return rv # type: ignore + + def _invoke(self, arguments: t.List[t.Any], autoescape: bool) -> str: + if self._environment.is_async: + return self._async_invoke(arguments, autoescape) # type: ignore + rv = self._func(*arguments) + if autoescape: rv = Markup(rv) + return rv - def __repr__(self): - return "<%s %s>" % ( - self.__class__.__name__, - self.name is None and "anonymous" or repr(self.name), - ) + def __repr__(self) -> str: + name = "anonymous" if self.name is None else repr(self.name) + return f"<{type(self).__name__} {name}>" -@implements_to_string -class Undefined(object): +class Undefined: """The default undefined type. This undefined type can be printed and iterated over, but every other access will raise an :exc:`UndefinedError`: @@ -711,14 +808,20 @@ class Undefined(object): "_undefined_exception", ) - def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError): + def __init__( + self, + hint: t.Optional[str] = None, + obj: t.Any = missing, + name: t.Optional[str] = None, + exc: t.Type[TemplateRuntimeError] = UndefinedError, + ) -> None: self._undefined_hint = hint self._undefined_obj = obj self._undefined_name = name self._undefined_exception = exc @property - def _undefined_message(self): + def _undefined_message(self) -> str: """Build a message about the undefined value based on how it was accessed. """ @@ -726,107 +829,78 @@ class Undefined(object): return self._undefined_hint if self._undefined_obj is missing: - return "%r is undefined" % self._undefined_name + return f"{self._undefined_name!r} is undefined" - if not isinstance(self._undefined_name, string_types): - return "%s has no element %r" % ( - object_type_repr(self._undefined_obj), - self._undefined_name, + if not isinstance(self._undefined_name, str): + return ( + f"{object_type_repr(self._undefined_obj)} has no" + f" element {self._undefined_name!r}" ) - return "%r has no attribute %r" % ( - object_type_repr(self._undefined_obj), - self._undefined_name, + return ( + f"{object_type_repr(self._undefined_obj)!r} has no" + f" attribute {self._undefined_name!r}" ) @internalcode - def _fail_with_undefined_error(self, *args, **kwargs): + def _fail_with_undefined_error( + self, *args: t.Any, **kwargs: t.Any + ) -> "te.NoReturn": """Raise an :exc:`UndefinedError` when operations are performed on the undefined value. """ raise self._undefined_exception(self._undefined_message) @internalcode - def __getattr__(self, name): + def __getattr__(self, name: str) -> t.Any: if name[:2] == "__": raise AttributeError(name) + return self._fail_with_undefined_error() - __add__ = ( - __radd__ - ) = ( - __mul__ - ) = ( - __rmul__ - ) = ( - __div__ - ) = ( - __rdiv__ - ) = ( - __truediv__ - ) = ( - __rtruediv__ - ) = ( - __floordiv__ - ) = ( - __rfloordiv__ - ) = ( - __mod__ - ) = ( - __rmod__ - ) = ( - __pos__ - ) = ( - __neg__ - ) = ( - __call__ - ) = ( - __getitem__ - ) = ( - __lt__ - ) = ( - __le__ - ) = ( - __gt__ - ) = ( - __ge__ - ) = ( - __int__ - ) = ( - __float__ - ) = ( - __complex__ - ) = __pow__ = __rpow__ = __sub__ = __rsub__ = _fail_with_undefined_error - - def __eq__(self, other): + __add__ = __radd__ = __sub__ = __rsub__ = _fail_with_undefined_error + __mul__ = __rmul__ = __div__ = __rdiv__ = _fail_with_undefined_error + __truediv__ = __rtruediv__ = _fail_with_undefined_error + __floordiv__ = __rfloordiv__ = _fail_with_undefined_error + __mod__ = __rmod__ = _fail_with_undefined_error + __pos__ = __neg__ = _fail_with_undefined_error + __call__ = __getitem__ = _fail_with_undefined_error + __lt__ = __le__ = __gt__ = __ge__ = _fail_with_undefined_error + __int__ = __float__ = __complex__ = _fail_with_undefined_error + __pow__ = __rpow__ = _fail_with_undefined_error + + def __eq__(self, other: t.Any) -> bool: return type(self) is type(other) - def __ne__(self, other): + def __ne__(self, other: t.Any) -> bool: return not self.__eq__(other) - def __hash__(self): + def __hash__(self) -> int: return id(type(self)) - def __str__(self): - return u"" + def __str__(self) -> str: + return "" - def __len__(self): + def __len__(self) -> int: return 0 - def __iter__(self): - if 0: - yield None + def __iter__(self) -> t.Iterator[t.Any]: + yield from () - def __nonzero__(self): - return False + async def __aiter__(self) -> t.AsyncIterator[t.Any]: + for _ in (): + yield - __bool__ = __nonzero__ + def __bool__(self) -> bool: + return False - def __repr__(self): + def __repr__(self) -> str: return "Undefined" -def make_logging_undefined(logger=None, base=None): +def make_logging_undefined( + logger: t.Optional["logging.Logger"] = None, base: t.Type[Undefined] = Undefined +) -> t.Type[Undefined]: """Given a logger object this returns a new undefined class that will log certain failures. It will log iterations and printing. If no logger is given a default logger is created. @@ -851,70 +925,39 @@ def make_logging_undefined(logger=None, base=None): logger = logging.getLogger(__name__) logger.addHandler(logging.StreamHandler(sys.stderr)) - if base is None: - base = Undefined - - def _log_message(undef): - if undef._undefined_hint is None: - if undef._undefined_obj is missing: - hint = "%s is undefined" % undef._undefined_name - elif not isinstance(undef._undefined_name, string_types): - hint = "%s has no element %s" % ( - object_type_repr(undef._undefined_obj), - undef._undefined_name, - ) - else: - hint = "%s has no attribute %s" % ( - object_type_repr(undef._undefined_obj), - undef._undefined_name, - ) - else: - hint = undef._undefined_hint - logger.warning("Template variable warning: %s", hint) - class LoggingUndefined(base): - def _fail_with_undefined_error(self, *args, **kwargs): + def _log_message(undef: Undefined) -> None: + logger.warning( # type: ignore + "Template variable warning: %s", undef._undefined_message + ) + + class LoggingUndefined(base): # type: ignore + __slots__ = () + + def _fail_with_undefined_error( # type: ignore + self, *args: t.Any, **kwargs: t.Any + ) -> "te.NoReturn": try: - return base._fail_with_undefined_error(self, *args, **kwargs) + super()._fail_with_undefined_error(*args, **kwargs) except self._undefined_exception as e: - logger.error("Template variable error: %s", str(e)) + logger.error("Template variable error: %s", e) # type: ignore raise e - def __str__(self): - rv = base.__str__(self) + def __str__(self) -> str: _log_message(self) - return rv + return super().__str__() # type: ignore - def __iter__(self): - rv = base.__iter__(self) + def __iter__(self) -> t.Iterator[t.Any]: _log_message(self) - return rv - - if PY2: - - def __nonzero__(self): - rv = base.__nonzero__(self) - _log_message(self) - return rv - - def __unicode__(self): - rv = base.__unicode__(self) - _log_message(self) - return rv + return super().__iter__() # type: ignore - else: - - def __bool__(self): - rv = base.__bool__(self) - _log_message(self) - return rv + def __bool__(self) -> bool: + _log_message(self) + return super().__bool__() # type: ignore return LoggingUndefined -# No @implements_to_string decorator here because __str__ -# is not overwritten from Undefined in this class. -# This would cause a recursion error in Python 2. class ChainableUndefined(Undefined): """An undefined that is chainable, where both ``__getattr__`` and ``__getitem__`` return itself rather than raising an @@ -933,16 +976,15 @@ class ChainableUndefined(Undefined): __slots__ = () - def __html__(self): - return self.__str__() + def __html__(self) -> str: + return str(self) - def __getattr__(self, _): + def __getattr__(self, _: str) -> "ChainableUndefined": return self - __getitem__ = __getattr__ + __getitem__ = __getattr__ # type: ignore -@implements_to_string class DebugUndefined(Undefined): """An undefined that returns the debug info when printed. @@ -959,18 +1001,22 @@ class DebugUndefined(Undefined): __slots__ = () - def __str__(self): - if self._undefined_hint is None: - if self._undefined_obj is missing: - return u"{{ %s }}" % self._undefined_name - return "{{ no such element: %s[%r] }}" % ( - object_type_repr(self._undefined_obj), - self._undefined_name, + def __str__(self) -> str: + if self._undefined_hint: + message = f"undefined value printed: {self._undefined_hint}" + + elif self._undefined_obj is missing: + message = self._undefined_name # type: ignore + + else: + message = ( + f"no such element: {object_type_repr(self._undefined_obj)}" + f"[{self._undefined_name!r}]" ) - return u"{{ undefined value printed: %s }}" % self._undefined_hint + + return f"{{{{ {message} }}}}" -@implements_to_string class StrictUndefined(Undefined): """An undefined that barks on print and iteration as well as boolean tests and all kinds of comparisons. In other words: you can do nothing @@ -992,17 +1038,13 @@ class StrictUndefined(Undefined): """ __slots__ = () - __iter__ = ( - __str__ - ) = ( - __len__ - ) = ( - __nonzero__ - ) = __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error + __iter__ = __str__ = __len__ = Undefined._fail_with_undefined_error + __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error + __contains__ = Undefined._fail_with_undefined_error -# remove remaining slots attributes, after the metaclass did the magic they -# are unneeded and irritating as they contain wrong data for the subclasses. +# Remove slots attributes, after the metaclass is applied they are +# unneeded and contain wrong data for subclasses. del ( Undefined.__slots__, ChainableUndefined.__slots__, |