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

uranusjr pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 46c0f85ba6 Add "literal" wrapper to disable field templating (#35017)
46c0f85ba6 is described below

commit 46c0f85ba6dd654501fc429ddd831461ebfefd3c
Author: Michał Sośnicki <[email protected]>
AuthorDate: Fri Nov 17 09:58:49 2023 +0100

    Add "literal" wrapper to disable field templating (#35017)
    
    Co-authored-by: Tzu-ping Chung <[email protected]>
---
 airflow/template/templater.py                   | 19 +++++++++++++++
 airflow/utils/template.py                       | 32 +++++++++++++++++++++++++
 docs/apache-airflow/core-concepts/operators.rst | 29 +++++++++++++++++-----
 tests/models/test_baseoperator.py               |  4 ++++
 tests/template/test_templater.py                | 22 ++++++++++++++++-
 5 files changed, 99 insertions(+), 7 deletions(-)

diff --git a/airflow/template/templater.py b/airflow/template/templater.py
index 9cb6a312ad..f4596f8da1 100644
--- a/airflow/template/templater.py
+++ b/airflow/template/templater.py
@@ -17,6 +17,7 @@
 
 from __future__ import annotations
 
+from dataclasses import dataclass
 from typing import TYPE_CHECKING, Any, Collection, Iterable, Sequence
 
 from airflow.utils.helpers import render_template_as_native, 
render_template_to_string
@@ -29,9 +30,27 @@ if TYPE_CHECKING:
     from sqlalchemy.orm import Session
 
     from airflow import DAG
+    from airflow.models.operator import Operator
     from airflow.utils.context import Context
 
 
+@dataclass(frozen=True)
+class LiteralValue(ResolveMixin):
+    """
+    A wrapper for a value that should be rendered as-is, without applying 
jinja templating to its contents.
+
+    :param value: The value to be rendered without templating
+    """
+
+    value: Any
+
+    def iter_references(self) -> Iterable[tuple[Operator, str]]:
+        return ()
+
+    def resolve(self, context: Context) -> Any:
+        return self.value
+
+
 class Templater(LoggingMixin):
     """
     This renders the template fields of object.
diff --git a/airflow/utils/template.py b/airflow/utils/template.py
new file mode 100644
index 0000000000..ae6042a7e9
--- /dev/null
+++ b/airflow/utils/template.py
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import Any
+
+from airflow.template.templater import LiteralValue
+
+
+def literal(value: Any) -> LiteralValue:
+    """
+    Wrap a value to ensure it is rendered as-is without applying Jinja 
templating to its contents.
+
+    Designed for use in an operator's template field.
+
+    :param value: The value to be rendered without templating
+    """
+    return LiteralValue(value)
diff --git a/docs/apache-airflow/core-concepts/operators.rst 
b/docs/apache-airflow/core-concepts/operators.rst
index ddbcf7da7a..c7193789bc 100644
--- a/docs/apache-airflow/core-concepts/operators.rst
+++ b/docs/apache-airflow/core-concepts/operators.rst
@@ -158,37 +158,54 @@ See the `Jinja documentation 
<https://jinja.palletsprojects.com/en/2.11.x/api/#j
 
 Some operators will also consider strings ending in specific suffixes (defined 
in ``template_ext``) to be references to files when rendering fields. This can 
be useful for loading scripts or queries directly from files rather than 
including them into DAG code.
 
-For example, consider a BashOperator which runs a multi-line bash script, this 
will load the file at ``script.sh`` and use its contents as the value for 
``bash_callable``:
+For example, consider a BashOperator which runs a multi-line bash script, this 
will load the file at ``script.sh`` and use its contents as the value for 
``bash_command``:
 
 .. code-block:: python
 
     run_script = BashOperator(
         task_id="run_script",
-        bash_callable="script.sh",
+        bash_command="script.sh",
     )
 
 By default, paths provided in this way should be provided relative to the 
DAG's folder (as this is the default Jinja template search path), but 
additional paths can be added by setting the ``template_searchpath`` arg on the 
DAG.
 
-In some cases you may want to disable template rendering on specific fields or 
prevent airflow from trying to read template files for a given suffix. Consider 
the following task:
+In some cases, you may want to exclude a string from templating and use it 
directly. Consider the following task:
 
 .. code-block:: python
 
     print_script = BashOperator(
         task_id="print_script",
-        bash_callable="cat script.sh",
+        bash_command="cat script.sh",
     )
 
+This will fail with ``TemplateNotFound: cat script.sh`` since Airflow would 
treat the string as a path to a file, not a command.
+We can prevent airflow from treating this value as a reference to a file by 
wrapping it in :func:`~airflow.util.template.literal`.
+This approach disables the rendering of both macros and files and can be 
applied to selected nested fields while retaining the default templating rules 
for the remainder of the content.
 
-This will fail with ``TemplateNotFound: cat script.sh``, but we can prevent 
airflow from treating this value as a reference to a file by overriding 
``template_ext``:
+.. code-block:: python
+
+    from airflow.utils.template import literal
+
+
+    fixed_print_script = BashOperator(
+        task_id="fixed_print_script",
+        bash_command=literal("cat script.sh"),
+    )
+
+.. versionadded:: 2.8
+    :func:`~airflow.util.template.literal` was added.
+
+Alternatively, if you want to prevent Airflow from treating a value as a 
reference to a file, you can override ``template_ext``:
 
 .. code-block:: python
 
     fixed_print_script = BashOperator(
         task_id="fixed_print_script",
-        bash_callable="cat script.sh",
+        bash_command="cat script.sh",
     )
     fixed_print_script.template_ext = ()
 
+
 .. _concepts:templating-native-objects:
 
 Rendering Fields as Native Python Objects
diff --git a/tests/models/test_baseoperator.py 
b/tests/models/test_baseoperator.py
index 793a31b465..fb46fd39c7 100644
--- a/tests/models/test_baseoperator.py
+++ b/tests/models/test_baseoperator.py
@@ -43,6 +43,7 @@ from airflow.models.dagrun import DagRun
 from airflow.models.taskinstance import TaskInstance
 from airflow.utils.edgemodifier import Label
 from airflow.utils.task_group import TaskGroup
+from airflow.utils.template import literal
 from airflow.utils.trigger_rule import TriggerRule
 from airflow.utils.types import DagRunType
 from airflow.utils.weight_rule import WeightRule
@@ -277,6 +278,9 @@ class TestBaseOperator:
             ),
             # By default, Jinja2 drops one (single) trailing newline
             ("{{ foo }}\n\n", {"foo": "bar"}, "bar\n"),
+            (literal("{{ foo }}"), {"foo": "bar"}, "{{ foo }}"),
+            (literal(["{{ foo }}_1", "{{ foo }}_2"]), {"foo": "bar"}, ["{{ foo 
}}_1", "{{ foo }}_2"]),
+            (literal(("{{ foo }}_1", "{{ foo }}_2")), {"foo": "bar"}, ("{{ foo 
}}_1", "{{ foo }}_2")),
         ],
     )
     def test_render_template(self, content, context, expected_output):
diff --git a/tests/template/test_templater.py b/tests/template/test_templater.py
index 1c3d848de3..c28e1f0503 100644
--- a/tests/template/test_templater.py
+++ b/tests/template/test_templater.py
@@ -20,7 +20,7 @@ from __future__ import annotations
 import jinja2
 
 from airflow.models.dag import DAG
-from airflow.template.templater import Templater
+from airflow.template.templater import LiteralValue, Templater
 from airflow.utils.context import Context
 
 
@@ -60,3 +60,23 @@ class TestTemplater:
         templater.template_ext = [".txt"]
         rendered_content = templater.render_template(templater.message, 
context)
         assert rendered_content == "Hello world"
+
+    def test_not_render_literal_value(self):
+        templater = Templater()
+        templater.template_ext = []
+        context = Context()
+        content = LiteralValue("Hello {{ name }}")
+
+        rendered_content = templater.render_template(content, context)
+
+        assert rendered_content == "Hello {{ name }}"
+
+    def test_not_render_file_literal_value(self):
+        templater = Templater()
+        templater.template_ext = [".txt"]
+        context = Context()
+        content = LiteralValue("template_file.txt")
+
+        rendered_content = templater.render_template(content, context)
+
+        assert rendered_content == "template_file.txt"

Reply via email to