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

jason810496 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 ff140cd0b4a Support inline ignore marker for check_core_imports_in_sdk 
hook (#65358)
ff140cd0b4a is described below

commit ff140cd0b4a048287ed0c069b8de9659aebf6c34
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Sat May 16 10:58:35 2026 +0800

    Support inline ignore marker for check_core_imports_in_sdk hook (#65358)
    
    * Support # nocheck: core-imports inline marker for 
check_core_imports_in_sdk hook
    
    * Address review comment
    
    * refactor: streamline import checks and update nocheck markers to noqa
    
    * refactor: streamline import checks and update nocheck markers to noqa
    
    * Restrict noqa code parsing to the leading comma-separated list
    
    A comment like `# noqa: F401 - see SDK002 docs` no longer
    accidentally suppresses SDK002 — codes mentioned in the trailing
    explanation are ignored.
    
    * Add positive test cases for noqa codes with trailing explanation
    
    Cover `# noqa: F401, SDK002 - needed for compat` (suppresses SDK002
    because it's part of the leading code list) and the symmetric case
    for the SDK001 hook.
    
    * Tighten noqa code parsing and detect bare-module attribute imports
    
    - Anchor `_NOQA_CODE_RE` with a word boundary so prefixes like
      `SDK002x` no longer suppress `SDK002`.
    - Extend the ImportFrom check to also test `<module>.<name>` so that
      `from airflow import settings` is reported as a core import. Mixed
      imports like `from airflow import sdk, settings` report only the
      offending names.
    
    * Print noqa suppression hint when an import violation is reported
    
    After listing offending imports, ``report_import_violations`` now
    echoes the ``# noqa: <code>`` escape hatch so users know the exact
    marker to add for an intentional import without having to dig into
    the hook source.
    
    * fixup: Add SDK002 ignore in task-sdk
    
    * Rename check_sdk_imports to check_sdk_imports_in_core
    
    Mirrors the naming of the sibling check_core_imports_in_sdk hook so the
    direction of each guard (what's checked in where) is obvious from the
    filename.
---
 airflow-core/.pre-commit-config.yaml               |   2 +-
 scripts/ci/prek/check_core_imports_in_sdk.py       |  62 ++----
 scripts/ci/prek/check_sdk_imports.py               |  88 ---------
 scripts/ci/prek/check_sdk_imports_in_core.py       |  64 ++++++
 scripts/ci/prek/common_prek_utils.py               | 129 +++++++++++-
 .../ci/prek/test_check_core_imports_in_sdk.py      | 220 +++++++++++++++++++++
 ...mports.py => test_check_sdk_imports_in_core.py} |  42 +++-
 task-sdk/src/airflow/sdk/execution_time/context.py |   2 +-
 task-sdk/src/airflow/sdk/plugins_manager.py        |   2 +-
 9 files changed, 470 insertions(+), 141 deletions(-)

diff --git a/airflow-core/.pre-commit-config.yaml 
b/airflow-core/.pre-commit-config.yaml
index 838747ab242..aea65d35b0e 100644
--- a/airflow-core/.pre-commit-config.yaml
+++ b/airflow-core/.pre-commit-config.yaml
@@ -299,7 +299,7 @@ repos:
           ^tests/unit/models/test_variable\.py$
       - id: check-sdk-imports
         name: Check for SDK imports in core files
-        entry: ../scripts/ci/prek/check_sdk_imports.py
+        entry: ../scripts/ci/prek/check_sdk_imports_in_core.py
         language: python
         types: [python]
         files: ^src/airflow/
diff --git a/scripts/ci/prek/check_core_imports_in_sdk.py 
b/scripts/ci/prek/check_core_imports_in_sdk.py
index 700d81f5ea1..abdb194cd75 100755
--- a/scripts/ci/prek/check_core_imports_in_sdk.py
+++ b/scripts/ci/prek/check_core_imports_in_sdk.py
@@ -25,44 +25,24 @@
 from __future__ import annotations
 
 import argparse
-import ast
 import sys
 from pathlib import Path
 
-from common_prek_utils import console
+from common_prek_utils import find_import_violations, report_import_violations
+
+NOCHECK_CODE = "SDK002"
 
 
 def check_file_for_core_imports(file_path: Path) -> list[tuple[int, str]]:
     """Check file for airflow-core imports (anything except airflow.sdk). 
Returns list of (line_num, import_statement)."""
-    try:
-        source = file_path.read_text(encoding="utf-8")
-        tree = ast.parse(source, filename=str(file_path))
-    except (OSError, UnicodeDecodeError, SyntaxError):
-        return []
-
-    mismatches = []
-
-    for node in ast.walk(tree):
-        # for `from airflow.x import y` statements
-        if isinstance(node, ast.ImportFrom):
-            if (
-                node.module
-                and node.module.startswith("airflow.")
-                and not node.module.startswith("airflow.sdk")
-            ):
-                import_names = ", ".join(alias.name for alias in node.names)
-                statement = f"from {node.module} import {import_names}"
-                mismatches.append((node.lineno, statement))
-        # for `import airflow.x` statements
-        elif isinstance(node, ast.Import):
-            for alias in node.names:
-                if alias.name.startswith("airflow.") and not 
alias.name.startswith("airflow.sdk"):
-                    statement = f"import {alias.name}"
-                    if alias.asname:
-                        statement += f" as {alias.asname}"
-                    mismatches.append((node.lineno, statement))
-
-    return mismatches
+    return find_import_violations(
+        file_path,
+        is_violating_module=lambda module: (
+            module.startswith("airflow.") and not 
module.startswith("airflow.sdk")
+        ),
+        nocheck_code=NOCHECK_CODE,
+        check_plain_imports=True,
+    )
 
 
 def main():
@@ -73,20 +53,12 @@ def main():
     if not args.files:
         return
 
-    total_violations = 0
-
-    for file_path in [Path(f) for f in args.files]:
-        mismatches = check_file_for_core_imports(file_path)
-        if mismatches:
-            console.print(f"[red]{file_path}[/red]:")
-            for line_num, statement in mismatches:
-                console.print(f"  [yellow]Line {line_num}[/yellow]: 
{statement}")
-            total_violations += len(mismatches)
-
-    if total_violations:
-        console.print()
-        console.print(f"[red]Found {total_violations} core import(s) in 
task-sdk files[/red]")
-        sys.exit(1)
+    report_import_violations(
+        args.files,
+        check_func=check_file_for_core_imports,
+        violation_label="core import(s) in task-sdk files",
+        nocheck_code=NOCHECK_CODE,
+    )
 
 
 if __name__ == "__main__":
diff --git a/scripts/ci/prek/check_sdk_imports.py 
b/scripts/ci/prek/check_sdk_imports.py
deleted file mode 100755
index b35ebc9db8f..00000000000
--- a/scripts/ci/prek/check_sdk_imports.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/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",
-# ]
-# ///
-from __future__ import annotations
-
-import argparse
-import ast
-import sys
-from pathlib import Path
-
-from common_prek_utils import console, has_nocheck_marker
-
-NOCHECK_MARKER = "# noqa: SDK001"
-
-
-def check_file_for_sdk_imports(file_path: Path) -> list[tuple[int, str]]:
-    """Check file for airflow.sdk imports. Returns list of (line_num, 
import_statement)."""
-    try:
-        with open(file_path, encoding="utf-8") as f:
-            source = f.read()
-            tree = ast.parse(source, filename=str(file_path))
-    except (OSError, UnicodeDecodeError, SyntaxError):
-        return []
-
-    source_lines = source.splitlines()
-    mismatches = []
-
-    for node in ast.walk(tree):
-        if isinstance(node, ast.ImportFrom):
-            if node.module and ("airflow.sdk" in node.module):
-                if has_nocheck_marker(source_lines, node, NOCHECK_MARKER):
-                    continue
-                import_names = ", ".join(alias.name for alias in node.names)
-                statement = f"from {node.module} import {import_names}"
-                mismatches.append((node.lineno, statement))
-
-    return mismatches
-
-
-def main():
-    parser = argparse.ArgumentParser(description="Check for SDK imports in 
airflow-core files")
-    parser.add_argument("files", nargs="*", help="Files to check")
-    args = parser.parse_args()
-
-    if not args.files:
-        return
-
-    files_to_check = [Path(f) for f in args.files if f.endswith(".py")]
-    total_violations = 0
-
-    for file_path in files_to_check:
-        mismatches = check_file_for_sdk_imports(file_path)
-        if mismatches:
-            console.print(f"[red]{file_path}[/red]:")
-            for line_num, statement in mismatches:
-                console.print(f"  [yellow]Line {line_num}[/yellow]: 
{statement}")
-            total_violations += len(mismatches)
-
-    if total_violations:
-        console.print()
-        console.print(f"[red]Found {total_violations} SDK import(s) in core 
files[/red]")
-        sys.exit(1)
-
-
-if __name__ == "__main__":
-    main()
-    sys.exit(0)
diff --git a/scripts/ci/prek/check_sdk_imports_in_core.py 
b/scripts/ci/prek/check_sdk_imports_in_core.py
new file mode 100755
index 00000000000..03192728930
--- /dev/null
+++ b/scripts/ci/prek/check_sdk_imports_in_core.py
@@ -0,0 +1,64 @@
+#!/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",
+# ]
+# ///
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+from common_prek_utils import find_import_violations, report_import_violations
+
+NOCHECK_CODE = "SDK001"
+
+
+def check_file_for_sdk_imports(file_path: Path) -> list[tuple[int, str]]:
+    """Check file for airflow.sdk imports. Returns list of (line_num, 
import_statement)."""
+    return find_import_violations(
+        file_path,
+        is_violating_module=lambda module: "airflow.sdk" in module,
+        nocheck_code=NOCHECK_CODE,
+    )
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Check for SDK imports in 
airflow-core files")
+    parser.add_argument("files", nargs="*", help="Files to check")
+    args = parser.parse_args()
+
+    if not args.files:
+        return
+
+    report_import_violations(
+        args.files,
+        check_func=check_file_for_sdk_imports,
+        violation_label="SDK import(s) in core files",
+        nocheck_code=NOCHECK_CODE,
+        only_python_files=True,
+    )
+
+
+if __name__ == "__main__":
+    main()
+    sys.exit(0)
diff --git a/scripts/ci/prek/common_prek_utils.py 
b/scripts/ci/prek/common_prek_utils.py
index f59828bd297..7bcdfa3ab27 100644
--- a/scripts/ci/prek/common_prek_utils.py
+++ b/scripts/ci/prek/common_prek_utils.py
@@ -26,7 +26,7 @@ import subprocess
 import sys
 import textwrap
 import time
-from collections.abc import Generator
+from collections.abc import Callable, Generator
 from contextlib import contextmanager
 from pathlib import Path
 from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
@@ -526,16 +526,137 @@ def get_all_provider_info_dicts() -> dict[str, dict]:
     return providers
 
 
-def has_nocheck_marker(source_lines: list[str], node: ast.ImportFrom, marker: 
str) -> bool:
-    """Check if the import statement has the given nocheck marker comment on 
any of its lines."""
+_NOQA_RE = re.compile(r"#\s*noqa\s*:\s*([^\n]*)", re.IGNORECASE)
+_NOQA_CODE_RE = re.compile(r"[A-Z]+\d+\b")
+
+
+def _parse_noqa_codes(line: str) -> set[str]:
+    """Extract codes from the leading comma-separated list in a ``# noqa: 
<codes>`` comment.
+
+    Each code must be terminated by a word boundary, so tokens like ``SDK002x``
+    or ``F401foo`` are not treated as the corresponding code.
+
+    Anything after the first non-code token is treated as explanatory text and
+    ignored, so ``# noqa: F401 - see SDK002 docs`` only yields ``{"F401"}``.
+    """
+    match = _NOQA_RE.search(line)
+    if not match:
+        return set()
+    codes: set[str] = set()
+    for raw in match.group(1).split(","):
+        code_match = _NOQA_CODE_RE.match(raw.strip())
+        if not code_match:
+            break
+        codes.add(code_match.group(0))
+    return codes
+
+
+def has_nocheck_marker(source_lines: list[str], node: ast.ImportFrom | 
ast.Import, nocheck_code: str) -> bool:
+    """
+    Check if the import statement has a ``# noqa: <codes>`` comment that lists
+    ``nocheck_code`` on any of its lines. The code may appear anywhere in the
+    comma-separated code list (e.g. ``# noqa: F401, SDK002``).
+    """
     start = node.lineno
     end = node.end_lineno or start
     for lineno in range(start, end + 1):
-        if lineno <= len(source_lines) and marker in source_lines[lineno - 1]:
+        if lineno <= len(source_lines) and nocheck_code in 
_parse_noqa_codes(source_lines[lineno - 1]):
             return True
     return False
 
 
+def find_import_violations(
+    file_path: Path,
+    *,
+    is_violating_module: Callable[[str], bool],
+    nocheck_code: str,
+    check_plain_imports: bool = False,
+) -> list[tuple[int, str]]:
+    """
+    Walk imports in ``file_path`` and return ``(lineno, statement)`` for each
+    that matches ``is_violating_module`` and is not suppressed by a
+    ``# noqa: <nocheck_code>`` comment.
+
+    :param check_plain_imports: also check ``import x`` statements (in addition
+        to ``from x import y``).
+    """
+    try:
+        source = file_path.read_text(encoding="utf-8")
+        tree = ast.parse(source, filename=str(file_path))
+    except (OSError, UnicodeDecodeError, SyntaxError):
+        return []
+
+    source_lines = source.splitlines()
+    violations: list[tuple[int, str]] = []
+
+    for node in ast.walk(tree):
+        if isinstance(node, ast.ImportFrom):
+            if not node.module:
+                continue
+            if is_violating_module(node.module):
+                violating_names = [alias.name for alias in node.names]
+            else:
+                # Catch ``from airflow import settings`` style imports where 
the
+                # offending module is the dotted ``<module>.<name>`` path.
+                violating_names = [
+                    alias.name for alias in node.names if 
is_violating_module(f"{node.module}.{alias.name}")
+                ]
+            if not violating_names:
+                continue
+            if has_nocheck_marker(source_lines, node, nocheck_code):
+                continue
+            statement = f"from {node.module} import {', 
'.join(violating_names)}"
+            violations.append((node.lineno, statement))
+        elif check_plain_imports and isinstance(node, ast.Import):
+            for alias in node.names:
+                if is_violating_module(alias.name):
+                    if has_nocheck_marker(source_lines, node, nocheck_code):
+                        continue
+                    statement = f"import {alias.name}"
+                    if alias.asname:
+                        statement += f" as {alias.asname}"
+                    violations.append((node.lineno, statement))
+
+    return violations
+
+
+def report_import_violations(
+    files: list[str],
+    *,
+    check_func: Callable[[Path], list[tuple[int, str]]],
+    violation_label: str,
+    nocheck_code: str | None = None,
+    only_python_files: bool = False,
+) -> None:
+    """Run ``check_func`` on each file, print violations, and exit(1) if any 
are found.
+
+    When ``nocheck_code`` is given, a hint pointing at the ``# noqa: <code>``
+    escape hatch is printed alongside the failure summary.
+    """
+    file_paths = [Path(f) for f in files if not only_python_files or 
f.endswith(".py")]
+    total_violations = 0
+
+    for file_path in file_paths:
+        mismatches = check_func(file_path)
+        if mismatches:
+            console.print(f"[red]{file_path}[/red]:")
+            for line_num, statement in mismatches:
+                console.print(f"  [yellow]Line {line_num}[/yellow]: 
{statement}")
+            total_violations += len(mismatches)
+
+    if total_violations:
+        console.print()
+        console.print(f"[red]Found {total_violations} {violation_label}[/red]")
+        if nocheck_code:
+            console.print(
+                f"[yellow]Hint:[/yellow] if an import above is intentional, 
append "
+                f"`# noqa: {nocheck_code}` to the import line (single-line 
imports) "
+                f"or to the opening/closing paren line (multi-line imports) to 
"
+                f"suppress this check for that statement."
+            )
+        sys.exit(1)
+
+
 def get_imports_from_file(file_path: Path, *, only_top_level: bool) -> 
list[str]:
     """
     Returns list of all imports in file.
diff --git a/scripts/tests/ci/prek/test_check_core_imports_in_sdk.py 
b/scripts/tests/ci/prek/test_check_core_imports_in_sdk.py
new file mode 100644
index 00000000000..70b3571ac53
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_core_imports_in_sdk.py
@@ -0,0 +1,220 @@
+# 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 textwrap
+from pathlib import Path
+
+import pytest
+from check_core_imports_in_sdk import check_file_for_core_imports
+
+
+class TestCheckFileForCoreImports:
+    @pytest.mark.parametrize(
+        "code, expected",
+        [
+            pytest.param(
+                "from airflow.models import DagRun\n",
+                [(1, "from airflow.models import DagRun")],
+                id="from-import-core",
+            ),
+            pytest.param(
+                "import airflow.models\n",
+                [(1, "import airflow.models")],
+                id="import-core",
+            ),
+            pytest.param(
+                "import airflow.models as models\n",
+                [(1, "import airflow.models as models")],
+                id="import-core-aliased",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG\n",
+                [],
+                id="sdk-import-allowed",
+            ),
+            pytest.param(
+                "from airflow.sdk.definitions import dag\n",
+                [],
+                id="sdk-submodule-allowed",
+            ),
+            pytest.param(
+                "import airflow.sdk\n",
+                [],
+                id="import-sdk-allowed",
+            ),
+            pytest.param(
+                "import os\nimport sys\n",
+                [],
+                id="stdlib-only",
+            ),
+            pytest.param(
+                "from airflow import settings\n",
+                [(1, "from airflow import settings")],
+                id="from-airflow-import-name",
+            ),
+            pytest.param(
+                "from airflow import sdk\n",
+                [],
+                id="from-airflow-import-sdk-allowed",
+            ),
+            pytest.param(
+                "from airflow import settings, models\n",
+                [(1, "from airflow import settings, models")],
+                id="from-airflow-import-multiple-names",
+            ),
+            pytest.param(
+                "from airflow import sdk, settings\n",
+                [(1, "from airflow import settings")],
+                id="from-airflow-import-mixed-names",
+            ),
+        ],
+    )
+    def test_detects_core_imports(self, tmp_path: Path, code: str, expected: 
list[tuple[int, str]]):
+        f = tmp_path / "example.py"
+        f.write_text(code)
+        assert check_file_for_core_imports(f) == expected
+
+
+class TestNocheckMarker:
+    @pytest.mark.parametrize(
+        "code, expected",
+        [
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: SDK002\n",
+                [],
+                id="from-import-suppressed",
+            ),
+            pytest.param(
+                "import airflow.models  # noqa: SDK002\n",
+                [],
+                id="import-suppressed",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: SDK002 - needed 
for compat\n",
+                [],
+                id="marker-with-extra-text",
+            ),
+            pytest.param(
+                textwrap.dedent("""\
+                    from airflow.models import (
+                        DagRun,
+                        TaskInstance,
+                    )  # noqa: SDK002
+                """),
+                [],
+                id="multiline-marker-on-closing-paren",
+            ),
+            pytest.param(
+                textwrap.dedent("""\
+                    from airflow.models import (  # noqa: SDK002
+                        DagRun,
+                        TaskInstance,
+                    )
+                """),
+                [],
+                id="multiline-marker-on-first-line",
+            ),
+            pytest.param(
+                textwrap.dedent("""\
+                    from airflow.models import (
+                        DagRun,  # noqa: SDK002
+                        TaskInstance,
+                    )
+                """),
+                [],
+                id="multiline-marker-on-middle-line",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: E402\n",
+                [(1, "from airflow.models import DagRun")],
+                id="wrong-marker-not-suppressed",
+            ),
+            pytest.param(
+                textwrap.dedent("""\
+                    from airflow.models import (
+                        DagRun,
+                        TaskInstance,
+                    )
+                """),
+                [(1, "from airflow.models import DagRun, TaskInstance")],
+                id="multiline-without-marker-detected",
+            ),
+            pytest.param(
+                textwrap.dedent("""\
+                    from airflow.models import DagRun  # noqa: SDK002
+                    from airflow.jobs import BaseJob
+                """),
+                [(2, "from airflow.jobs import BaseJob")],
+                id="only-marked-line-suppressed",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: F401, SDK002\n",
+                [],
+                id="combined-codes-target-last",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: SDK002, F401\n",
+                [],
+                id="combined-codes-target-first",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: E402, SDK002, 
F401\n",
+                [],
+                id="combined-codes-target-middle",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa:SDK002\n",
+                [],
+                id="no-space-after-colon",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: F401\n",
+                [(1, "from airflow.models import DagRun")],
+                id="other-code-only-not-suppressed",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: F401 - see SDK002 
docs\n",
+                [(1, "from airflow.models import DagRun")],
+                id="code-in-explanation-not-suppressed",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: F401, SDK002 - 
needed for compat\n",
+                [],
+                id="combined-codes-with-explanation-suppressed",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa: SDK002x\n",
+                [(1, "from airflow.models import DagRun")],
+                id="partial-code-match-not-suppressed",
+            ),
+            pytest.param(
+                "from airflow import settings  # noqa: SDK002\n",
+                [],
+                id="from-airflow-import-name-suppressed",
+            ),
+            pytest.param(
+                "from airflow.models import DagRun  # noqa\n",
+                [(1, "from airflow.models import DagRun")],
+                id="bare-noqa-not-suppressed",
+            ),
+        ],
+    )
+    def test_nocheck_marker(self, tmp_path: Path, code: str, expected: 
list[tuple[int, str]]):
+        f = tmp_path / "example.py"
+        f.write_text(code)
+        assert check_file_for_core_imports(f) == expected
diff --git a/scripts/tests/ci/prek/test_check_sdk_imports.py 
b/scripts/tests/ci/prek/test_check_sdk_imports_in_core.py
similarity index 74%
rename from scripts/tests/ci/prek/test_check_sdk_imports.py
rename to scripts/tests/ci/prek/test_check_sdk_imports_in_core.py
index cf097027dc3..ed1bcbe057f 100644
--- a/scripts/tests/ci/prek/test_check_sdk_imports.py
+++ b/scripts/tests/ci/prek/test_check_sdk_imports_in_core.py
@@ -20,7 +20,7 @@ import textwrap
 from pathlib import Path
 
 import pytest
-from check_sdk_imports import check_file_for_sdk_imports
+from check_sdk_imports_in_core import check_file_for_sdk_imports
 
 
 class TestCheckFileForSdkImports:
@@ -132,6 +132,46 @@ class TestNocheckMarker:
                 [(2, "from airflow.sdk.definitions import dag")],
                 id="only-marked-line-suppressed",
             ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa: F401, SDK001\n",
+                [],
+                id="combined-codes-target-last",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa: SDK001, F401\n",
+                [],
+                id="combined-codes-target-first",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa: E402, SDK001, F401\n",
+                [],
+                id="combined-codes-target-middle",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa:SDK001\n",
+                [],
+                id="no-space-after-colon",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa: F401\n",
+                [(1, "from airflow.sdk import DAG")],
+                id="other-code-only-not-suppressed",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa: F401 - see SDK001 
docs\n",
+                [(1, "from airflow.sdk import DAG")],
+                id="code-in-explanation-not-suppressed",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa: F401, SDK001 - needed 
for compat\n",
+                [],
+                id="combined-codes-with-explanation-suppressed",
+            ),
+            pytest.param(
+                "from airflow.sdk import DAG  # noqa\n",
+                [(1, "from airflow.sdk import DAG")],
+                id="bare-noqa-not-suppressed",
+            ),
         ],
     )
     def test_nocheck_marker(self, tmp_path: Path, code: str, expected: 
list[tuple[int, str]]):
diff --git a/task-sdk/src/airflow/sdk/execution_time/context.py 
b/task-sdk/src/airflow/sdk/execution_time/context.py
index 1aaa68ed795..cdd25803989 100644
--- a/task-sdk/src/airflow/sdk/execution_time/context.py
+++ b/task-sdk/src/airflow/sdk/execution_time/context.py
@@ -1101,7 +1101,7 @@ def context_to_airflow_vars(context: Mapping[str, Any], 
in_env_var_format: bool
     """
     from datetime import datetime
 
-    from airflow import settings
+    from airflow import settings  # noqa: SDK002
 
     params = {}
     if in_env_var_format:
diff --git a/task-sdk/src/airflow/sdk/plugins_manager.py 
b/task-sdk/src/airflow/sdk/plugins_manager.py
index 8419a7dbba9..dca92ea0be8 100644
--- a/task-sdk/src/airflow/sdk/plugins_manager.py
+++ b/task-sdk/src/airflow/sdk/plugins_manager.py
@@ -23,7 +23,7 @@ import logging
 from functools import cache
 from typing import TYPE_CHECKING
 
-from airflow import settings
+from airflow import settings  # noqa: SDK002
 from airflow.sdk._shared.module_loading import import_string
 from airflow.sdk._shared.observability.metrics import stats
 from airflow.sdk._shared.plugins_manager import (

Reply via email to