summaryrefslogtreecommitdiffhomepage
path: root/scripts/jinja2/nativetypes.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/jinja2/nativetypes.py')
-rw-r--r--scripts/jinja2/nativetypes.py124
1 files changed, 124 insertions, 0 deletions
diff --git a/scripts/jinja2/nativetypes.py b/scripts/jinja2/nativetypes.py
new file mode 100644
index 0000000..20597d5
--- /dev/null
+++ b/scripts/jinja2/nativetypes.py
@@ -0,0 +1,124 @@
+import typing as t
+from ast import literal_eval
+from ast import parse
+from itertools import chain
+from itertools import islice
+
+from . import nodes
+from .compiler import CodeGenerator
+from .compiler import Frame
+from .compiler import has_safe_repr
+from .environment import Environment
+from .environment import Template
+
+
+def native_concat(values: t.Iterable[t.Any]) -> t.Optional[t.Any]:
+ """Return a native Python type from the list of compiled nodes. If
+ the result is a single node, its value is returned. Otherwise, the
+ nodes are concatenated as strings. If the result can be parsed with
+ :func:`ast.literal_eval`, the parsed value is returned. Otherwise,
+ the string is returned.
+
+ :param values: Iterable of outputs to concatenate.
+ """
+ head = list(islice(values, 2))
+
+ if not head:
+ return None
+
+ if len(head) == 1:
+ raw = head[0]
+ if not isinstance(raw, str):
+ return raw
+ else:
+ raw = "".join([str(v) for v in chain(head, values)])
+
+ try:
+ return literal_eval(
+ # In Python 3.10+ ast.literal_eval removes leading spaces/tabs
+ # from the given string. For backwards compatibility we need to
+ # parse the string ourselves without removing leading spaces/tabs.
+ parse(raw, mode="eval")
+ )
+ except (ValueError, SyntaxError, MemoryError):
+ return raw
+
+
+class NativeCodeGenerator(CodeGenerator):
+ """A code generator which renders Python types by not adding
+ ``str()`` around output nodes.
+ """
+
+ @staticmethod
+ def _default_finalize(value: t.Any) -> t.Any:
+ return value
+
+ def _output_const_repr(self, group: t.Iterable[t.Any]) -> str:
+ return repr("".join([str(v) for v in group]))
+
+ def _output_child_to_const(
+ self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
+ ) -> t.Any:
+ const = node.as_const(frame.eval_ctx)
+
+ if not has_safe_repr(const):
+ raise nodes.Impossible()
+
+ if isinstance(node, nodes.TemplateData):
+ return const
+
+ return finalize.const(const) # type: ignore
+
+ def _output_child_pre(
+ self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
+ ) -> None:
+ if finalize.src is not None:
+ self.write(finalize.src)
+
+ def _output_child_post(
+ self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
+ ) -> None:
+ if finalize.src is not None:
+ self.write(")")
+
+
+class NativeEnvironment(Environment):
+ """An environment that renders templates to native Python types."""
+
+ code_generator_class = NativeCodeGenerator
+
+
+class NativeTemplate(Template):
+ environment_class = NativeEnvironment
+
+ def render(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ """Render the template to produce a native Python type. If the
+ result is a single node, its value is returned. Otherwise, the
+ nodes are concatenated as strings. If the result can be parsed
+ with :func:`ast.literal_eval`, the parsed value is returned.
+ Otherwise, the string is returned.
+ """
+ ctx = self.new_context(dict(*args, **kwargs))
+
+ try:
+ return native_concat(self.root_render_func(ctx)) # type: ignore
+ except Exception:
+ return self.environment.handle_exception()
+
+ async def render_async(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
+ if not self.environment.is_async:
+ raise RuntimeError(
+ "The environment was not created with async mode enabled."
+ )
+
+ ctx = self.new_context(dict(*args, **kwargs))
+
+ try:
+ return native_concat(
+ [n async for n in self.root_render_func(ctx)] # type: ignore
+ )
+ except Exception:
+ return self.environment.handle_exception()
+
+
+NativeEnvironment.template_class = NativeTemplate