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

shahar1 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 3a5fe2f521a Honor string passive extra in FTPHook and FTPSHook (#73694)
3a5fe2f521a is described below

commit 3a5fe2f521abaf3678c40ec92c4cb961c99618eb
Author: Suliman Abdulrazzaq 
<[email protected]>
AuthorDate: Fri Sep 25 11:45:51 2026 +0300

    Honor string passive extra in FTPHook and FTPSHook (#73694)
    
    A passive value given as a string, such as {"passive": "false"}, was
    passed straight to set_pasv(), where any non-empty string is truthy, so
    active mode could not be selected that way. Parse string values before
    using them in both hooks.
---
 .../ftp/src/airflow/providers/ftp/hooks/ftp.py     | 17 ++++++++++--
 providers/ftp/tests/unit/ftp/hooks/test_ftp.py     | 32 ++++++++++++++++++++++
 2 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py 
b/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py
index 04052e6b142..a63a0b4f608 100644
--- a/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py
+++ b/providers/ftp/src/airflow/providers/ftp/hooks/ftp.py
@@ -28,6 +28,19 @@ from airflow.providers.common.compat.sdk import BaseHook
 logger = logging.getLogger(__name__)
 
 
+def _get_passive_mode(extra: dict[str, Any]) -> bool:
+    """
+    Read the ``passive`` connection extra as a boolean.
+
+    Extras parsed from a connection URI (e.g. ``ftp://host?passive=false``) 
are strings,
+    and a non-empty string such as ``"false"`` is truthy.
+    """
+    passive = extra.get("passive", True)
+    if isinstance(passive, str):
+        return passive.strip().lower() not in ("false", "0", "no", "off")
+    return bool(passive)
+
+
 class FTPHook(BaseHook):
     """
     Interact with FTP.
@@ -63,7 +76,7 @@ class FTPHook(BaseHook):
         """Return an FTP connection object."""
         if self.conn is None:
             params = self.get_connection(self.ftp_conn_id)
-            pasv = params.extra_dejson.get("passive", True)
+            pasv = _get_passive_mode(params.extra_dejson)
             encoding = params.extra_dejson.get("encoding")
             self.encoding = encoding
             if encoding:
@@ -301,7 +314,7 @@ class FTPSHook(FTPHook):
 
         if self.conn is None:
             params = self.get_connection(self.ftp_conn_id)
-            pasv = params.extra_dejson.get("passive", True)
+            pasv = _get_passive_mode(params.extra_dejson)
             encoding = params.extra_dejson.get("encoding")
             self.encoding = encoding
 
diff --git a/providers/ftp/tests/unit/ftp/hooks/test_ftp.py 
b/providers/ftp/tests/unit/ftp/hooks/test_ftp.py
index 0cfad429c5f..e6be24698be 100644
--- a/providers/ftp/tests/unit/ftp/hooks/test_ftp.py
+++ b/providers/ftp/tests/unit/ftp/hooks/test_ftp.py
@@ -172,6 +172,18 @@ class TestIntegrationFTPHook:
             Connection(conn_id="ftp_active", conn_type="ftp", 
host="localhost", extra='{"passive": false}')
         )
 
+        create_connection_without_db(
+            Connection(
+                conn_id="ftp_active_str", conn_type="ftp", host="localhost", 
extra='{"passive": "false"}'
+            )
+        )
+
+        create_connection_without_db(
+            Connection(
+                conn_id="ftp_passive_str", conn_type="ftp", host="localhost", 
extra='{"passive": "true"}'
+            )
+        )
+
         create_connection_without_db(
             Connection(
                 conn_id="ftp_custom_port",
@@ -220,6 +232,16 @@ class TestIntegrationFTPHook:
 
         self._test_mode(FTPHook, "ftp_active", False)
 
+    @pytest.mark.parametrize(
+        ("connection_id", "expected_mode"),
+        [("ftp_active_str", False), ("ftp_passive_str", True)],
+    )
+    @mock.patch("ftplib.FTP")
+    def test_ftp_mode_from_string_extra(self, mock_ftp, connection_id, 
expected_mode):
+        from airflow.providers.ftp.hooks.ftp import FTPHook
+
+        self._test_mode(FTPHook, connection_id, expected_mode)
+
     @mock.patch("ftplib.FTP")
     def test_ftp_custom_port(self, mock_ftp):
         from airflow.providers.ftp.hooks.ftp import FTPHook
@@ -252,6 +274,16 @@ class TestIntegrationFTPHook:
 
         self._test_mode(FTPSHook, "ftp_active", False)
 
+    @pytest.mark.parametrize(
+        ("connection_id", "expected_mode"),
+        [("ftp_active_str", False), ("ftp_passive_str", True)],
+    )
+    @mock.patch("ftplib.FTP_TLS")
+    def test_ftps_mode_from_string_extra(self, mock_ftp, connection_id, 
expected_mode):
+        from airflow.providers.ftp.hooks.ftp import FTPSHook
+
+        self._test_mode(FTPSHook, connection_id, expected_mode)
+
     @mock.patch("ftplib.FTP")
     def test_ftp_encoding_extra(self, mock_ftp):
         from airflow.providers.ftp.hooks.ftp import FTPHook

Reply via email to