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