This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch elizabeth/nested-jinja
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 18ebf1e2a8d008e874bb132f4892c4b3257ff32d
Author: Elizabeth Thompson <[email protected]>
AuthorDate: Thu Feb 6 14:28:49 2025 -0800

    add functionality for nested jinja rendering
---
 superset/jinja_context.py                     |  18 ++++-
 tests/integration_tests/test_jinja_context.py | 104 ++++++++++++++++++++++++++
 2 files changed, 119 insertions(+), 3 deletions(-)

diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index b0e29505a0..59ca71f0ea 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -596,12 +596,24 @@ class BaseTemplateProcessor:
         >>> sql = "SELECT '{{ datetime(2017, 1, 1).isoformat() }}'"
         >>> process_template(sql)
         "SELECT '2017-01-01T00:00:00'"
+        This function will attempt to resolve nested Jinja expressions
+        up to 4 iterations.
         """
-        template = self.env.from_string(sql)
+        # Initialize the rendered SQL with the original SQL string.
+        rendered_sql = sql
         kwargs.update(self._context)
-
         context = validate_template_context(self.engine, kwargs)
-        return template.render(context)
+
+        # Render recursively up to 4 levels.
+        for _i in range(4):
+            template = self.env.from_string(rendered_sql)
+            new_rendered_sql = template.render(context)
+            # If rendering no longer changes the SQL, break early.
+            if new_rendered_sql == rendered_sql:
+                break
+            rendered_sql = new_rendered_sql
+
+        return rendered_sql
 
 
 class JinjaTemplateProcessor(BaseTemplateProcessor):
diff --git a/tests/integration_tests/test_jinja_context.py 
b/tests/integration_tests/test_jinja_context.py
index d098e756c4..730986c0c9 100644
--- a/tests/integration_tests/test_jinja_context.py
+++ b/tests/integration_tests/test_jinja_context.py
@@ -226,3 +226,107 @@ def test_custom_template_processors_ignored(app_context: 
AppContext) -> None:
     template = "SELECT '$DATE()'"
     tp = get_template_processor(database=maindb)
     assert tp.process_template(template) == template
+
+
+def test_recursive_template(app_context: AppContext) -> None:
+    """
+    Tests a 4-level deep chain that fully resolves:
+      a -> "{{ b }}"
+      b -> "{{ c }}"
+      c -> "{{ d }}"
+      d -> "final"
+    Expecting: "Result: final"
+    """
+    maindb = superset.utils.database.get_example_database()
+    template = "Result: {{ a }}"
+    tp = get_template_processor(
+        database=maindb,
+        a="{{ b }}",
+        b="{{ c }}",
+        c="{{ d }}",
+        d="final",
+    )
+    assert tp.process_template(template) == "Result: final"
+
+
+def test_circular_template(app_context: AppContext) -> None:
+    """
+    Tests a circular dependency where a variable references itself.
+    In this case, since the rendered value never changes, the process should
+    stop immediately. Expecting the unresolved variable to remain.
+    For example: a -> "{{ a }}" results in "Circular: {{ a }}"
+    """
+    maindb = superset.utils.database.get_example_database()
+    template = "Circular: {{ a }}"
+    tp = get_template_processor(
+        database=maindb,
+        a="{{ a }}",
+    )
+    # The value never changes, so we expect it to remain unresolved.
+    assert tp.process_template(template) == "Circular: {{ a }}"
+
+
+def test_no_template(app_context: AppContext) -> None:
+    """
+    Tests that if there are no Jinja expressions in the template,
+    the output remains unchanged.
+    """
+    maindb = superset.utils.database.get_example_database()
+    template = "No templating here"
+    tp = get_template_processor(database=maindb)
+    assert tp.process_template(template) == "No templating here"
+
+
+def test_single_level_template(app_context: AppContext) -> None:
+    """
+    Tests a simple single-level substitution.
+    For example, replacing {{ name }} with "World".
+    """
+    maindb = superset.utils.database.get_example_database()
+    template = "Hello, {{ name }}!"
+    tp = get_template_processor(database=maindb, name="World")
+    assert tp.process_template(template) == "Hello, World!"
+
+
+def test_three_level_recursive_template(app_context: AppContext) -> None:
+    """
+    Tests a 3-level chain:
+      a -> "{{ b }}"
+      b -> "{{ c }}"
+      c -> "done"
+    Expecting: "Chain: done"
+    """
+    maindb = superset.utils.database.get_example_database()
+    template = "Chain: {{ a }}"
+    tp = get_template_processor(
+        database=maindb,
+        a="{{ b }}",
+        b="{{ c }}",
+        c="done",
+    )
+    assert tp.process_template(template) == "Chain: done"
+
+
+def test_chain_exceeding_max_recursion(app_context: AppContext) -> None:
+    """
+    Tests a 5-level deep chain when the renderer is limited to 4 iterations.
+    The chain is defined as:
+      a -> "{{ b }}"
+      b -> "{{ c }}"
+      c -> "{{ d }}"
+      d -> "{{ e }}"
+      e -> "end"
+    After 4 iterations the chain stops one level short of full resolution,
+    so we expect: "Chain: {{ e }}"
+    """
+    maindb = superset.utils.database.get_example_database()
+    template = "Chain: {{ a }}"
+    tp = get_template_processor(
+        database=maindb,
+        a="{{ b }}",
+        b="{{ c }}",
+        c="{{ d }}",
+        d="{{ e }}",
+        e="end",
+    )
+    assert tp.process_template(template) == "Chain: {{ e }}"

Reply via email to