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

Lee-W 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 36e7d02a40c Deprecating `poll_interval` in `ExternalTaskSensor` for 
`poke_interval` (#68997)
36e7d02a40c is described below

commit 36e7d02a40c5d1117ebbbd36f2af5737b25ada24
Author: Jake McGrath <[email protected]>
AuthorDate: Fri Jul 31 09:27:59 2026 -0400

    Deprecating `poll_interval` in `ExternalTaskSensor` for `poke_interval` 
(#68997)
    
    Co-authored-by: Phani Kumar <[email protected]>
    Co-authored-by: Kaxil Naik <[email protected]>
    Co-authored-by: Tzu-ping Chung <[email protected]>
---
 .../providers/standard/sensors/external_task.py    |  41 +++++-
 .../standard/sensors/test_external_task_sensor.py  | 145 +++++++++++++++++++++
 2 files changed, 181 insertions(+), 5 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 8270386754f..034e86ca261 100644
--- a/providers/standard/src/airflow/providers/standard/sensors/external_task.py
+++ b/providers/standard/src/airflow/providers/standard/sensors/external_task.py
@@ -23,6 +23,7 @@ import warnings
 from collections.abc import Callable, Collection, Iterable, Sequence
 from typing import TYPE_CHECKING, ClassVar
 
+from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.models.dag import DagModel
 from airflow.providers.common.compat.sdk import (
     AirflowSkipException,
@@ -170,7 +171,8 @@ class ExternalTaskSensor(BaseSensorOperator):
         external_task_id is not None) or check if the DAG to wait for exists 
(when
         external_task_id is None), and immediately cease waiting if the 
external task
         or DAG does not exist (default value: False).
-    :param poll_interval: polling period in seconds to check for the status
+    :param poke_interval: polling period in seconds to check for the status
+    :param poll_interval: (DEPRECATED) use ``poke_interval`` instead
     :param deferrable: Run sensor in deferrable mode
     """
 
@@ -191,10 +193,19 @@ class ExternalTaskSensor(BaseSensorOperator):
         execution_delta: datetime.timedelta | None = None,
         execution_date_fn: Callable | None = None,
         check_existence: bool = False,
-        poll_interval: float = 2.0,
+        poll_interval: float | None = None,
         deferrable: bool = conf.getboolean("operators", "default_deferrable", 
fallback=False),
         **kwargs,
     ):
+        if poll_interval is not None:
+            warnings.warn(
+                "Parameter `poll_interval` is deprecated and will be removed 
in a future release. "
+                "Use `poke_interval` instead.",
+                AirflowProviderDeprecationWarning,
+                stacklevel=2,
+            )
+            kwargs.setdefault("poke_interval", poll_interval)
+
         super().__init__(**kwargs)
 
         self.allowed_states: list[str] = (
@@ -263,9 +274,29 @@ class ExternalTaskSensor(BaseSensorOperator):
         self.check_existence = check_existence
         self._has_checked_existence = False
         self.deferrable = deferrable
-        self.poll_interval = poll_interval
         self.external_dates_filter: str | None = None
 
+    @property
+    def poll_interval(self) -> float:
+        """Deprecated alias for poke_interval."""
+        warnings.warn(
+            "Attribute poll_interval is deprecated and will be removed in a 
future release. "
+            "Use poke_interval instead.",
+            AirflowProviderDeprecationWarning,
+            stacklevel=2,
+        )
+        return self.poke_interval
+
+    @poll_interval.setter
+    def poll_interval(self, value: float) -> None:
+        warnings.warn(
+            "Attribute poll_interval is deprecated and will be removed in a 
future release. "
+            "Use poke_interval instead.",
+            AirflowProviderDeprecationWarning,
+            stacklevel=2,
+        )
+        self.poke_interval = value
+
     def _get_dttm_filter(self, context: Context) -> 
Sequence[datetime.datetime]:
         logical_date = self._get_logical_date(context)
 
@@ -452,7 +483,7 @@ class ExternalTaskSensor(BaseSensorOperator):
                         allowed_states=self.allowed_states,
                         failed_states=self.failed_states,
                         skipped_states=self.skipped_states,
-                        poke_interval=self.poll_interval,
+                        poke_interval=self.poke_interval,
                         soft_fail=self.soft_fail,
                         logical_dates=list(dttm_filter),
                         run_ids=None,
@@ -477,7 +508,7 @@ class ExternalTaskSensor(BaseSensorOperator):
                         allowed_states=self.allowed_states,
                         failed_states=self.failed_states,
                         skipped_states=self.skipped_states,
-                        poke_interval=self.poll_interval,
+                        poke_interval=self.poke_interval,
                         soft_fail=self.soft_fail,
                         execution_dates=list(dttm_filter),
                         logical_dates=None,
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 ef6bd33a764..3bf470ae33b 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
@@ -27,6 +27,7 @@ import pytest
 from sqlalchemy import select
 
 from airflow import settings
+from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.models import DagRun, TaskInstance
 from airflow.models.dag import DAG
 from airflow.models.serialized_dag import SerializedDagModel
@@ -1445,6 +1446,121 @@ class TestExternalTaskSensorV3:
         assert exc.value.trigger.external_task_ids == ["test_task"]
         assert exc.value.trigger.logical_dates == [DEFAULT_DATE]
 
+    def test_poke_interval_set_on_init(self):
+        """Test that poke_interval is set on init and the deprecated 
poll_interval attribute mirrors it."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+            poke_interval=30,
+        )
+
+        assert sensor.poke_interval == 30
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="poll_interval"):
+            assert sensor.poll_interval == 30
+
+    def test_poll_interval_attribute_get_set_deprecated(self):
+        """Reading or writing the poll_interval attribute is deprecated but 
still mirrors poke_interval."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+            poke_interval=30,
+        )
+
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="poll_interval"):
+            sensor.poll_interval = 15
+
+        assert sensor.poke_interval == 15
+
+    def test_poke_interval_default_when_unset(self):
+        """The BaseSensor default of 60.0 must be preserved."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+        )
+
+        assert sensor.poke_interval == 60.0
+
+    def test_poke_interval_accepts_timedelta(self):
+        """poke_interval should accept a timedelta, coerced to seconds, same 
as the base sensor."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+            poke_interval=timedelta(seconds=5),
+        )
+
+        assert sensor.poke_interval == 5.0
+
+    def test_poke_interval_no_warning_when_poll_interval_unset(self, recwarn):
+        """Passing only poke_interval must not emit the poll_interval 
deprecation warning."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+            poke_interval=15,
+        )
+
+        assert sensor.poke_interval == 15
+        assert not any(issubclass(w.category, 
AirflowProviderDeprecationWarning) for w in recwarn.list)
+
+    def test_poll_interval_deprecated_and_sets_poke_interval(self):
+        """poll_interval is deprecated, but its value is still respected as 
poke_interval."""
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="poll_interval"):
+            sensor = ExternalTaskSensor(
+                task_id=TASK_ID,
+                external_task_id=EXTERNAL_TASK_ID,
+                external_dag_id=EXTERNAL_DAG_ID,
+                poll_interval=45,
+            )
+
+        assert sensor.poke_interval == 45
+
+    def test_poll_interval_zero_still_deprecated(self):
+        """Regression test: a falsy poll_interval (0) must still take the 
deprecation path."""
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="poll_interval"):
+            sensor = ExternalTaskSensor(
+                task_id=TASK_ID,
+                external_task_id=EXTERNAL_TASK_ID,
+                external_dag_id=EXTERNAL_DAG_ID,
+                poll_interval=0,
+            )
+
+        assert sensor.poke_interval == 0
+
+    def test_poke_interval_overrides_poll_interval_when_both_set(self):
+        """When both are provided, poke_interval takes precedence over the 
deprecated poll_interval."""
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="poll_interval"):
+            sensor = ExternalTaskSensor(
+                task_id=TASK_ID,
+                external_task_id=EXTERNAL_TASK_ID,
+                external_dag_id=EXTERNAL_DAG_ID,
+                poke_interval=10,
+                poll_interval=99,
+            )
+
+        assert sensor.poke_interval == 10
+
+    @pytest.mark.execution_timeout(10)
+    def test_deferrable_poke_interval_passed_to_trigger(self, dag_maker):
+        """Test that poke_interval is correctly forwarded to WorkflowTrigger 
when deferrable=True."""
+        with dag_maker("test_dag_child"):
+            op = ExternalTaskSensor(
+                task_id="test_external_task_sensor_check",
+                external_dag_id="test_dag_parent",
+                external_task_id="test_task",
+                deferrable=True,
+                poke_interval=30,
+            )
+
+        with pytest.raises(TaskDeferred) as exc:
+            op.execute(context=self.context)
+
+        assert isinstance(exc.value.trigger, WorkflowTrigger)
+        assert exc.value.trigger.poke_interval == 30
+
     @pytest.mark.execution_timeout(10)
     def test_external_task_sensor_only_dag_id(self, dag_maker):
         """Test that the sensor works correctly when only external_dag_id is 
provided."""
@@ -1550,6 +1666,22 @@ class TestExternalTaskAsyncSensor:
 
         assert isinstance(exc.value.trigger, WorkflowTrigger), "Trigger is not 
a WorkflowTrigger"
 
+    def test_deferrable_poke_interval_passed_to_trigger(self):
+        """Test that poke_interval flows through to WorkflowTrigger on both 
AF2 and AF3 paths."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+            deferrable=True,
+            poke_interval=30,
+        )
+
+        with pytest.raises(TaskDeferred) as exc:
+            sensor.execute(context={"execution_date": DEFAULT_DATE, 
"logical_date": DEFAULT_DATE})
+
+        assert isinstance(exc.value.trigger, WorkflowTrigger)
+        assert exc.value.trigger.poke_interval == 30
+
     def test_defer_and_fire_failed_state_trigger(self):
         """Tests that an ExternalTaskNotFoundError is raised in case of error 
event"""
         sensor = ExternalTaskSensor(
@@ -1664,6 +1796,19 @@ class TestExternalTaskAsyncSensor:
 
         assert sensor.external_dates_filter == DEFAULT_DATE.isoformat()
 
+    def test_poke_interval_set_on_init(self):
+        """Test that poke_interval is set on init and the deprecated 
poll_interval attribute mirrors it."""
+        sensor = ExternalTaskSensor(
+            task_id=TASK_ID,
+            external_task_id=EXTERNAL_TASK_ID,
+            external_dag_id=EXTERNAL_DAG_ID,
+            poke_interval=30,
+        )
+
+        assert sensor.poke_interval == 30
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="poll_interval"):
+            assert sensor.poll_interval == 30
+
 
 @pytest.mark.skipif(not AIRFLOW_V_3_0_PLUS, reason="Needs Flask app context 
fixture for AF 2")
 @pytest.mark.parametrize(

Reply via email to