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

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 35dd6f89dc0 [v3-3-test] Enable ruff B023 (function-uses-loop-variable) 
and fix violations (#70640) (#72273)
35dd6f89dc0 is described below

commit 35dd6f89dc0de82ddb37b73f7de8b2942437ce80
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Aug 29 18:48:20 2026 -0500

    [v3-3-test] Enable ruff B023 (function-uses-loop-variable) and fix 
violations (#70640) (#72273)
    
    B023 catches the late-binding closure-over-loop-variable footgun where
    a function defined inside a loop captures the loop variable by
    reference, so every function in the resulting list sees the same
    (final) value — a classic silent-bug source in Python
    
(https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result).
    
    The one user-visible fix is in providers/standard/.../triggers/file.py,
    where the FileTrigger's os.walk lambda was dispatched to a worker
    thread via anyio.to_thread.run_sync while the outer glob iteration
    could advance, potentially walking the wrong path.
    
    The rest are pre-existing latent-bug or false-positive sites in a
    migration script, breeze translation helpers, sphinx extensions, a
    system-test example DAG, the SMTP OAuth2 auth callback, secrets_masker
    subclass compat shim, and two provider unit tests — fixed by binding
    the loop-derived variable as a default argument on the inner function
    or lambda.
    (cherry picked from commit af0a3770a687458a357cd8820cbda07918a90cda)
    
    Co-authored-by: Deepak kumar <[email protected]>
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .../0101_3_2_0_ui_improvements_for_deadlines.py    |  6 ++++++
 .../src/airflow_breeze/commands/ui_commands.py     |  4 ++--
 .../src/sphinx_exts/providers_extensions.py        |  4 ++--
 .../unit/apache/hive/transfers/test_s3_to_hive.py  |  4 +++-
 .../exasol/tests/unit/exasol/hooks/test_sql.py     |  2 +-
 .../cloud/cloud_sql/example_cloud_sql_query.py     |  8 +++++++-
 .../smtp/src/airflow/providers/smtp/hooks/smtp.py  |  2 +-
 .../airflow/providers/standard/triggers/file.py    |  2 +-
 pyproject.toml                                     |  1 +
 .../secrets_masker/secrets_masker.py               | 22 ++++++++++++++--------
 10 files changed, 38 insertions(+), 17 deletions(-)

diff --git 
a/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py
 
b/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py
index 3e1a5c5d54f..a77e780b683 100644
--- 
a/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py
+++ 
b/airflow-core/src/airflow/migrations/versions/0101_3_2_0_ui_improvements_for_deadlines.py
@@ -28,6 +28,12 @@ Revises: e79fc784f145
 Create Date: 2025-10-17 16:04:55.016272
 """
 
+# ruff: noqa: B023
+# _migrate_dag_deadlines is defined inside a for-loop but is invoked and fully 
drained
+# (via `list(_migrate_dag_deadlines(dag_conn))`) within the same iteration, so 
the
+# captured loop values never leak across iterations. This migration shipped in 
3.2.0 —
+# do not rewrite the body for lint hygiene; document the invariant instead.
+
 from __future__ import annotations
 
 import contextlib
diff --git a/dev/breeze/src/airflow_breeze/commands/ui_commands.py 
b/dev/breeze/src/airflow_breeze/commands/ui_commands.py
index 983b31b8160..1f17b4f7d00 100644
--- a/dev/breeze/src/airflow_breeze/commands/ui_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ui_commands.py
@@ -518,7 +518,7 @@ def add_missing_translations(language: str, summary: 
dict[str, LocaleSummary]):
             lang_data = {}  # Start with an empty dict if the file doesn't 
exist
 
         # Helper to recursively add missing keys, including plural forms
-        def add_keys(src, dst, prefix=""):
+        def add_keys(src, dst, prefix="", missing_keys=missing_keys):
             for k, v in src.items():
                 full_key = f"{prefix}.{k}" if prefix else k
                 base = get_plural_base(full_key, suffixes)
@@ -575,7 +575,7 @@ def remove_unused_translations(language: str, summary: 
dict[str, LocaleSummary])
             continue
 
         # Helper to recursively remove unused keys
-        def remove_keys(dst, prefix=""):
+        def remove_keys(dst, prefix="", unused_keys=unused_keys):
             keys_to_remove = []
             for k, v in list(dst.items()):
                 full_key = f"{prefix}.{k}" if prefix else k
diff --git a/devel-common/src/sphinx_exts/providers_extensions.py 
b/devel-common/src/sphinx_exts/providers_extensions.py
index ddd2bbafebb..5aa40ea9b59 100644
--- a/devel-common/src/sphinx_exts/providers_extensions.py
+++ b/devel-common/src/sphinx_exts/providers_extensions.py
@@ -379,8 +379,8 @@ def _get_providers_class_registry(
                         .replace("/", ".")
                     ),
                     class_extras={
-                        "provider_name": lambda **kwargs: 
provider_yaml_content["package-name"],
-                        "provider_version": lambda **kwargs: 
provider_yaml_content["versions"][0],
+                        "provider_name": lambda pyc=provider_yaml_content, 
**kwargs: pyc["package-name"],
+                        "provider_version": lambda pyc=provider_yaml_content, 
**kwargs: pyc["versions"][0],
                         **(class_extras or {}),
                     },
                 )
diff --git 
a/providers/apache/hive/tests/unit/apache/hive/transfers/test_s3_to_hive.py 
b/providers/apache/hive/tests/unit/apache/hive/transfers/test_s3_to_hive.py
index 68ba1f04499..7e5999001bf 100644
--- a/providers/apache/hive/tests/unit/apache/hive/transfers/test_s3_to_hive.py
+++ b/providers/apache/hive/tests/unit/apache/hive/transfers/test_s3_to_hive.py
@@ -223,7 +223,9 @@ class TestS3ToHiveTransfer:
             # against expected file output
 
             
tests_common.test_utils.file_loading.load_file_from_resources.side_effect = (
-                lambda *args, **kwargs: self._load_file_side_effect(args, 
op_fn, ext)
+                lambda *args, _op_fn=op_fn, _ext=ext, **kwargs: 
self._load_file_side_effect(
+                    args, _op_fn, _ext
+                )
             )
             # Execute S3ToHiveTransfer
             s32hive = S3ToHiveOperator(**self.kwargs)
diff --git a/providers/exasol/tests/unit/exasol/hooks/test_sql.py 
b/providers/exasol/tests/unit/exasol/hooks/test_sql.py
index 87013320616..33e3b5a4958 100644
--- a/providers/exasol/tests/unit/exasol/hooks/test_sql.py
+++ b/providers/exasol/tests/unit/exasol/hooks/test_sql.py
@@ -265,7 +265,7 @@ def test_query(
         cursors = []
         for index in range(len(cursor_descriptions)):
             cur = mock.MagicMock(
-                rowcount=lambda: len(cursor_results[index]),
+                rowcount=lambda _idx=index: len(cursor_results[_idx]),
             )
             cur.columns.return_value = get_columns(cursor_descriptions[index])
             cur.fetchall.return_value = cursor_results[index]
diff --git 
a/providers/google/tests/system/google/cloud/cloud_sql/example_cloud_sql_query.py
 
b/providers/google/tests/system/google/cloud/cloud_sql/example_cloud_sql_query.py
index e4cc7b82930..1b2c0f5f2b6 100644
--- 
a/providers/google/tests/system/google/cloud/cloud_sql/example_cloud_sql_query.py
+++ 
b/providers/google/tests/system/google/cloud/cloud_sql/example_cloud_sql_query.py
@@ -475,7 +475,13 @@ with DAG(
             return connection_id
 
         @task_group(group_id=f"create_connections_{database_type}")
-        def create_connections(instance: str, db_type: str, ip_address: str, 
port: str):
+        def create_connections(
+            instance: str,
+            db_type: str,
+            ip_address: str,
+            port: str,
+            database_type: str = database_type,
+        ):
             for conn in CONNECTIONS:
                 conn_id = f"{conn.id}_{database_type}"
                 create_connection(
diff --git a/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py 
b/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
index 991d13c1abc..b3c5bf1baad 100644
--- a/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
+++ b/providers/smtp/src/airflow/providers/smtp/hooks/smtp.py
@@ -140,7 +140,7 @@ class SmtpHook(BaseHook):
                             )
                         self._smtp_client.auth(
                             "XOAUTH2",
-                            lambda _=None: build_xoauth2_string(user_identity, 
self._access_token),
+                            lambda _=None, ui=user_identity: 
build_xoauth2_string(ui, self._access_token),
                         )
                     elif self.smtp_user and self.smtp_password:
                         self._smtp_client.login(self.smtp_user, 
self.smtp_password)
diff --git a/providers/standard/src/airflow/providers/standard/triggers/file.py 
b/providers/standard/src/airflow/providers/standard/triggers/file.py
index 8c7b8894c26..f581ac2a96c 100644
--- a/providers/standard/src/airflow/providers/standard/triggers/file.py
+++ b/providers/standard/src/airflow/providers/standard/triggers/file.py
@@ -84,7 +84,7 @@ class FileTrigger(BaseTrigger):
                     self.log.info("Found File %s last modified: %s", path, 
mod_time)
                     yield TriggerEvent(True)
                     return
-                for _, _, files in await anyio.to_thread.run_sync(lambda: 
list(os.walk(path))):
+                for _, _, files in await anyio.to_thread.run_sync(lambda p: 
list(os.walk(p)), path):
                     if files:
                         yield TriggerEvent(True)
                         return
diff --git a/pyproject.toml b/pyproject.toml
index 32af09e3b87..be55a72d7de 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -663,6 +663,7 @@ extend-select = [
     "B012", # Checks for `break`, `continue`, and `return` statements in 
`finally` blocks
     "B017", # Checks for pytest.raises context managers that catch Exception 
or BaseException.
     "B019", # Use of functools.lru_cache or functools.cache on methods can 
lead to memory leaks
+    "B023", # Function definition does not bind loop variable (late binding of 
closure over loop var)
     "B028", # No explicit stacklevel keyword argument found
     "TRY002", # Prohibit use of `raise Exception`, use specific exceptions 
instead.
     "RET505", # Unnecessary {branch} after return statement
diff --git 
a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py 
b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
index 9157a91a7fb..05c86073492 100644
--- a/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
+++ b/shared/secrets_masker/src/airflow_shared/secrets_masker/secrets_masker.py
@@ -219,14 +219,20 @@ class SecretsMasker(logging.Filter):
                     break
             else:
                 # Block only runs if no break above.
-
-                f = cls._redact
-
-                @functools.wraps(f)
-                def _redact(*args, replacement: str = "***", **kwargs):
-                    return f(*args, **kwargs)
-
-                cls._redact = _redact
+                # Use a factory so the captured subclass method is held in the 
factory's
+                # closure rather than as a default argument on `_redact`. A 
default arg
+                # would leak the name into `_redact`'s keyword signature 
(visible via
+                # `inspect.signature(..., follow_wrapped=False)` and 
callable-visible via
+                # `**kwargs`), which would let a caller silently substitute a 
different
+                # function inside the secrets-masking path.
+                def _make_redact(f):
+                    @functools.wraps(f)
+                    def _redact(*args, replacement: str = "***", **kwargs):
+                        return f(*args, **kwargs)
+
+                    return _redact
+
+                cls._redact = _make_redact(cls._redact)
                 ...
 
     @classmethod

Reply via email to