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