From 8d15261504e0ea14589ff23fbdaf92224d6975aa Mon Sep 17 00:00:00 2001 From: test Date: Sat, 18 Apr 2026 14:28:22 +0530 Subject: [PATCH 1/3] SFTP: use compat timezone helpers in sensor and trigger Replace direct airflow.utils.timezone imports with providers compat timezone helpers in SFTPSensor and SFTPTrigger, preserving existing newer_than parsing and UTC comparison behavior. --- .../airflow/providers/sftp/sensors/sftp.py | 15 ++++++++++----- .../airflow/providers/sftp/triggers/sftp.py | 11 ++++++----- .../sftp/tests/unit/sftp/sensors/test_sftp.py | 19 +++++++++++++++++-- .../tests/unit/sftp/triggers/test_sftp.py | 16 ++++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/providers/sftp/src/airflow/providers/sftp/sensors/sftp.py b/providers/sftp/src/airflow/providers/sftp/sensors/sftp.py index 1c21bb0108d75..459963900dcea 100644 --- a/providers/sftp/src/airflow/providers/sftp/sensors/sftp.py +++ b/providers/sftp/src/airflow/providers/sftp/sensors/sftp.py @@ -26,10 +26,15 @@ from paramiko.sftp import SFTP_NO_SUCH_FILE -from airflow.providers.common.compat.sdk import AirflowException, BaseSensorOperator, PokeReturnValue, conf +from airflow.providers.common.compat.sdk import ( + AirflowException, + BaseSensorOperator, + PokeReturnValue, + conf, + timezone, +) from airflow.providers.sftp.hooks.sftp import SFTPHook from airflow.providers.sftp.triggers.sftp import SFTPTrigger -from airflow.utils.timezone import convert_to_utc, parse if TYPE_CHECKING: from airflow.providers.common.compat.sdk import Context @@ -119,9 +124,9 @@ def _get_files(self) -> list[str]: continue if isinstance(self.newer_than, str): - self.newer_than = parse(self.newer_than) - _mod_time = convert_to_utc(datetime.strptime(mod_time, "%Y%m%d%H%M%S")) - _newer_than = convert_to_utc(self.newer_than) + self.newer_than = timezone.parse(self.newer_than) + _mod_time = timezone.convert_to_utc(datetime.strptime(mod_time, "%Y%m%d%H%M%S")) + _newer_than = timezone.convert_to_utc(self.newer_than) if _newer_than <= _mod_time: files_found.append(actual_file_present) self.log.info( diff --git a/providers/sftp/src/airflow/providers/sftp/triggers/sftp.py b/providers/sftp/src/airflow/providers/sftp/triggers/sftp.py index 6ecdbaa158940..a46f29d5a4abe 100644 --- a/providers/sftp/src/airflow/providers/sftp/triggers/sftp.py +++ b/providers/sftp/src/airflow/providers/sftp/triggers/sftp.py @@ -24,10 +24,9 @@ from dateutil.parser import parse as parse_date -from airflow.providers.common.compat.sdk import AirflowException +from airflow.providers.common.compat.sdk import AirflowException, timezone from airflow.providers.sftp.hooks.sftp import SFTPHookAsync from airflow.triggers.base import BaseTrigger, TriggerEvent -from airflow.utils.timezone import convert_to_utc class SFTPTrigger(BaseTrigger): @@ -87,7 +86,7 @@ async def run(self) -> AsyncIterator[TriggerEvent]: if isinstance(self.newer_than, str): self.newer_than = parse_date(self.newer_than) - _newer_than = convert_to_utc(self.newer_than) if self.newer_than else None + _newer_than = timezone.convert_to_utc(self.newer_than) if self.newer_than else None while True: try: if self.file_pattern: @@ -102,7 +101,9 @@ async def run(self) -> AsyncIterator[TriggerEvent]: mod_time = datetime.fromtimestamp(float(file.attrs.mtime)).strftime( "%Y%m%d%H%M%S" ) - mod_time_utc = convert_to_utc(datetime.strptime(mod_time, "%Y%m%d%H%M%S")) + mod_time_utc = timezone.convert_to_utc( + datetime.strptime(mod_time, "%Y%m%d%H%M%S") + ) if _newer_than <= mod_time_utc: files_sensed.append(file.filename) else: @@ -118,7 +119,7 @@ async def run(self) -> AsyncIterator[TriggerEvent]: else: mod_time = await hook.get_mod_time(self.path) if _newer_than: - mod_time_utc = convert_to_utc(datetime.strptime(mod_time, "%Y%m%d%H%M%S")) + mod_time_utc = timezone.convert_to_utc(datetime.strptime(mod_time, "%Y%m%d%H%M%S")) if _newer_than <= mod_time_utc: yield TriggerEvent({"status": "success", "message": f"Sensed file: {self.path}"}) return diff --git a/providers/sftp/tests/unit/sftp/sensors/test_sftp.py b/providers/sftp/tests/unit/sftp/sensors/test_sftp.py index 3e1252129ff78..16a97c6b458ec 100644 --- a/providers/sftp/tests/unit/sftp/sensors/test_sftp.py +++ b/providers/sftp/tests/unit/sftp/sensors/test_sftp.py @@ -17,6 +17,8 @@ # under the License. from __future__ import annotations +import importlib +import warnings from datetime import datetime, timezone as stdlib_timezone from unittest import mock from unittest.mock import Mock, patch @@ -25,15 +27,28 @@ from paramiko.sftp import SFTP_FAILURE from pendulum import datetime as pendulum_datetime, timezone -from airflow.providers.common.compat.sdk import AirflowException +from airflow.providers.common.compat.sdk import AirflowException, PokeReturnValue from airflow.providers.sftp.sensors.sftp import SFTPSensor -from airflow.sensors.base import PokeReturnValue +from airflow.utils.deprecation_tools import DeprecatedImportWarning # Ignore missing args provided by default_args # mypy: disable-error-code="arg-type" class TestSFTPSensor: + def test_no_timezone_deprecated_import_warning_on_module_reload(self): + with warnings.catch_warnings(record=True) as captured_warnings: + warnings.simplefilter("always") + import airflow.providers.sftp.sensors.sftp as sftp_sensor_module + + importlib.reload(sftp_sensor_module) + + assert not any( + issubclass(warning.category, DeprecatedImportWarning) + and "airflow.utils.timezone" in str(warning.message) + for warning in captured_warnings + ) + @patch("airflow.providers.sftp.sensors.sftp.SFTPHook") def test_file_present(self, sftp_hook_mock): sftp_hook_mock.return_value.isfile.return_value = True diff --git a/providers/sftp/tests/unit/sftp/triggers/test_sftp.py b/providers/sftp/tests/unit/sftp/triggers/test_sftp.py index fae994716b5b6..b154e0f17bc5d 100644 --- a/providers/sftp/tests/unit/sftp/triggers/test_sftp.py +++ b/providers/sftp/tests/unit/sftp/triggers/test_sftp.py @@ -18,7 +18,9 @@ import asyncio import datetime +import importlib import time +import warnings from unittest import mock import pytest @@ -27,9 +29,23 @@ from airflow.providers.common.compat.sdk import AirflowException from airflow.providers.sftp.triggers.sftp import SFTPTrigger from airflow.triggers.base import TriggerEvent +from airflow.utils.deprecation_tools import DeprecatedImportWarning class TestSFTPTrigger: + def test_no_timezone_deprecated_import_warning_on_module_reload(self): + with warnings.catch_warnings(record=True) as captured_warnings: + warnings.simplefilter("always") + import airflow.providers.sftp.triggers.sftp as sftp_trigger_module + + importlib.reload(sftp_trigger_module) + + assert not any( + issubclass(warning.category, DeprecatedImportWarning) + and "airflow.utils.timezone" in str(warning.message) + for warning in captured_warnings + ) + def test_sftp_trigger_serialization(self): """ Asserts that the SFTPTrigger correctly serializes its arguments and classpath. From 28769897babbf76421054c16e05bba9e1151dd76 Mon Sep 17 00:00:00 2001 From: test Date: Sat, 18 Apr 2026 14:28:22 +0530 Subject: [PATCH 2/3] SFTP tests: keep warning checks compatible with older Airflow Adjust warning-category imports in SFTP sensor/trigger unit tests so the deprecation-warning assertion works across compatibility test environments. --- providers/sftp/tests/unit/sftp/sensors/test_sftp.py | 6 +++++- providers/sftp/tests/unit/sftp/triggers/test_sftp.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/providers/sftp/tests/unit/sftp/sensors/test_sftp.py b/providers/sftp/tests/unit/sftp/sensors/test_sftp.py index 16a97c6b458ec..6af37a6aab452 100644 --- a/providers/sftp/tests/unit/sftp/sensors/test_sftp.py +++ b/providers/sftp/tests/unit/sftp/sensors/test_sftp.py @@ -29,7 +29,11 @@ from airflow.providers.common.compat.sdk import AirflowException, PokeReturnValue from airflow.providers.sftp.sensors.sftp import SFTPSensor -from airflow.utils.deprecation_tools import DeprecatedImportWarning + +try: + from airflow.utils.deprecation_tools import DeprecatedImportWarning +except ImportError: + DeprecatedImportWarning = DeprecationWarning # Ignore missing args provided by default_args # mypy: disable-error-code="arg-type" diff --git a/providers/sftp/tests/unit/sftp/triggers/test_sftp.py b/providers/sftp/tests/unit/sftp/triggers/test_sftp.py index b154e0f17bc5d..ab0c22dd79c63 100644 --- a/providers/sftp/tests/unit/sftp/triggers/test_sftp.py +++ b/providers/sftp/tests/unit/sftp/triggers/test_sftp.py @@ -29,7 +29,11 @@ from airflow.providers.common.compat.sdk import AirflowException from airflow.providers.sftp.triggers.sftp import SFTPTrigger from airflow.triggers.base import TriggerEvent -from airflow.utils.deprecation_tools import DeprecatedImportWarning + +try: + from airflow.utils.deprecation_tools import DeprecatedImportWarning +except ImportError: + DeprecatedImportWarning = DeprecationWarning class TestSFTPTrigger: From abebce5c467083c8206588d902ef866ae6a2189b Mon Sep 17 00:00:00 2001 From: test Date: Sat, 18 Apr 2026 14:28:22 +0530 Subject: [PATCH 3/3] SFTP tests: make warning fallback mypy-safe Use a typed warning-category alias instead of assigning to an imported type name, resolving mypy failures in provider compatibility checks. --- providers/sftp/tests/unit/sftp/sensors/test_sftp.py | 7 +++++-- providers/sftp/tests/unit/sftp/triggers/test_sftp.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/providers/sftp/tests/unit/sftp/sensors/test_sftp.py b/providers/sftp/tests/unit/sftp/sensors/test_sftp.py index 6af37a6aab452..be81f712b280b 100644 --- a/providers/sftp/tests/unit/sftp/sensors/test_sftp.py +++ b/providers/sftp/tests/unit/sftp/sensors/test_sftp.py @@ -30,10 +30,13 @@ from airflow.providers.common.compat.sdk import AirflowException, PokeReturnValue from airflow.providers.sftp.sensors.sftp import SFTPSensor +WARNING_CATEGORY: type[Warning] try: from airflow.utils.deprecation_tools import DeprecatedImportWarning except ImportError: - DeprecatedImportWarning = DeprecationWarning + WARNING_CATEGORY = DeprecationWarning +else: + WARNING_CATEGORY = DeprecatedImportWarning # Ignore missing args provided by default_args # mypy: disable-error-code="arg-type" @@ -48,7 +51,7 @@ def test_no_timezone_deprecated_import_warning_on_module_reload(self): importlib.reload(sftp_sensor_module) assert not any( - issubclass(warning.category, DeprecatedImportWarning) + issubclass(warning.category, WARNING_CATEGORY) and "airflow.utils.timezone" in str(warning.message) for warning in captured_warnings ) diff --git a/providers/sftp/tests/unit/sftp/triggers/test_sftp.py b/providers/sftp/tests/unit/sftp/triggers/test_sftp.py index ab0c22dd79c63..e6c2502f780d6 100644 --- a/providers/sftp/tests/unit/sftp/triggers/test_sftp.py +++ b/providers/sftp/tests/unit/sftp/triggers/test_sftp.py @@ -30,10 +30,13 @@ from airflow.providers.sftp.triggers.sftp import SFTPTrigger from airflow.triggers.base import TriggerEvent +WARNING_CATEGORY: type[Warning] try: from airflow.utils.deprecation_tools import DeprecatedImportWarning except ImportError: - DeprecatedImportWarning = DeprecationWarning + WARNING_CATEGORY = DeprecationWarning +else: + WARNING_CATEGORY = DeprecatedImportWarning class TestSFTPTrigger: @@ -45,7 +48,7 @@ def test_no_timezone_deprecated_import_warning_on_module_reload(self): importlib.reload(sftp_trigger_module) assert not any( - issubclass(warning.category, DeprecatedImportWarning) + issubclass(warning.category, WARNING_CATEGORY) and "airflow.utils.timezone" in str(warning.message) for warning in captured_warnings )