Skip to content

Commit f089b5b

Browse files
authored
Fix trigger template rendering failure when operator template_fields differ from trigger attributes (#64715)
Fix trigger template rendering failure when operator template_fields differ from trigger attributes (#64715)
1 parent 3ac0d74 commit f089b5b

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

airflow-core/src/airflow/triggers/base.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,26 @@ def task_instance(self, value: TaskInstance | None) -> None:
109109
if self.task_instance:
110110
self.task_id = self.task_instance.task_id
111111
if self.task:
112-
self.template_fields = self.task.template_fields
113112
self.template_ext = self.task.template_ext
113+
# Only keep operator template_fields that are also keys in
114+
# start_trigger_args.trigger_kwargs *and* exist on the trigger.
115+
# Using the full operator template_fields would cause
116+
# AttributeError when the trigger does not have attributes with
117+
# the same names as the operator (e.g. "bash_command").
118+
#
119+
# When start_trigger_args is None (normal defer path), the triggerer
120+
# does not build a template context, so render_template_fields is
121+
# never called and empty template_fields is safe.
122+
start_trigger_args = getattr(self.task, "start_trigger_args", None)
123+
trigger_kwarg_keys = (
124+
set((start_trigger_args.trigger_kwargs or {}).keys()) if start_trigger_args else set()
125+
)
126+
if trigger_kwarg_keys:
127+
self.template_fields = tuple(
128+
f for f in self.task.template_fields if f in trigger_kwarg_keys and hasattr(self, f)
129+
)
130+
else:
131+
self.template_fields = ()
114132

115133
def render_template_fields(
116134
self,
@@ -127,7 +145,8 @@ def render_template_fields(
127145
"""
128146
if not jinja_env:
129147
jinja_env = self.get_template_env()
130-
# We only need to render templated fields if templated fields are part of the start_trigger_args
148+
# self.template_fields is already filtered (in the task_instance setter) to only
149+
# include fields present in start_trigger_args.trigger_kwargs and on this trigger.
131150
self._do_render_template_fields(self, self.template_fields, context, jinja_env, set())
132151

133152
@abc.abstractmethod

airflow-core/tests/unit/triggers/test_base_trigger.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ class DummyOperator(BaseOperator):
2727
template_fields = ("name",)
2828

2929

30+
class OperatorWithExtraTemplateFields(BaseOperator):
31+
"""Operator whose template_fields do NOT all exist on the trigger."""
32+
33+
template_fields = ("bash_command", "env", "name")
34+
35+
def __init__(self, bash_command="", env=None, name="", **kwargs):
36+
super().__init__(**kwargs)
37+
self.bash_command = bash_command
38+
self.env = env
39+
self.name = name
40+
41+
3042
class DummyTrigger(BaseTrigger):
3143
def __init__(self, name: str, **kwargs):
3244
super().__init__(**kwargs)
@@ -67,3 +79,62 @@ def test_render_template_fields(create_task_instance):
6779
trigger.render_template_fields(context={"name": "world"})
6880

6981
assert trigger.name == "Hello world"
82+
83+
84+
@pytest.mark.db_test
85+
def test_render_template_fields_filters_to_trigger_kwargs(create_task_instance):
86+
"""Only fields present in both trigger_kwargs and on the trigger should be rendered.
87+
88+
Operator template_fields like 'bash_command' and 'env' that don't exist on the
89+
trigger must be excluded to avoid AttributeError.
90+
"""
91+
op = OperatorWithExtraTemplateFields(
92+
task_id="extra_fields_task",
93+
bash_command="echo hello",
94+
env={"KEY": "val"},
95+
name="static",
96+
)
97+
ti = create_task_instance(
98+
task=op,
99+
start_from_trigger=True,
100+
start_trigger_args=StartTriggerArgs(
101+
trigger_cls=f"{DummyTrigger.__module__}.{DummyTrigger.__qualname__}",
102+
next_method="resume_method",
103+
trigger_kwargs={"name": "Hello {{ name }}"},
104+
),
105+
)
106+
107+
trigger = DummyTrigger(name="Hello {{ name }}")
108+
trigger.task_instance = ti
109+
110+
# Only 'name' should be in template_fields; 'bash_command' and 'env' are excluded
111+
# because they aren't keys in trigger_kwargs or don't exist on the trigger.
112+
assert trigger.template_fields == ("name",)
113+
114+
# Rendering must not raise AttributeError for missing operator fields
115+
trigger.render_template_fields(context={"name": "world"})
116+
assert trigger.name == "Hello world"
117+
118+
119+
@pytest.mark.db_test
120+
def test_render_template_fields_empty_when_no_trigger_kwargs(create_task_instance):
121+
"""When start_trigger_args has no trigger_kwargs, template_fields should be empty."""
122+
op = DummyOperator(task_id="no_kwargs_task")
123+
ti = create_task_instance(
124+
task=op,
125+
start_from_trigger=True,
126+
start_trigger_args=StartTriggerArgs(
127+
trigger_cls=f"{DummyTrigger.__module__}.{DummyTrigger.__qualname__}",
128+
next_method="resume_method",
129+
trigger_kwargs=None,
130+
),
131+
)
132+
133+
trigger = DummyTrigger(name="Hello {{ name }}")
134+
trigger.task_instance = ti
135+
136+
assert trigger.template_fields == ()
137+
138+
# Rendering with empty template_fields is a no-op
139+
trigger.render_template_fields(context={"name": "world"})
140+
assert trigger.name == "Hello {{ name }}"

0 commit comments

Comments
 (0)