Coverage for /pythoncovmergedfiles/medio/medio/src/airflow/airflow/template/templater.py: 21%
90 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# Licensed to the Apache Software Foundation (ASF) under one
2# or more contributor license agreements. See the NOTICE file
3# distributed with this work for additional information
4# regarding copyright ownership. The ASF licenses this file
5# to you under the Apache License, Version 2.0 (the
6# "License"); you may not use this file except in compliance
7# with the License. You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing,
12# software distributed under the License is distributed on an
13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14# KIND, either express or implied. See the License for the
15# specific language governing permissions and limitations
16# under the License.
18from __future__ import annotations
20from typing import TYPE_CHECKING, Any, Collection, Iterable, Sequence
22from airflow.utils.context import Context
23from airflow.utils.helpers import render_template_as_native, render_template_to_string
24from airflow.utils.log.logging_mixin import LoggingMixin
25from airflow.utils.mixins import ResolveMixin
26from airflow.utils.session import NEW_SESSION, provide_session
28if TYPE_CHECKING:
29 import jinja2
30 from sqlalchemy.orm import Session
32 from airflow import DAG
35class Templater(LoggingMixin):
36 """
37 This renders the template fields of object.
39 :meta private:
40 """
42 # For derived classes to define which fields will get jinjaified.
43 template_fields: Collection[str]
44 # Defines which files extensions to look for in the templated fields.
45 template_ext: Sequence[str]
47 def get_template_env(self, dag: DAG | None = None) -> jinja2.Environment:
48 """Fetch a Jinja template environment from the DAG or instantiate empty environment if no DAG."""
49 # This is imported locally since Jinja2 is heavy and we don't need it
50 # for most of the functionalities. It is imported by get_template_env()
51 # though, so we don't need to put this after the 'if dag' check.
52 from airflow.templates import SandboxedEnvironment
54 if dag:
55 return dag.get_template_env(force_sandboxed=False)
56 return SandboxedEnvironment(cache_size=0)
58 def prepare_template(self) -> None:
59 """Hook triggered after the templated fields get replaced by their content.
61 If you need your object to alter the content of the file before the
62 template is rendered, it should override this method to do so.
63 """
65 def resolve_template_files(self) -> None:
66 """Getting the content of files for template_field / template_ext."""
67 if self.template_ext:
68 for field in self.template_fields:
69 content = getattr(self, field, None)
70 if content is None:
71 continue
72 elif isinstance(content, str) and any(content.endswith(ext) for ext in self.template_ext):
73 env = self.get_template_env()
74 try:
75 setattr(self, field, env.loader.get_source(env, content)[0]) # type: ignore
76 except Exception:
77 self.log.exception("Failed to resolve template field %r", field)
78 elif isinstance(content, list):
79 env = self.get_template_env()
80 for i, item in enumerate(content):
81 if isinstance(item, str) and any(item.endswith(ext) for ext in self.template_ext):
82 try:
83 content[i] = env.loader.get_source(env, item)[0] # type: ignore
84 except Exception:
85 self.log.exception("Failed to get source %s", item)
86 self.prepare_template()
88 @provide_session
89 def _do_render_template_fields(
90 self,
91 parent: Any,
92 template_fields: Iterable[str],
93 context: Context,
94 jinja_env: jinja2.Environment,
95 seen_oids: set[int],
96 *,
97 session: Session = NEW_SESSION,
98 ) -> None:
99 for attr_name in template_fields:
100 value = getattr(parent, attr_name)
101 rendered_content = self.render_template(
102 value,
103 context,
104 jinja_env,
105 seen_oids,
106 )
107 if rendered_content:
108 setattr(parent, attr_name, rendered_content)
110 def _render(self, template, context, dag: DAG | None = None) -> Any:
111 if dag and dag.render_template_as_native_obj:
112 return render_template_as_native(template, context)
113 return render_template_to_string(template, context)
115 def render_template(
116 self,
117 content: Any,
118 context: Context,
119 jinja_env: jinja2.Environment | None = None,
120 seen_oids: set[int] | None = None,
121 ) -> Any:
122 """Render a templated string.
124 If *content* is a collection holding multiple templated strings, strings
125 in the collection will be templated recursively.
127 :param content: Content to template. Only strings can be templated (may
128 be inside a collection).
129 :param context: Dict with values to apply on templated content
130 :param jinja_env: Jinja environment. Can be provided to avoid
131 re-creating Jinja environments during recursion.
132 :param seen_oids: template fields already rendered (to avoid
133 *RecursionError* on circular dependencies)
134 :return: Templated content
135 """
136 # "content" is a bad name, but we're stuck to it being public API.
137 value = content
138 del content
140 if seen_oids is not None:
141 oids = seen_oids
142 else:
143 oids = set()
145 if id(value) in oids:
146 return value
148 if not jinja_env:
149 jinja_env = self.get_template_env()
151 if isinstance(value, str):
152 if any(value.endswith(ext) for ext in self.template_ext): # A filepath.
153 template = jinja_env.get_template(value)
154 else:
155 template = jinja_env.from_string(value)
156 return self._render(template, context)
157 if isinstance(value, ResolveMixin):
158 return value.resolve(context)
160 # Fast path for common built-in collections.
161 if value.__class__ is tuple:
162 return tuple(self.render_template(element, context, jinja_env, oids) for element in value)
163 elif isinstance(value, tuple): # Special case for named tuples.
164 return value.__class__(*(self.render_template(el, context, jinja_env, oids) for el in value))
165 elif isinstance(value, list):
166 return [self.render_template(element, context, jinja_env, oids) for element in value]
167 elif isinstance(value, dict):
168 return {k: self.render_template(v, context, jinja_env, oids) for k, v in value.items()}
169 elif isinstance(value, set):
170 return {self.render_template(element, context, jinja_env, oids) for element in value}
172 # More complex collections.
173 self._render_nested_template_fields(value, context, jinja_env, oids)
174 return value
176 def _render_nested_template_fields(
177 self,
178 value: Any,
179 context: Context,
180 jinja_env: jinja2.Environment,
181 seen_oids: set[int],
182 ) -> None:
183 if id(value) in seen_oids:
184 return
185 seen_oids.add(id(value))
186 try:
187 nested_template_fields = value.template_fields
188 except AttributeError:
189 # content has no inner template fields
190 return
191 self._do_render_template_fields(value, nested_template_fields, context, jinja_env, seen_oids)