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 2fab09576de Require Python 3.11+ for permission doc generators and 
fail on parse errors (#74654)
2fab09576de is described below

commit 2fab09576deeb31a1e576575631bd63bc038c279
Author: Pushkal Gupta <[email protected]>
AuthorDate: Sun Oct 18 21:07:31 2026 +0530

    Require Python 3.11+ for permission doc generators and fail on parse errors 
(#74654)
    
    * Pin permission doc generators to Python 3.11 and fail on parse errors
    
    * Raise instead of exiting on parse failure and point docs at uv run
---
 airflow-core/docs/security/api_permissions_ref.rst |  2 +-
 providers/fab/docs/auth-manager/access-control.rst |  2 +-
 scripts/ci/prek/extract_permissions.py             | 28 +++++++++---
 scripts/ci/prek/fab_permissions_doc.py             | 14 ++++--
 scripts/tests/ci/prek/test_extract_permissions.py  | 51 +++++++++++++++++++---
 5 files changed, 80 insertions(+), 17 deletions(-)

diff --git a/airflow-core/docs/security/api_permissions_ref.rst 
b/airflow-core/docs/security/api_permissions_ref.rst
index ddb1ef0142b..0e9af0696c5 100644
--- a/airflow-core/docs/security/api_permissions_ref.rst
+++ b/airflow-core/docs/security/api_permissions_ref.rst
@@ -16,7 +16,7 @@
     under the License.
 
 .. THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-   Regenerate with:  python scripts/ci/prek/extract_permissions.py
+   Regenerate with:  uv run scripts/ci/prek/extract_permissions.py
    Trigger:          prek run generate-api-permissions-doc --all-files
 
 API Endpoint Permission Reference
diff --git a/providers/fab/docs/auth-manager/access-control.rst 
b/providers/fab/docs/auth-manager/access-control.rst
index 4677f428e62..8cf134dd607 100644
--- a/providers/fab/docs/auth-manager/access-control.rst
+++ b/providers/fab/docs/auth-manager/access-control.rst
@@ -209,7 +209,7 @@ Stable API Permissions
 .. BEGIN GENERATED PERMISSIONS TABLE
 
 .. THE TABLE BELOW IS AUTO-GENERATED. DO NOT EDIT IT MANUALLY.
-   Regenerate with:  python scripts/ci/prek/fab_permissions_doc.py
+   Regenerate with:  uv run scripts/ci/prek/fab_permissions_doc.py
    Trigger:          prek run generate-fab-permissions-doc --all-files
 
 .. list-table:: Stable REST API permissions (FAB auth manager)
diff --git a/scripts/ci/prek/extract_permissions.py 
b/scripts/ci/prek/extract_permissions.py
index 5ade72296bf..b3f86e55a07 100644
--- a/scripts/ci/prek/extract_permissions.py
+++ b/scripts/ci/prek/extract_permissions.py
@@ -15,6 +15,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+# /// script
+# requires-python = ">=3.11"
+# ///
 """
 Extract permission requirements from FastAPI routes in Airflow REST API.
 
@@ -44,6 +47,13 @@ OUTPUT_RST = REPO_ROOT / 
"airflow-core/docs/security/api_permissions_ref.rst"
 API_PREFIX = "/api/v2"
 
 
+# ---------------------------------------------------------------------------
+# Errors
+# ---------------------------------------------------------------------------
+class RouteParseError(Exception):
+    """A route file could not be read or parsed, so its endpoints are 
unknown."""
+
+
 # ---------------------------------------------------------------------------
 # Data model
 # ---------------------------------------------------------------------------
@@ -276,8 +286,10 @@ def extract_from_file(path: pathlib.Path) -> 
list[PermissionEntry]:
         source = path.read_text(encoding="utf-8")
         tree = ast.parse(source)
     except (OSError, SyntaxError) as exc:
-        print(f"[WARN] Could not parse {path.name}: {exc}", file=sys.stderr)
-        return []
+        # Returning no entries here would quietly shrink a security reference 
that readers
+        # trust to be complete, and the caller has no way to tell that apart 
from a file
+        # that genuinely declares no routes.
+        raise RouteParseError(f"Could not parse {path}: {exc}") from exc
 
     # Build lookup tables for this file
     module_consts = _extract_module_string_constants(tree)
@@ -440,7 +452,7 @@ RST_HEADER = """\
     under the License.
 
 .. THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-   Regenerate with:  python scripts/ci/prek/extract_permissions.py
+   Regenerate with:  uv run scripts/ci/prek/extract_permissions.py
    Trigger:          prek run generate-api-permissions-doc --all-files
 
 API Endpoint Permission Reference
@@ -517,7 +529,11 @@ def main(argv: list[str] | None = None) -> int:
     )
     args = parser.parse_args(argv)
 
-    entries = extract_all_permissions(PUBLIC_ROUTES_DIR)
+    try:
+        entries = extract_all_permissions(PUBLIC_ROUTES_DIR)
+    except RouteParseError as exc:
+        print(f"[FAIL] {exc}", file=sys.stderr)
+        return 1
     content = render_rst(entries)
 
     if args.print_only:
@@ -527,14 +543,14 @@ def main(argv: list[str] | None = None) -> int:
     if args.check:
         if not OUTPUT_RST.exists():
             print(
-                f"[FAIL] {OUTPUT_RST} does not exist. Run: python 
scripts/ci/prek/extract_permissions.py",
+                f"[FAIL] {OUTPUT_RST} does not exist. Run: uv run 
scripts/ci/prek/extract_permissions.py",
                 file=sys.stderr,
             )
             return 1
         existing = OUTPUT_RST.read_text(encoding="utf-8")
         if existing != content:
             print(
-                f"[FAIL] {OUTPUT_RST} is stale. Run: python 
scripts/ci/prek/extract_permissions.py",
+                f"[FAIL] {OUTPUT_RST} is stale. Run: uv run 
scripts/ci/prek/extract_permissions.py",
                 file=sys.stderr,
             )
             return 1
diff --git a/scripts/ci/prek/fab_permissions_doc.py 
b/scripts/ci/prek/fab_permissions_doc.py
index cfabeb581cf..3bbbdf075b9 100755
--- a/scripts/ci/prek/fab_permissions_doc.py
+++ b/scripts/ci/prek/fab_permissions_doc.py
@@ -15,6 +15,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+# /// script
+# requires-python = ">=3.11"
+# ///
 """
 Generate the FAB auth manager's REST API permission table.
 
@@ -41,6 +44,7 @@ import sys
 from extract_permissions import (
     PUBLIC_ROUTES_DIR,
     PermissionEntry,
+    RouteParseError,
     extract_all_permissions,
 )
 
@@ -360,7 +364,7 @@ def render_rst(entries: list[PermissionEntry]) -> str:
         BEGIN_MARKER,
         "",
         ".. THE TABLE BELOW IS AUTO-GENERATED. DO NOT EDIT IT MANUALLY.",
-        "   Regenerate with:  python scripts/ci/prek/fab_permissions_doc.py",
+        "   Regenerate with:  uv run scripts/ci/prek/fab_permissions_doc.py",
         "   Trigger:          prek run generate-fab-permissions-doc 
--all-files",
         "",
         ".. list-table:: Stable REST API permissions (FAB auth manager)",
@@ -391,7 +395,11 @@ def main(argv: list[str] | None = None) -> int:
     parser.add_argument("--print", dest="print_only", action="store_true", 
help="Print instead of write.")
     args = parser.parse_args(argv)
 
-    entries = extract_all_permissions(PUBLIC_ROUTES_DIR)
+    try:
+        entries = extract_all_permissions(PUBLIC_ROUTES_DIR)
+    except RouteParseError as exc:
+        print(f"[FAIL] {exc}", file=sys.stderr)
+        return 1
     table = render_rst(entries)
 
     if args.print_only:
@@ -409,7 +417,7 @@ def main(argv: list[str] | None = None) -> int:
     if args.check:
         if current != updated:
             print(
-                f"[FAIL] {OUTPUT_RST} is stale. Run: python 
{pathlib.Path(__file__).name}",
+                f"[FAIL] {OUTPUT_RST} is stale. Run: uv run 
scripts/ci/prek/{pathlib.Path(__file__).name}",
                 file=sys.stderr,
             )
             return 1
diff --git a/scripts/tests/ci/prek/test_extract_permissions.py 
b/scripts/tests/ci/prek/test_extract_permissions.py
index 763578f4697..269f2989877 100644
--- a/scripts/tests/ci/prek/test_extract_permissions.py
+++ b/scripts/tests/ci/prek/test_extract_permissions.py
@@ -50,6 +50,7 @@ if str(PREK_DIR) not in sys.path:
 from extract_permissions import (  # noqa: E402
     _FN_TO_RESOURCE_INFO,
     PermissionEntry,
+    RouteParseError,
     _build_resource_label,
     _extract_method_arg,
     _extract_module_string_constants,
@@ -537,14 +538,16 @@ class TestExtractFromFile:
         assert len(entries) == 1
         assert entries[0].resource == "DAG"
 
-    def test_syntax_error_returns_empty_list(self, tmp_path, capsys):
+    def test_syntax_error_raises_instead_of_dropping_entries(self, tmp_path):
         f = tmp_path / "broken.py"
         f.write_text("def broken(:\n")
-        entries = extract_from_file(f)
-        assert entries == []
-        # Should print a warning, not raise
-        captured = capsys.readouterr()
-        assert "WARN" in captured.err
+        with pytest.raises(RouteParseError) as exc_info:
+            extract_from_file(f)
+        assert str(f) in str(exc_info.value)
+
+    def test_unreadable_file_raises(self, tmp_path):
+        with pytest.raises(RouteParseError):
+            extract_from_file(tmp_path / "does_not_exist.py")
 
     def test_source_file_is_basename(self, tmp_path):
         f = _make_route_file(
@@ -835,3 +838,39 @@ class TestCheckMode:
         monkeypatch.setattr(ep, "OUTPUT_RST", tmp_path / "does_not_exist.rst")
         result = ep.main(["--check"])
         assert result == 1
+
+
+# ===========================================================================
+# Integration test: a route file that cannot be parsed must fail the run
+# ===========================================================================
+
+
+class TestParseFailureIsFatal:
+    @staticmethod
+    def _routes_dir_with_broken_file(tmp_path):
+        routes_dir = tmp_path / "routes"
+        routes_dir.mkdir()
+        (routes_dir / "broken.py").write_text("def broken(:\n")
+        return routes_dir
+
+    def test_main_fails_and_leaves_the_doc_alone(self, tmp_path, monkeypatch, 
capsys):
+        import extract_permissions as ep
+
+        rst_file = tmp_path / "api_permissions_ref.rst"
+        rst_file.write_text("the full table")
+
+        monkeypatch.setattr(ep, "PUBLIC_ROUTES_DIR", 
self._routes_dir_with_broken_file(tmp_path))
+        monkeypatch.setattr(ep, "OUTPUT_RST", rst_file)
+
+        assert ep.main([]) == 1
+        assert "[FAIL]" in capsys.readouterr().err
+        assert rst_file.read_text() == "the full table"
+
+    def test_fab_main_fails_too(self, tmp_path, monkeypatch, capsys):
+        """``fab_permissions_doc`` shares the extractor, so it must fail the 
same way."""
+        import fab_permissions_doc as fab
+
+        monkeypatch.setattr(fab, "PUBLIC_ROUTES_DIR", 
self._routes_dir_with_broken_file(tmp_path))
+
+        assert fab.main([]) == 1
+        assert "[FAIL]" in capsys.readouterr().err

Reply via email to