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(

Reply via email to