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 }}"
