Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions airflow-core/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ repos:
language: python
pass_filenames: true
files: ^src/airflow/.*\.py$
- id: check-http-exception-import-from-fastapi
name: Check HTTPException is imported from fastapi
entry: ../scripts/ci/prek/check_http_exception_import_from_fastapi.py
language: python
pass_filenames: true
files: >
(?x)
^src/airflow/api_fastapi/.*\.py$|
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage gap worth widening: airflow-core/src/airflow/utils/serve_logs/log_server.py wires a FastAPI app and raises HTTPException in ~10 places, but it lives outside src/airflow/api_fastapi/, so this regex skips it. If someone there swaps from fastapi import HTTPException for from starlette.exceptions import HTTPException (or http.client), the hook will not catch it -- the exact bug class this PR is meant to prevent.

Options: add ^src/airflow/utils/serve_logs/.*\.py$ to the union, or rescope the regex to FastAPI usage rather than path.

^tests/unit/api_fastapi/.*\.py$
- id: create-missing-init-py-files-tests
name: Create missing init.py files in tests
entry: ../scripts/ci/prek/check_init_in_tests.py
Expand Down
35 changes: 35 additions & 0 deletions providers/amazon/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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.
---
default_stages: [pre-commit, pre-push]
minimum_prek_version: '0.3.4'
default_language_version:
python: python3
node: 22.19.0
golang: 1.24.0
repos:
- repo: local
hooks:
- id: check-http-exception-import-from-fastapi
name: Check HTTPException is imported from fastapi
entry: ../../scripts/ci/prek/check_http_exception_import_from_fastapi.py
language: python
pass_filenames: true
files: >
(?x)
^src/airflow/providers/amazon/aws/auth_manager/.*\.py$|
^tests/unit/amazon/aws/auth_manager/.*\.py$
9 changes: 9 additions & 0 deletions providers/common/ai/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ repos:
entry: ../../../scripts/ci/prek/compile_provider_assets.py ai
pass_filenames: false
additional_dependencies: ['pnpm@10.25.0']
- id: check-http-exception-import-from-fastapi
name: Check HTTPException is imported from fastapi
entry: ../../../scripts/ci/prek/check_http_exception_import_from_fastapi.py
language: python
pass_filenames: true
files: >
(?x)
^src/airflow/providers/common/ai/plugins/.*\.py$|
^tests/unit/common/ai/plugins/.*\.py$
9 changes: 9 additions & 0 deletions providers/edge3/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ repos:
entry: ../../scripts/ci/prek/generate_openapi_spec_providers.py edge
pass_filenames: false
files: ^src/airflow/providers/edge3/worker_api/.*\.py$
- id: check-http-exception-import-from-fastapi
name: Check HTTPException is imported from fastapi
entry: ../../scripts/ci/prek/check_http_exception_import_from_fastapi.py
language: python
pass_filenames: true
files: >
(?x)
^src/airflow/providers/edge3/(worker_api|plugins)/.*\.py$|
^tests/unit/edge3/(worker_api|plugins)/.*\.py$
- id: ts-compile-lint-edge-ui
name: Compile / format / lint edge UI
description: TS types generation / ESLint / Prettier new UI files in Edge Provider
Expand Down
9 changes: 9 additions & 0 deletions providers/fab/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ repos:
entry: ../../scripts/ci/prek/generate_openapi_spec_providers.py fab
pass_filenames: false
files: ^src/airflow/providers/fab/auth_manager/api_fastapi/.*\.py$
- id: check-http-exception-import-from-fastapi
name: Check HTTPException is imported from fastapi
entry: ../../scripts/ci/prek/check_http_exception_import_from_fastapi.py
language: python
pass_filenames: true
files: >
(?x)
^src/airflow/providers/fab/auth_manager/api_fastapi/.*\.py$|
^tests/unit/fab/auth_manager/api_fastapi/.*\.py$
- id: update-migration-references-fab
name: Update migration ref doc for FAB
language: python
Expand Down
9 changes: 9 additions & 0 deletions providers/keycloak/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ repos:
entry: ../../scripts/ci/prek/generate_openapi_spec_providers.py keycloak
pass_filenames: false
files: ^src/airflow/providers/keycloak/auth_manager/.*\.py$
- id: check-http-exception-import-from-fastapi
name: Check HTTPException is imported from fastapi
entry: ../../scripts/ci/prek/check_http_exception_import_from_fastapi.py
language: python
pass_filenames: true
files: >
(?x)
^src/airflow/providers/keycloak/auth_manager/.*\.py$|
^tests/unit/keycloak/auth_manager/.*\.py$
131 changes: 131 additions & 0 deletions scripts/ci/prek/check_http_exception_import_from_fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/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.
"""Check that ``HTTPException`` is imported from ``fastapi`` in fastapi-using trees.

The hook is wired into per-distribution ``.pre-commit-config.yaml`` files
(``airflow-core``, ``providers/amazon``, ``providers/common/ai``,
``providers/edge3``, ``providers/fab``, ``providers/keycloak``), each
scoped to the subtree that actually wires a FastAPI app. Provider trees
that mix client and server code (e.g. edge3's ``cli/`` is a client) are
scoped to the server-side subfolders only to avoid false positives on
stdlib HTTP usage in the client. Within those scopes, every
``HTTPException`` must come from ``fastapi`` (which re-exports the
Starlette class). Two common mistakes this hook catches:

* ``from starlette.exceptions import HTTPException`` — a different class at
runtime; ``isinstance(exc, fastapi.HTTPException)`` and
``pytest.raises(fastapi.HTTPException)`` will not match it.
* ``from http.client import HTTPException`` — an unrelated stdlib exception
whose constructor signature differs, so the route returns 500 instead of
the intended HTTP status.
"""

# /// 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


def _is_fastapi_module(module: str) -> bool:
"""Return True if *module* is ``fastapi`` or a submodule of it."""
return module == "fastapi" or module.startswith("fastapi.")


def check_file(file_path: Path) -> list[tuple[int, str]]:
"""Return list of ``(line_number, import_statement)`` violations."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test for this under scripts/tests/ci/prek/test_check_http_exception_import_from_fastapi.py? The sibling hook check_conf_import_in_providers has one, and CLAUDE.md asks for tests with every new feature. A small parametrized test over a handful of good/bad import strings (plain, aliased, dotted-from fastapi.exceptions, starlette.exceptions, http.client) would lock in the AST logic and guard against regressions when the alias-rendering / module-prefix logic is touched.

try:
source = file_path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(file_path))
except (OSError, UnicodeDecodeError, SyntaxError):
return []

violations: list[tuple[int, str]] = []

for node in ast.walk(tree):
if not isinstance(node, ast.ImportFrom) or not node.module:
continue
if _is_fastapi_module(node.module):
continue
bad_aliases = [alias for alias in node.names if alias.name == "HTTPException"]
if not bad_aliases:
continue
rendered = ", ".join(
alias.name if not alias.asname else f"{alias.name} as {alias.asname}" for alias in bad_aliases
)
violations.append((node.lineno, f"from {node.module} import {rendered}"))

return violations


def main() -> None:
parser = argparse.ArgumentParser(description="Check that HTTPException is imported from fastapi")
parser.add_argument("files", nargs="*", help="Files to check")
args = parser.parse_args()

if not args.files:
return

total_violations = 0

for file_path in [Path(f) for f in args.files]:
violations = check_file(file_path)
if not violations:
continue
if console:
console.print(f"[red]{file_path}[/red]:")
for line_num, statement in violations:
console.print(f" [yellow]Line {line_num}[/yellow]: {statement}")
else:
print(f"{file_path}:")
for line_num, statement in violations:
print(f" Line {line_num}: {statement}")
total_violations += len(violations)

if total_violations:
message = (
f"Found {total_violations} HTTPException import(s) not coming from `fastapi`.\n"
"Use `from fastapi import HTTPException` instead. Importing it from "
"`starlette.exceptions`, `http.client`, or any other module yields a "
"different class at runtime and breaks `isinstance` / `pytest.raises` "
"checks against `fastapi.HTTPException` (and, for `http.client`, calls "
"the wrong constructor so the route returns 500 instead of the intended "
"status)."
)
if console:
console.print()
console.print(f"[red]{message}[/red]")
else:
print()
print(message)
sys.exit(1)


if __name__ == "__main__":
main()
sys.exit(0)
Loading