Repository: pallets/jinja (python)

Issue:
Native types for macros
With [native types](https://jinja.palletsprojects.com/en/3.0.x/nativetypes/) we can template out native Python types, which is useful in for example Ansible.

It seems that macros always generate strings though, even in a native environment. Would it be possible to make macros output native types as well, so they are more useful when building up a native data structure?

For (a trivial) example, I would like this to output the integer 5, not the string "[".
```
env.from_string("{%- macro x() -%}{{- [5,6] -}}{%- endmacro -%}{{- x()[0] -}}").render()
```

Pull request description:
Support native types in macros
<!--
Before opening a PR, open a ticket describing the issue or feature the PR will address. Follow the steps in CONTRIBUTING.rst.

Replace this comment with a description of the change. Describe how it addresses the linked ticket.
-->

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to automatically close an issue.
-->

- fixes #1510

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the docs folder and in code.
- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
- [ ] Run `pre-commit` hooks and fix any issues.
- [ ] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 63df08a1..d641712d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,6 +22,8 @@ Unreleased
         ``resolve``.
     -   ``unicode_urlencode`` is renamed to ``url_quote``.
 
+-   Add support for native types in macros. :issue:`1510`
+
 
 Version 3.0.3
 -------------
diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py
index cc25c3ed..ce6f2c55 100644
--- a/src/jinja2/compiler.py
+++ b/src/jinja2/compiler.py
@@ -724,6 +724,7 @@ class CodeGenerator(NodeVisitor):
         """
         self.writeline("resolve = context.resolve_or_missing")
         self.writeline("undefined = environment.undefined")
+        self.writeline("concat = environment.concat")
         # always use the standard Undefined class for the implicit else of
         # conditional expressions
         self.writeline("cond_expr_undefined = Undefined")
diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py
index a94b5d88..85ac0b38 100644
--- a/src/jinja2/environment.py
+++ b/src/jinja2/environment.py
@@ -281,6 +281,8 @@ class Environment:
     #: :class:`~jinja2.compiler.CodeGenerator` for more information.
     code_generator_class: t.Type["CodeGenerator"] = CodeGenerator
 
+    concat = "".join
+
     #: the context class that is used for templates.  See
     #: :class:`~jinja2.runtime.Context` for more information.
     context_class: t.Type[Context] = Context
@@ -1282,7 +1284,7 @@ class Template:
         ctx = self.new_context(dict(*args, **kwargs))
 
         try:
-            return concat(self.root_render_func(ctx))  # type: ignore
+            return self.environment.concat(self.root_render_func(ctx))  # type: ignore
         except Exception:
             self.environment.handle_exception()
 
@@ -1303,7 +1305,9 @@ class Template:
         ctx = self.new_context(dict(*args, **kwargs))
 
         try:
-            return concat([n async for n in self.root_render_func(ctx)])  # type: ignore
+            return self.environment.concat(  # type: ignore
+                [n async for n in self.root_render_func(ctx)]  # type: ignore
+            )
         except Exception:
             return self.environment.handle_exception()
 
diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py
index 20597d50..ac086103 100644
--- a/src/jinja2/nativetypes.py
+++ b/src/jinja2/nativetypes.py
@@ -3,6 +3,7 @@ from ast import literal_eval
 from ast import parse
 from itertools import chain
 from itertools import islice
+from types import GeneratorType
 
 from . import nodes
 from .compiler import CodeGenerator
@@ -31,7 +32,9 @@ def native_concat(values: t.Iterable[t.Any]) -> t.Optional[t.Any]:
         if not isinstance(raw, str):
             return raw
     else:
-        raw = "".join([str(v) for v in chain(head, values)])
+        if isinstance(values, GeneratorType):
+            values = chain(head, values)
+        raw = "".join([str(v) for v in values])
 
     try:
         return literal_eval(
@@ -86,6 +89,7 @@ class NativeEnvironment(Environment):
     """An environment that renders templates to native Python types."""
 
     code_generator_class = NativeCodeGenerator
+    concat = staticmethod(native_concat)  # type: ignore
 
 
 class NativeTemplate(Template):
@@ -101,7 +105,9 @@ class NativeTemplate(Template):
         ctx = self.new_context(dict(*args, **kwargs))
 
         try:
-            return native_concat(self.root_render_func(ctx))  # type: ignore
+            return self.environment_class.concat(  # type: ignore
+                self.root_render_func(ctx)  # type: ignore
+            )
         except Exception:
             return self.environment.handle_exception()
 
@@ -114,7 +120,7 @@ class NativeTemplate(Template):
         ctx = self.new_context(dict(*args, **kwargs))
 
         try:
-            return native_concat(
+            return self.environment_class.concat(  # type: ignore
                 [n async for n in self.root_render_func(ctx)]  # type: ignore
             )
         except Exception:
diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py
index ab7b042f..985842b2 100644
--- a/src/jinja2/runtime.py
+++ b/src/jinja2/runtime.py
@@ -49,7 +49,6 @@ exported = [
     "Markup",
     "TemplateRuntimeError",
     "missing",
-    "concat",
     "escape",
     "markup_join",
     "str_join",


Test patch:
diff --git a/tests/test_nativetypes.py b/tests/test_nativetypes.py
index 9bae938c..8c852525 100644
--- a/tests/test_nativetypes.py
+++ b/tests/test_nativetypes.py
@@ -153,3 +153,10 @@ def test_leading_spaces(env):
     t = env.from_string(" {{ True }}")
     result = t.render()
     assert result == " True"
+
+
+def test_macro(env):
+    t = env.from_string("{%- macro x() -%}{{- [1,2] -}}{%- endmacro -%}{{- x()[1] -}}")
+    result = t.render()
+    assert result == 2
+    assert isinstance(result, int)