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

potiuk 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 42bbe8d5916 Fix deferrable autofix inserting a forbidden conf import 
in providers (#72366)
42bbe8d5916 is described below

commit 42bbe8d5916319af6c279a5ec99c449fdf569226
Author: PoAn Yang <[email protected]>
AuthorDate: Tue Sep 8 03:23:36 2026 +0900

    Fix deferrable autofix inserting a forbidden conf import in providers 
(#72366)
    
    Signed-off-by: PoAn Yang <[email protected]>
---
 .../docs/authoring-and-scheduling/deferring.rst    |  3 +-
 scripts/ci/prek/check_deferrable_default.py        |  2 +-
 .../tests/ci/prek/test_check_deferrable_default.py | 96 ++++++++++++++++++++++
 3 files changed, 98 insertions(+), 3 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/deferring.rst 
b/airflow-core/docs/authoring-and-scheduling/deferring.rst
index e3577c437aa..f21a8639d02 100644
--- a/airflow-core/docs/authoring-and-scheduling/deferring.rst
+++ b/airflow-core/docs/authoring-and-scheduling/deferring.rst
@@ -86,8 +86,7 @@ When writing a deferrable operators these are the main points 
to consider:
     from datetime import timedelta
     from typing import Any
 
-    from airflow.configuration import conf
-    from airflow.sdk import BaseSensorOperator, Context
+    from airflow.sdk import BaseSensorOperator, Context, conf
     from airflow.providers.standard.triggers.temporal import TimeDeltaTrigger
 
 
diff --git a/scripts/ci/prek/check_deferrable_default.py 
b/scripts/ci/prek/check_deferrable_default.py
index 8a9252d7644..174d321d60c 100755
--- a/scripts/ci/prek/check_deferrable_default.py
+++ b/scripts/ci/prek/check_deferrable_default.py
@@ -99,7 +99,7 @@ def iter_check_deferrable_default_errors(module_filename: 
str) -> Iterator[str]:
 
 def _fix_invalid_deferrable_default_value(module_filename: str) -> None:
     context = CodemodContext(filename=module_filename)
-    AddImportsVisitor.add_needed_import(context, "airflow.configuration", 
"conf")
+    AddImportsVisitor.add_needed_import(context, 
"airflow.providers.common.compat.sdk", "conf")
     transformer = DefaultDeferrableTransformer()
 
     source_cst_tree = cst.parse_module(open(module_filename).read())
diff --git a/scripts/tests/ci/prek/test_check_deferrable_default.py 
b/scripts/tests/ci/prek/test_check_deferrable_default.py
new file mode 100644
index 00000000000..f8ad3aee31d
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_deferrable_default.py
@@ -0,0 +1,96 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import ast
+
+import pytest
+from check_deferrable_default import _fix_invalid_deferrable_default_value
+
+COMPAT_CONF_MODULE = "airflow.providers.common.compat.sdk"
+
+# ``ast.unparse`` normalises to single quotes, matching 
``_is_valid_deferrable_default``.
+EXPECTED_DEFAULT = "conf.getboolean('operators', 'default_deferrable', 
fallback=False)"
+
+INVALID_OPERATOR = """
+    from __future__ import annotations
+
+
+    class MyOperator:
+        def __init__(self, *, deferrable: bool = False, **kwargs) -> None:
+            self.deferrable = deferrable
+    """
+
+
+def get_conf_import_modules(code: str) -> list[str]:
+    """Return the module each ``conf`` import in *code* comes from."""
+    return [
+        node.module or ""
+        for node in ast.walk(ast.parse(code))
+        if isinstance(node, ast.ImportFrom) and any(alias.name == "conf" for 
alias in node.names)
+    ]
+
+
+def get_deferrable_default(code: str) -> str | None:
+    """Return the unparsed default of the keyword-only ``deferrable`` 
parameter in *code*."""
+    for node in ast.walk(ast.parse(code)):
+        if isinstance(node, ast.FunctionDef) and node.name == "__init__":
+            for argument, default in zip(node.args.kwonlyargs, 
node.args.kw_defaults):
+                if argument.arg == "deferrable" and default is not None:
+                    return ast.unparse(default)
+    return None
+
+
+class TestFixInvalidDeferrableDefaultValue:
+    def test_invalid_default_is_rewritten_with_compat_import(self, 
write_python_file):
+        path = write_python_file(INVALID_OPERATOR)
+
+        _fix_invalid_deferrable_default_value(str(path))
+
+        code = path.read_text()
+        assert get_deferrable_default(code) == EXPECTED_DEFAULT
+        assert get_conf_import_modules(code) == [COMPAT_CONF_MODULE]
+
+    @pytest.mark.parametrize(
+        "existing_import",
+        [
+            pytest.param(f"from {COMPAT_CONF_MODULE} import conf", 
id="conf-only"),
+            pytest.param(
+                f"from {COMPAT_CONF_MODULE} import AirflowException, 
BaseOperator, conf",
+                id="combined-import",
+            ),
+        ],
+    )
+    def test_existing_compat_import_is_not_shadowed(self, write_python_file, 
existing_import: str):
+        path = write_python_file(
+            f"""
+            from __future__ import annotations
+
+            {existing_import}
+
+
+            class MyOperator:
+                def __init__(self, *, deferrable: bool = False, **kwargs) -> 
None:
+                    self.deferrable = deferrable
+            """
+        )
+
+        _fix_invalid_deferrable_default_value(str(path))
+
+        code = path.read_text()
+        assert get_deferrable_default(code) == EXPECTED_DEFAULT
+        assert get_conf_import_modules(code) == [COMPAT_CONF_MODULE]

Reply via email to