This is an automated email from the ASF dual-hosted git repository.
potiuk 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 5efa9a1c8bf Make the HITLOperator summary extension point public
(#70391)
5efa9a1c8bf is described below
commit 5efa9a1c8bf362223143cc2b28353b08816fc61f
Author: Shahar Epstein <[email protected]>
AuthorDate: Sat Aug 1 05:37:15 2026 +0300
Make the HITLOperator summary extension point public (#70391)
#70345 turned hitl_summary into a computed property and moved runtime
and subclass additions into a private _hitl_summary_extra dict. That
left HITLOperator subclasses without a supported way to extend the
summary (the pattern the old attribute explicitly documented) and leaks
the private attribute name into OpenLineage include_full_task_info
events. The property change has not shipped in a provider release yet,
so renaming the dict to a public hitl_summary_extra establishes the
replacement contract while it is still free of any compatibility cost.
---
.../airflow/providers/standard/operators/hitl.py | 24 ++++++++++++----------
.../tests/unit/standard/operators/test_hitl.py | 11 ++++++++++
2 files changed, 24 insertions(+), 11 deletions(-)
diff --git
a/providers/standard/src/airflow/providers/standard/operators/hitl.py
b/providers/standard/src/airflow/providers/standard/operators/hitl.py
index ce4f4d4a878..62628b261fa 100644
--- a/providers/standard/src/airflow/providers/standard/operators/hitl.py
+++ b/providers/standard/src/airflow/providers/standard/operators/hitl.py
@@ -133,7 +133,7 @@ class HITLOperator(BaseOperator):
self.validate_defaults()
# Runtime/subclass additions to the summary; config-derived entries
live in the property.
- self._hitl_summary_extra: dict[str, Any] = {}
+ self.hitl_summary_extra: dict[str, Any] = {}
@property
def hitl_summary(self) -> dict[str, Any]:
@@ -141,7 +141,9 @@ class HITLOperator(BaseOperator):
Summary of the Human-in-the-loop request, for listeners/observability.
A property so the ``subject``/``body`` template fields are read after
rendering, not
- captured as un-rendered Jinja in ``__init__``.
+ captured as un-rendered Jinja in ``__init__``. Each access builds a
fresh snapshot, so
+ mutating the returned dict has no effect — runtime code and subclasses
extend the summary
+ by adding entries to ``hitl_summary_extra`` instead.
"""
return {
"subject": self.subject,
@@ -151,7 +153,7 @@ class HITLOperator(BaseOperator):
"multiple": self.multiple,
"assigned_users": self.assigned_users,
"serialized_params": self.serialized_params or None,
- **self._hitl_summary_extra,
+ **self.hitl_summary_extra,
}
def validate_options(self) -> None:
@@ -221,7 +223,7 @@ class HITLOperator(BaseOperator):
timeout_datetime = None
# Enrich summary with runtime info
- self._hitl_summary_extra["timeout_datetime"] = (
+ self.hitl_summary_extra["timeout_datetime"] = (
timeout_datetime.isoformat() if timeout_datetime else None
)
@@ -259,7 +261,7 @@ class HITLOperator(BaseOperator):
def execute_complete(self, context: Context, event: dict[str, Any]) -> Any:
if "error" in event:
- self._hitl_summary_extra["error_type"] = event["error_type"]
+ self.hitl_summary_extra["error_type"] = event["error_type"]
self.process_trigger_event_error(event)
chosen_options = event["chosen_options"]
@@ -267,7 +269,7 @@ class HITLOperator(BaseOperator):
self.validate_chosen_options(chosen_options)
self.validate_params_input(params_input)
- self._hitl_summary_extra.update(
+ self.hitl_summary_extra.update(
{
"chosen_options": chosen_options,
"params_input": params_input,
@@ -445,14 +447,14 @@ class ApprovalOperator(HITLOperator, SkipMixin):
**kwargs,
)
- self._hitl_summary_extra["ignore_downstream_trigger_rules"] =
self.ignore_downstream_trigger_rules
- self._hitl_summary_extra["fail_on_reject"] = self.fail_on_reject
+ self.hitl_summary_extra["ignore_downstream_trigger_rules"] =
self.ignore_downstream_trigger_rules
+ self.hitl_summary_extra["fail_on_reject"] = self.fail_on_reject
def execute_complete(self, context: Context, event: dict[str, Any]) -> Any:
ret = super().execute_complete(context=context, event=event)
chosen_option = ret["chosen_options"][0]
- self._hitl_summary_extra["approved"] = chosen_option == self.APPROVE
+ self.hitl_summary_extra["approved"] = chosen_option == self.APPROVE
if chosen_option == self.APPROVE:
self.log.info("Approved. Proceeding with downstream tasks...")
return ret
@@ -506,7 +508,7 @@ class HITLBranchOperator(HITLOperator, BranchMixIn):
super().__init__(**kwargs)
self.options_mapping = options_mapping or {}
self.validate_options_mapping()
- self._hitl_summary_extra["options_mapping"] = self.options_mapping
+ self.hitl_summary_extra["options_mapping"] = self.options_mapping
def validate_options_mapping(self) -> None:
"""
@@ -541,7 +543,7 @@ class HITLBranchOperator(HITLOperator, BranchMixIn):
# Map options to task IDs using the mapping, fallback to original
option
chosen_options = [self.options_mapping.get(option, option) for option
in chosen_options]
- self._hitl_summary_extra["branches_to_execute"] = chosen_options
+ self.hitl_summary_extra["branches_to_execute"] = chosen_options
return self.do_branch(context=context,
branches_to_execute=chosen_options)
diff --git a/providers/standard/tests/unit/standard/operators/test_hitl.py
b/providers/standard/tests/unit/standard/operators/test_hitl.py
index e83d3c91446..16220ccc203 100644
--- a/providers/standard/tests/unit/standard/operators/test_hitl.py
+++ b/providers/standard/tests/unit/standard/operators/test_hitl.py
@@ -989,6 +989,17 @@ class TestHITLSummaryForListeners:
assert op.hitl_summary["subject"] == "Review for 2020-01-01"
assert op.hitl_summary["body"] == "Deploy 2020-01-01?"
+ def test_summary_extension_via_hitl_summary_extra(self) -> None:
+ """Subclasses and runtime code extend the summary through the public
hitl_summary_extra dict."""
+ op = HITLOperator(
+ task_id="test",
+ subject="Review",
+ options=["A", "B"],
+ )
+ op.hitl_summary_extra["team"] = "data-eng"
+
+ assert op.hitl_summary["team"] == "data-eng"
+
def test_approval_operator_init_summary(self) -> None:
"""ApprovalOperator hitl_summary includes base + approval-specific
fields."""
op = ApprovalOperator(