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 c3838a40414 Add prek hook to enforce conf import from compat SDK in
providers (#64564)
c3838a40414 is described below
commit c3838a4041410fe1fa5f69b84d6dc025682741e0
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Wed Apr 1 18:18:18 2026 +0800
Add prek hook to enforce conf import from compat SDK in providers (#64564)
* Add prek hook to enforce conf import from compat SDK in providers
* Add executor file checks for allowed configuration imports
---
providers/.pre-commit-config.yaml | 8 ++
scripts/ci/prek/check_conf_import_in_providers.py | 128 +++++++++++++++++++
.../ci/prek/test_check_conf_import_in_providers.py | 137 +++++++++++++++++++++
3 files changed, 273 insertions(+)
diff --git a/providers/.pre-commit-config.yaml
b/providers/.pre-commit-config.yaml
index 4926188916f..2cf29a4957e 100644
--- a/providers/.pre-commit-config.yaml
+++ b/providers/.pre-commit-config.yaml
@@ -240,6 +240,14 @@ repos:
language: python
files: ^.*/src/airflow/providers/.*version_compat.*\.py$
require_serial: true
+ - id: check-conf-import-in-providers
+ name: Check conf is imported from compat SDK in providers
+ entry: ../scripts/ci/prek/check_conf_import_in_providers.py
+ language: python
+ types: [python]
+ files: ^.*/src/airflow/providers/.*\.py$
+ exclude: ^.*/common/compat/sdk\.py$
+ require_serial: false
- id: provider-version-compat
name: Check for correct version_compat imports in providers
entry: ../scripts/ci/prek/check_provider_version_compat.py
diff --git a/scripts/ci/prek/check_conf_import_in_providers.py
b/scripts/ci/prek/check_conf_import_in_providers.py
new file mode 100755
index 00000000000..b2186377a96
--- /dev/null
+++ b/scripts/ci/prek/check_conf_import_in_providers.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+#
+# 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.
+# /// script
+# requires-python = ">=3.10,<3.11"
+# dependencies = [
+# "rich>=13.6.0",
+# ]
+# ///
+"""
+Check that provider files import ``conf`` only from
``airflow.providers.common.compat.sdk``.
+
+Providers must not import ``conf`` directly from ``airflow.configuration`` or
+``airflow.sdk.configuration``. The compat SDK re-exports ``conf`` and ensures
+the code works across Airflow 2 and 3.
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+from common_prek_utils import console, get_imports_from_file
+
+# Fully-qualified import names that are forbidden (as returned by
get_imports_from_file)
+FORBIDDEN_CONF_IMPORTS = {
+ "airflow.configuration.conf",
+ "airflow.sdk.configuration.conf",
+}
+
+# Executor files run inside Airflow-Core, so they may use
airflow.configuration directly.
+# Only airflow.sdk.configuration remains forbidden for them.
+EXECUTOR_ALLOWED_CONF_IMPORTS = {
+ "airflow.configuration.conf",
+}
+
+ALLOWED_IMPORT = "from airflow.providers.common.compat.sdk import conf"
+
+
+def is_excluded(path: Path) -> bool:
+ """Check if a file is the compat SDK module itself (which must define the
re-export)."""
+ return path.as_posix().endswith("airflow/providers/common/compat/sdk.py")
+
+
+def is_executor_file(path: Path) -> bool:
+ """Check if a file is an executor module (lives under an ``executors/``
directory)."""
+ return "executors" in path.parts
+
+
+def find_forbidden_conf_imports(path: Path) -> list[str]:
+ """Return forbidden conf imports found in *path*."""
+ imports = get_imports_from_file(path, only_top_level=False)
+ forbidden = FORBIDDEN_CONF_IMPORTS
+ if is_executor_file(path):
+ forbidden = forbidden - EXECUTOR_ALLOWED_CONF_IMPORTS
+ return [imp for imp in imports if imp in forbidden]
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Check that provider files import conf only from the
compat SDK."
+ )
+ parser.add_argument("files", nargs="*", type=Path, help="Python source
files to check.")
+ return parser.parse_args()
+
+
+def main() -> int:
+ args = parse_args()
+
+ if not args.files:
+ console.print("[yellow]No files provided.[/]")
+ return 0
+
+ provider_files = [path for path in args.files if not is_excluded(path)]
+
+ if not provider_files:
+ return 0
+
+ errors: list[str] = []
+
+ for path in provider_files:
+ try:
+ forbidden = find_forbidden_conf_imports(path)
+ except Exception as e:
+ console.print(f"[red]Failed to parse {path}: {e}[/]")
+ return 2
+
+ if forbidden:
+ errors.append(f"\n[red]{path}:[/]")
+ for imp in forbidden:
+ errors.append(f" - {imp}")
+
+ if errors:
+ console.print("\n[red]Some provider files import conf from forbidden
modules![/]\n")
+ console.print(
+ "[yellow]Provider files must import conf from the compat SDK:[/]\n"
+ f" {ALLOWED_IMPORT}\n"
+ "\n[yellow]The following imports are forbidden:[/]\n"
+ " - from airflow.configuration import conf\n"
+ " - from airflow.sdk.configuration import conf\n"
+ )
+ console.print("[red]Found forbidden imports in:[/]")
+ for error in errors:
+ console.print(error)
+ return 1
+
+ console.print("[green]All provider files import conf from the correct
module![/]")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/tests/ci/prek/test_check_conf_import_in_providers.py
b/scripts/tests/ci/prek/test_check_conf_import_in_providers.py
new file mode 100644
index 00000000000..647739ac448
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_conf_import_in_providers.py
@@ -0,0 +1,137 @@
+# 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
+
+from pathlib import Path
+
+import pytest
+from check_conf_import_in_providers import find_forbidden_conf_imports,
is_excluded, is_executor_file
+
+
+class TestIsExcluded:
+ @pytest.mark.parametrize(
+ "path, expected",
+ [
+ pytest.param(
+
"providers/common/compat/src/airflow/providers/common/compat/sdk.py",
+ True,
+ id="compat-sdk-module",
+ ),
+ pytest.param(
+ "providers/amazon/src/airflow/providers/amazon/hooks/s3.py",
+ False,
+ id="regular-provider-file",
+ ),
+ ],
+ )
+ def test_is_excluded(self, path: str, expected: bool):
+ assert is_excluded(Path(path)) is expected
+
+
+class TestIsExecutorFile:
+ @pytest.mark.parametrize(
+ "path, expected",
+ [
+ pytest.param(
+
"providers/edge3/src/airflow/providers/edge3/executors/edge_executor.py",
+ True,
+ id="executor-file",
+ ),
+ pytest.param(
+
"providers/celery/src/airflow/providers/celery/executors/celery_executor.py",
+ True,
+ id="celery-executor-file",
+ ),
+ pytest.param(
+ "providers/amazon/src/airflow/providers/amazon/hooks/s3.py",
+ False,
+ id="regular-provider-file",
+ ),
+ ],
+ )
+ def test_is_executor_file(self, path: str, expected: bool):
+ assert is_executor_file(Path(path)) is expected
+
+
+class TestFindForbiddenConfImports:
+ @pytest.mark.parametrize(
+ "code, expected",
+ [
+ pytest.param(
+ "from airflow.configuration import conf\n",
+ ["airflow.configuration.conf"],
+ id="from-airflow-configuration",
+ ),
+ pytest.param(
+ "from airflow.sdk.configuration import conf\n",
+ ["airflow.sdk.configuration.conf"],
+ id="from-airflow-sdk-configuration",
+ ),
+ pytest.param(
+ "from airflow.configuration import conf as global_conf\n",
+ ["airflow.configuration.conf"],
+ id="aliased-conf",
+ ),
+ pytest.param(
+ "def foo():\n from airflow.configuration import conf\n",
+ ["airflow.configuration.conf"],
+ id="inside-function",
+ ),
+ pytest.param(
+ "from __future__ import annotations\n"
+ "from typing import TYPE_CHECKING\n"
+ "if TYPE_CHECKING:\n"
+ " from airflow.sdk.configuration import conf\n",
+ ["airflow.sdk.configuration.conf"],
+ id="inside-type-checking",
+ ),
+ ],
+ )
+ def test_forbidden_imports(self, tmp_path: Path, code: str, expected:
list[str]):
+ f = tmp_path / "example.py"
+ f.write_text(code)
+ assert find_forbidden_conf_imports(f) == expected
+
+ @pytest.mark.parametrize(
+ "code",
+ [
+ pytest.param("from airflow.providers.common.compat.sdk import
conf\n", id="compat-sdk"),
+ pytest.param("from airflow.configuration import has_option\n",
id="other-config-attr"),
+ pytest.param("from airflow.configuration import
AirflowConfigParser\n", id="config-parser"),
+ pytest.param("from airflow.providers.amazon.hooks.s3 import
S3Hook\n", id="provider-import"),
+ pytest.param("import os\nimport sys\n", id="stdlib-only"),
+ pytest.param("x = 1\n", id="no-imports"),
+ ],
+ )
+ def test_allowed_imports(self, tmp_path: Path, code: str):
+ f = tmp_path / "example.py"
+ f.write_text(code)
+ assert find_forbidden_conf_imports(f) == []
+
+ def test_executor_allows_airflow_configuration_conf(self, tmp_path: Path):
+ executor_dir = tmp_path / "executors"
+ executor_dir.mkdir()
+ f = executor_dir / "my_executor.py"
+ f.write_text("from airflow.configuration import conf\n")
+ assert find_forbidden_conf_imports(f) == []
+
+ def test_executor_still_forbids_sdk_configuration_conf(self, tmp_path:
Path):
+ executor_dir = tmp_path / "executors"
+ executor_dir.mkdir()
+ f = executor_dir / "my_executor.py"
+ f.write_text("from airflow.sdk.configuration import conf\n")
+ assert find_forbidden_conf_imports(f) ==
["airflow.sdk.configuration.conf"]