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

gopidesu 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 4761082189e Fix key error in _handle_execution_date_fn for 
ExternalTaskSensor (#53728)
4761082189e is described below

commit 4761082189e4a70de474c634128cd5bfa98ad8ce
Author: GPK <[email protected]>
AuthorDate: Sat Jul 26 13:20:20 2025 +0100

    Fix key error in _handle_execution_date_fn for ExternalTaskSensor (#53728)
    
    * fix(external_task_sensor): fallback to execution_date key for 
execution_date_fn context - Try context['logical_date'], then 
context['execution_date'], then dag_run.run_after - Prevents KeyError when 
users run in environments without 'logical_date' Closes #52236
    
    fix(external_task_sensor): fallback to execution_date key for 
execution_date_fn context
    - Try context['logical_date'], then context['execution_date'], then 
dag_run.run_after
    - Prevents KeyError when users run in environments without 'logical_date'
    Closes #52236
    
    * test: alias stdlib datetime to dt to avoid shadowing timezone datetime
    
    * chore: apply blank-line fix to docstring and add logical_date tests
    
    Signed-off-by: yeswanth-bf <[email protected]>
    
    * Update external_task.py
    
    * Fix key error in _handle_execution_date_fn for ExternalTaskSensor
    
    * Fix tests
    
    ---------
    
    Signed-off-by: yeswanth-bf <[email protected]>
    Co-authored-by: yeswanth-bf <[email protected]>
    Co-authored-by: Yeswanth Surampudi 
<[email protected]>
---
 .../providers/standard/sensors/external_task.py    | 33 +++++++++---
 .../standard/sensors/test_external_task_sensor.py  | 63 +++++++++++++++++++++-
 2 files changed, 87 insertions(+), 9 deletions(-)

diff --git 
a/providers/standard/src/airflow/providers/standard/sensors/external_task.py 
b/providers/standard/src/airflow/providers/standard/sensors/external_task.py
index 7506a88ec0c..ffff3f85517 100644
--- a/providers/standard/src/airflow/providers/standard/sensors/external_task.py
+++ b/providers/standard/src/airflow/providers/standard/sensors/external_task.py
@@ -252,20 +252,15 @@ class ExternalTaskSensor(BaseSensorOperator):
         self.poll_interval = poll_interval
 
     def _get_dttm_filter(self, context):
-        logical_date = context.get("logical_date")
-        if AIRFLOW_V_3_0_PLUS:
-            if logical_date is None:
-                dag_run = context.get("dag_run")
-                if TYPE_CHECKING:
-                    assert dag_run
+        logical_date = self._get_logical_date(context)
 
-                logical_date = dag_run.run_after
         if self.execution_delta:
             dttm = logical_date - self.execution_delta
         elif self.execution_date_fn:
             dttm = self._handle_execution_date_fn(context=context)
         else:
             dttm = logical_date
+
         return dttm if isinstance(dttm, list) else [dttm]
 
     def poke(self, context: Context) -> bool:
@@ -522,6 +517,28 @@ class ExternalTaskSensor(BaseSensorOperator):
             dttm_filter, self.external_task_group_id, self.external_dag_id, 
session
         )
 
+    def _get_logical_date(self, context) -> datetime.datetime:
+        """
+        Handle backwards- and forwards-compatible retrieval of the date.
+
+        to pass as the positional argument to execution_date_fn.
+        """
+        # Airflow 3.x: contexts define "logical_date" (or fall back to 
dag_run.run_after).
+        if AIRFLOW_V_3_0_PLUS:
+            logical_date = context.get("logical_date")
+            dag_run = context.get("dag_run")
+            if not (logical_date or (dag_run and dag_run.run_after)):
+                raise ValueError(
+                    "Either `logical_date` or `dag_run.run_after` must be 
provided in the context"
+                )
+            return logical_date or dag_run.run_after
+
+        # Airflow 2.x and earlier: contexts used "execution_date"
+        execution_date = context.get("execution_date")
+        if not execution_date:
+            raise ValueError("Either `execution_date` must be provided in the 
context`")
+        return execution_date
+
     def _handle_execution_date_fn(self, context) -> Any:
         """
         Handle backward compatibility.
@@ -534,7 +551,7 @@ class ExternalTaskSensor(BaseSensorOperator):
         from airflow.utils.operator_helpers import make_kwargs_callable
 
         # Remove "logical_date" because it is already a mandatory positional 
argument
-        logical_date = context["logical_date"]
+        logical_date = self._get_logical_date(context)
         kwargs = {k: v for k, v in context.items() if k not in 
{"execution_date", "logical_date"}}
         # Add "context" in the kwargs for backward compatibility (because 
context used to be
         # an acceptable argument of execution_date_fn)
diff --git 
a/providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py 
b/providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py
index 978c617b4de..8c2435c16c4 100644
--- 
a/providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py
+++ 
b/providers/standard/tests/unit/standard/sensors/test_external_task_sensor.py
@@ -1024,7 +1024,7 @@ exit 0
 
     @pytest.mark.execution_timeout(10)
     def test_external_task_sensor_deferrable(self, dag_maker):
-        context = {}
+        context = {"execution_date": DEFAULT_DATE}
         with dag_maker() as dag:
             op = ExternalTaskSensor(
                 task_id="test_external_task_sensor_check",
@@ -1047,6 +1047,30 @@ exit 0
         assert exc.value.trigger.external_task_ids == ["test_task"]
         assert exc.value.trigger.execution_dates == [DEFAULT_DATE]
 
+    def test_get_logical_date(self):
+        """For AF 2, we check for execution_date in context."""
+        context = {"execution_date": DEFAULT_DATE}
+        op = ExternalTaskSensor(
+            task_id="test_external_task_sensor_check",
+            external_dag_id="test_dag_parent",
+            external_task_id="test_task",
+        )
+        assert op._get_logical_date(context) == DEFAULT_DATE
+
+    def test_handle_execution_date_fn(self):
+        def func(dt, context):
+            assert context["execution_date"] == dt
+            return dt + timedelta(0)
+
+        op = ExternalTaskSensor(
+            task_id="test_external_task_sensor_check",
+            external_dag_id="test_dag_parent",
+            external_task_id="test_task",
+            execution_date_fn=func,
+        )
+        context = {"execution_date": DEFAULT_DATE}
+        assert op._handle_execution_date_fn(context) == DEFAULT_DATE
+
 
 @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="Different test for AF 2")
 @pytest.mark.usefixtures("testing_dag_bundle")
@@ -1335,6 +1359,43 @@ class TestExternalTaskSensorV3:
             task_group_id="test_group",
         )
 
+    def test_get_logical_date(self):
+        """For AF 3, we check for logical date or dag_run.run_after  in 
context."""
+
+        context = {"logical_date": DEFAULT_DATE}
+        op = ExternalTaskSensor(
+            task_id="test_external_task_sensor_check",
+            external_dag_id="test_dag_parent",
+            external_task_id="test_task",
+        )
+        assert op._get_logical_date(context) == DEFAULT_DATE
+
+    def test_get_logical_date_with_dag_run_after(self):
+        """For AF 3, we check for logical date or dag_run.run_after  in 
context."""
+        op = ExternalTaskSensor(
+            task_id="test_external_task_sensor_check",
+            external_dag_id="test_dag_parent",
+            external_task_id="test_task",
+        )
+        mock_dag_run = mock.MagicMock()
+        mock_dag_run.run_after = DEFAULT_DATE
+        context = {"dag_run": mock_dag_run}
+        assert op._get_logical_date(context) == DEFAULT_DATE
+
+    def test_handle_execution_date_fn(self):
+        def func(dt, context):
+            assert context["logical_date"] == dt
+            return dt + timedelta(0)
+
+        op = ExternalTaskSensor(
+            task_id="test_external_task_sensor_check",
+            external_dag_id="test_dag_parent",
+            external_task_id="test_task",
+            execution_date_fn=func,
+        )
+        context = {"logical_date": DEFAULT_DATE}
+        assert op._handle_execution_date_fn(context) == DEFAULT_DATE
+
 
 class TestExternalTaskAsyncSensor:
     TASK_ID = "external_task_sensor_check"

Reply via email to