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 4395fe94773 Add tests for scripts and remove redundant sys.path.insert
calls (#63598)
4395fe94773 is described below
commit 4395fe947730feaca9135c2071ab3bca7ad431cf
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Mar 14 17:22:46 2026 +0100
Add tests for scripts and remove redundant sys.path.insert calls (#63598)
* Add tests for scripts and remove redundant sys.path.insert calls
- Remove 85 redundant `sys.path.insert(0,
str(Path(__file__).parent.resolve()))`
calls from scripts in ci/prek/, cov/, and in_container/. Python already
adds the script's directory to sys.path when running a file directly,
making these calls unnecessary.
- Keep 6 cross-directory sys.path.insert calls that are genuinely needed
(AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_ROOT, etc.).
- Add __init__.py files to scripts/ci/ and scripts/ci/prek/ to make them
proper Python packages.
- Add scripts/pyproject.toml with package discovery and pytest config.
- Add 176 tests covering: common_prek_utils (insert_documentation,
check_list_sorted, get_provider_id_from_path, ConsoleDiff, etc.),
new_session_in_provide_session, check_deprecations, unittest_testcase,
changelog_duplicates, newsfragments, checkout_no_credentials, and
check_order_dockerfile_extras.
- Add scripts tests to CI: new SCRIPTS_FILES file group in selective
checks, run-scripts-tests output, and tests-scripts job in
basic-tests.yml.
- Document scripts as a workspace distribution in CLAUDE.md.
* Add pytest as dev dependency for scripts distribution
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Use devel-common instead of pytest for scripts dev dependencies
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Fix xdist test collection order for newsfragment tests
Sort the VALID_CHANGE_TYPES set when passing to parametrize to ensure
deterministic test ordering across xdist workers.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Update scripts/ci/prek/changelog_duplicates.py
Co-authored-by: Dev-iL <[email protected]>
* Refactor scripts tests: convert setup methods to fixtures and extract
constants
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Dev-iL <[email protected]>
---
.github/workflows/basic-tests.yml | 23 ++
.github/workflows/ci-amd-arm.yml | 2 +
.pre-commit-config.yaml | 10 +-
AGENTS.md | 8 +-
.../src/airflow_breeze/utils/selective_checks.py | 11 +
pyproject.toml | 4 +
.../{cov/restapi_coverage.py => ci/__init__.py} | 16 -
.../restapi_coverage.py => ci/prek/__init__.py} | 16 -
scripts/ci/prek/breeze_cmd_line.py | 2 -
scripts/ci/prek/capture_airflowctl_help.py | 4 -
scripts/ci/prek/changelog_duplicates.py | 49 +--
.../ci/prek/check_airflow_bug_report_template.py | 3 -
scripts/ci/prek/check_airflow_imports.py | 1 -
scripts/ci/prek/check_airflow_imports_in_shared.py | 1 -
.../ci/prek/check_airflow_v_imports_in_tests.py | 5 +-
.../ci/prek/check_airflowctl_command_coverage.py | 2 -
.../prek/check_base_operator_partial_arguments.py | 2 -
scripts/ci/prek/check_cli_definition_imports.py | 1 -
scripts/ci/prek/check_common_sql_dependency.py | 4 +-
scripts/ci/prek/check_core_imports_in_sdk.py | 1 -
scripts/ci/prek/check_core_imports_in_shared.py | 1 -
scripts/ci/prek/check_default_configuration.py | 4 -
scripts/ci/prek/check_execution_api_versions.py | 2 -
scripts/ci/prek/check_extra_packages_ref.py | 10 +-
scripts/ci/prek/check_i18n_json.py | 3 -
scripts/ci/prek/check_imports_in_providers.py | 4 -
scripts/ci/prek/check_init_decorator_arguments.py | 2 -
scripts/ci/prek/check_integrations_list.py | 1 -
scripts/ci/prek/check_k8s_schemas_published.py | 2 -
scripts/ci/prek/check_kubeconform.py | 2 -
scripts/ci/prek/check_min_python_version.py | 3 -
scripts/ci/prek/check_order_dockerfile_extras.py | 4 +-
scripts/ci/prek/check_provider_docs.py | 5 +-
scripts/ci/prek/check_provider_yaml_files.py | 2 -
.../check_providers_subpackages_all_have_init.py | 1 -
scripts/ci/prek/check_revision_heads_map.py | 2 -
scripts/ci/prek/check_schema_defaults.py | 4 -
scripts/ci/prek/check_sdk_imports.py | 1 -
.../prek/check_shared_distributions_structure.py | 1 -
.../ci/prek/check_shared_distributions_usage.py | 1 -
.../ci/prek/check_system_tests_hidden_in_index.py | 1 -
.../check_template_context_variable_in_sync.py | 3 -
scripts/ci/prek/check_template_fields.py | 2 -
scripts/ci/prek/check_test_only_imports_in_src.py | 1 -
scripts/ci/prek/check_tests_in_right_folders.py | 2 -
scripts/ci/prek/check_ti_vs_tis_attributes.py | 2 -
scripts/ci/prek/check_version_consistency.py | 8 +-
scripts/ci/prek/common_prek_utils.py | 2 +-
scripts/ci/prek/compile_ui_assets.py | 2 -
scripts/ci/prek/compile_ui_assets_dev.py | 3 -
scripts/ci/prek/download_k8s_schemas.py | 3 -
scripts/ci/prek/generate_airflow_diagrams.py | 1 -
scripts/ci/prek/generate_openapi_spec.py | 4 -
scripts/ci/prek/generate_openapi_spec_providers.py | 2 -
scripts/ci/prek/generate_volumes_for_sources.py | 4 -
scripts/ci/prek/lint_helm.py | 2 -
scripts/ci/prek/lint_json_schema.py | 2 -
scripts/ci/prek/local_yml_mounts.py | 3 -
scripts/ci/prek/migration_reference.py | 4 -
scripts/ci/prek/mypy.py | 6 +-
scripts/ci/prek/mypy_folder.py | 3 -
scripts/ci/prek/newsfragments.py | 57 +--
scripts/ci/prek/sync_translation_namespaces.py | 2 -
scripts/ci/prek/ts_compile_lint_common_ai.py | 1 -
scripts/ci/prek/ts_compile_lint_edge.py | 1 -
.../prek/ts_compile_lint_simple_auth_manager_ui.py | 1 -
scripts/ci/prek/ts_compile_lint_ui.py | 1 -
scripts/ci/prek/update_airflow_pyproject_toml.py | 4 +-
scripts/ci/prek/update_chart_dependencies.py | 3 -
scripts/ci/prek/update_example_dags_paths.py | 1 -
scripts/ci/prek/update_providers_build_files.py | 1 -
scripts/ci/prek/update_providers_dependencies.py | 2 -
scripts/ci/prek/update_source_date_epoch.py | 4 -
scripts/ci/prek/update_versions.py | 3 -
scripts/ci/prek/upgrade_important_versions.py | 4 +-
scripts/ci/prek/validate_chart_annotations.py | 3 -
scripts/cov/cli_coverage.py | 5 -
scripts/cov/core_coverage.py | 5 -
scripts/cov/other_coverage.py | 5 -
scripts/cov/restapi_coverage.py | 5 -
.../in_container/install_airflow_and_providers.py | 1 -
.../in_container/install_airflow_python_client.py | 2 -
.../install_development_dependencies.py | 2 -
.../in_container/run_capture_airflowctl_help.py | 2 -
.../in_container/run_check_imports_in_providers.py | 1 -
scripts/in_container/run_generate_constraints.py | 2 -
scripts/in_container/run_generate_openapi_spec.py | 5 +-
.../run_generate_openapi_spec_providers.py | 1 -
.../in_container/run_provider_yaml_files_check.py | 12 +-
scripts/pyproject.toml | 76 ++++
.../{cov/restapi_coverage.py => tests/__init__.py} | 16 -
.../restapi_coverage.py => tests/ci/__init__.py} | 16 -
.../ci/prek/__init__.py} | 16 -
scripts/tests/ci/prek/conftest.py | 76 ++++
scripts/tests/ci/prek/test_changelog_duplicates.py | 101 +++++
scripts/tests/ci/prek/test_check_deprecations.py | 206 ++++++++++
.../ci/prek/test_check_order_dockerfile_extras.py | 118 ++++++
.../tests/ci/prek/test_checkout_no_credentials.py | 251 ++++++++++++
scripts/tests/ci/prek/test_common_prek_utils.py | 425 +++++++++++++++++++++
.../ci/prek/test_new_session_in_provide_session.py | 243 ++++++++++++
scripts/tests/ci/prek/test_newsfragments.py | 81 ++++
scripts/tests/ci/prek/test_unittest_testcase.py | 93 +++++
102 files changed, 1806 insertions(+), 328 deletions(-)
diff --git a/.github/workflows/basic-tests.yml
b/.github/workflows/basic-tests.yml
index b026316889d..c3be7603724 100644
--- a/.github/workflows/basic-tests.yml
+++ b/.github/workflows/basic-tests.yml
@@ -40,6 +40,10 @@ on: # yamllint disable-line rule:truthy
description: "Whether to run breeze integration tests (true/false)"
required: true
type: string
+ run-scripts-tests:
+ description: "Whether to run scripts tests (true/false)"
+ required: true
+ type: string
basic-checks-only:
description: "Whether to run only basic checks (true/false)"
required: true
@@ -145,6 +149,25 @@ jobs:
- name: "Run shared ${{ matrix.shared-distribution }} tests"
run: uv run --group dev pytest --color=yes -n auto
working-directory: shared/${{ matrix.shared-distribution }}
+ tests-scripts:
+ timeout-minutes: 10
+ name: Scripts tests
+ runs-on: ${{ fromJSON(inputs.runners) }}
+ if: inputs.run-scripts-tests == 'true'
+ steps:
+ - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ with:
+ fetch-depth: 1
+ persist-credentials: false
+ - name: "Install uv"
+ run: pip install "uv==${UV_VERSION}"
+ env:
+ UV_VERSION: ${{ inputs.uv-version }}
+ - name: "Run scripts tests"
+ run: uv run --project . pytest --color=yes -n auto
+ working-directory: ./scripts/
+
tests-ui:
timeout-minutes: 15
name: React UI tests
diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml
index 50f5ba8c95e..9e509925317 100644
--- a/.github/workflows/ci-amd-arm.yml
+++ b/.github/workflows/ci-amd-arm.yml
@@ -120,6 +120,7 @@ jobs:
run-task-sdk-tests: ${{
steps.selective-checks.outputs.run-task-sdk-tests }}
run-task-sdk-integration-tests: ${{
steps.selective-checks.outputs.run-task-sdk-integration-tests }}
run-breeze-integration-tests: ${{
steps.selective-checks.outputs.run-breeze-integration-tests }}
+ run-scripts-tests: ${{ steps.selective-checks.outputs.run-scripts-tests
}}
runner-type: ${{ steps.selective-checks.outputs.runner-type }}
run-ui-tests: ${{ steps.selective-checks.outputs.run-ui-tests }}
run-ui-e2e-tests: ${{ steps.selective-checks.outputs.run-ui-e2e-tests }}
@@ -204,6 +205,7 @@ jobs:
skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }}
canary-run: ${{needs.build-info.outputs.canary-run}}
run-breeze-integration-tests:
${{needs.build-info.outputs.run-breeze-integration-tests}}
+ run-scripts-tests: ${{needs.build-info.outputs.run-scripts-tests}}
latest-versions-only: ${{needs.build-info.outputs.latest-versions-only}}
use-uv: ${{needs.build-info.outputs.use-uv}}
platform: ${{ needs.build-info.outputs.platform }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 702496555b3..84bcfbe669c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -700,7 +700,15 @@ repos:
name: Verify usage of Airflow deprecation classes in core
entry: category=DeprecationWarning|category=PendingDeprecationWarning
files: \.py$
- exclude:
^airflow-core/src/airflow/configuration\.py$|^airflow-core/tests/.*$|^providers/.*/src/airflow/providers/|^scripts/in_container/verify_providers\.py$|^providers/.*/tests/.*$|^devel-common/
+ exclude: >
+ (?x)
+ ^airflow-core/src/airflow/configuration\.py$|
+ ^airflow-core/tests/.*$|
+ ^providers/.*/src/airflow/providers/|
+ ^scripts/in_container/verify_providers\.py$|
+ ^providers/.*/tests/.*$|
+ ^scripts/tests/.*$|
+ ^devel-common/
pass_filenames: true
- id: check-provide-create-sessions-imports
language: pygrep
diff --git a/AGENTS.md b/AGENTS.md
index 236d953c115..32cedcee6be 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,7 +27,8 @@
- **Run Helm tests in parallel with xdist** `breeze testing helm-tests
--use-xdist`
- **Run Helm tests with specific K8s version:** `breeze testing helm-tests
--use-xdist --kubernetes-version 1.35.0`
- **Run specific Helm test type:** `breeze testing helm-tests --use-xdist
--test-type <type>` (types: `airflow_aux`, `airflow_core`, `apiserver`,
`dagprocessor`, `other`, `redis`, `security`, `statsd`, `webserver`)
-- **Run other suites of tests** `breeze testing <test_group>` (test groups:
`airflow-ctl-tests`, `docker-compose-tests`, `task-sdk-tests`
+- **Run other suites of tests** `breeze testing <test_group>` (test groups:
`airflow-ctl-tests`, `docker-compose-tests`, `task-sdk-tests`)
+- **Run scripts tests:** `uv run --project scripts pytest scripts/tests/ -xvs`
- **Run Airflow CLI:** `breeze run airflow dags list`
- **Type-check:** `breeze run mypy path/to/code`
- **Lint with ruff only:** `prek run ruff --from-ref <target_branch>`
@@ -56,6 +57,11 @@ UV workspace monorepo. Key paths:
- `providers/` — 100+ provider packages, each with its own `pyproject.toml`
- `airflow-ctl/` — management CLI tool
- `chart/` — Helm chart for Kubernetes deployment
+- `dev/` — development utilities and scripts used to bootstrap the
environment, releases, breeze dev env
+- `scripts/` — utility scripts for CI, Docker, and prek hooks (workspace
distribution `apache-airflow-scripts`)
+ - `ci/prek/` — prek (pre-commit) hook scripts; shared utilities in
`common_prek_utils.py`
+ - `tests/` — pytest tests for the scripts; run with `uv run --project
scripts pytest scripts/tests/`
+
## Architecture Boundaries
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 4a349c459ac..f39f4462a4e 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -135,6 +135,7 @@ class FileGroupForCi(Enum):
UNIT_TEST_FILES = auto()
DEVEL_TOML_FILES = auto()
UI_ENGLISH_TRANSLATION_FILES = auto()
+ SCRIPTS_FILES = auto()
class AllProvidersSentinel:
@@ -333,6 +334,12 @@ CI_FILE_GROUP_MATCHES: HashableDict[FileGroupForCi] =
HashableDict(
FileGroupForCi.UI_ENGLISH_TRANSLATION_FILES: [
r"^airflow-core/src/airflow/ui/public/i18n/locales/en/.*\.json$",
],
+ FileGroupForCi.SCRIPTS_FILES: [
+ r"^scripts/ci/.*\.py$",
+ r"^scripts/cov/.*\.py$",
+ r"^scripts/tools/.*\.py$",
+ r"^scripts/tests/.*\.py$",
+ ],
}
)
@@ -945,6 +952,10 @@ class SelectiveChecks:
FileGroupForCi.AIRFLOW_CTL_INTEGRATION_TEST_FILES
)
+ @cached_property
+ def run_scripts_tests(self) -> bool:
+ return self._should_be_run(FileGroupForCi.SCRIPTS_FILES)
+
@cached_property
def run_kubernetes_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.KUBERNETES_FILES)
diff --git a/pyproject.toml b/pyproject.toml
index 878533fff2f..b4a5b024cf6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -785,6 +785,7 @@ testing = ["dev", "providers.tests", "tests_common",
"tests", "system", "unit",
"providers/**/tests/*" = ["D", "TID253", "S101", "TRY002"]
"performance/tests/*" = ["S101"]
"dev/registry/tests/*" = ["S101"]
+"scripts/tests/*" = ["S101"]
# Shared distributions SHOULD use relative imports when referencing each other
# This one disables 'ban-relative-imports'.
@@ -1288,6 +1289,7 @@ dev = [
"apache-airflow[all]",
"apache-airflow-breeze",
"apache-airflow-dev",
+ "apache-airflow-scripts",
"apache-airflow-devel-common[no-doc]",
"apache-airflow-docker-tests",
"apache-airflow-task-sdk-integration-tests",
@@ -1344,6 +1346,7 @@ no-build-isolation-package = ["sphinx-redoc"]
apache-airflow = {workspace = true}
apache-airflow-breeze = {workspace = true}
apache-airflow-dev = {workspace = true}
+apache-airflow-scripts = {workspace = true}
apache-airflow-core = {workspace = true}
apache-airflow-ctl = {workspace = true}
apache-airflow-ctl-tests = {workspace = true}
@@ -1480,6 +1483,7 @@ members = [
"airflow-ctl-tests",
"dev",
"devel-common",
+ "scripts",
"docker-tests",
"task-sdk-integration-tests",
"helm-tests",
diff --git a/scripts/cov/restapi_coverage.py b/scripts/ci/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/ci/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/ci/__init__.py
@@ -14,19 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
- args = ["-qq"] + source_files
- run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/cov/restapi_coverage.py b/scripts/ci/prek/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/ci/prek/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/ci/prek/__init__.py
@@ -14,19 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
- args = ["-qq"] + source_files
- run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/ci/prek/breeze_cmd_line.py
b/scripts/ci/prek/breeze_cmd_line.py
index c040bcca657..d95cc4e8552 100755
--- a/scripts/ci/prek/breeze_cmd_line.py
+++ b/scripts/ci/prek/breeze_cmd_line.py
@@ -27,9 +27,7 @@ from __future__ import annotations
import os
import subprocess
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, console,
initialize_breeze_prek
BREEZE_INSTALL_DIR = AIRFLOW_ROOT_PATH / "dev" / "breeze"
diff --git a/scripts/ci/prek/capture_airflowctl_help.py
b/scripts/ci/prek/capture_airflowctl_help.py
index b988fbf9c8f..d9618c58ad0 100755
--- a/scripts/ci/prek/capture_airflowctl_help.py
+++ b/scripts/ci/prek/capture_airflowctl_help.py
@@ -24,10 +24,6 @@
# ///
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/changelog_duplicates.py
b/scripts/ci/prek/changelog_duplicates.py
index b96526f2e85..e3b7f636911 100755
--- a/scripts/ci/prek/changelog_duplicates.py
+++ b/scripts/ci/prek/changelog_duplicates.py
@@ -38,25 +38,30 @@ known_exceptions = [
pr_number_re = re.compile(r".*\(#([0-9]{1,6})\)`?`?$")
-files = sys.argv[1:]
-
-failed = False
-for filename in files:
- seen = []
- dups = []
- with open(filename) as f:
- for line in f:
- match = pr_number_re.search(line)
- if match:
- pr_number = match.group(1)
- if pr_number not in seen:
- seen.append(pr_number)
- elif pr_number not in known_exceptions:
- dups.append(pr_number)
-
- if dups:
- print(f"Duplicate changelog entries found for {filename}: {dups}")
- failed = True
-
-if failed:
- sys.exit(1)
+
+def find_duplicates(lines: list[str]) -> list[str]:
+ """Find duplicate PR numbers in changelog lines, excluding known
exceptions."""
+ seen: list[str] = []
+ dups: list[str] = []
+ for line in lines:
+ if (match := pr_number_re.search(line)) and (pr := match.group(1)):
+ if pr not in seen:
+ seen.append(pr)
+ elif pr not in known_exceptions:
+ dups.append(pr)
+ return dups
+
+
+def main(filenames: list[str]) -> int:
+ failed = False
+ for filename in filenames:
+ with open(filename) as f:
+ dups = find_duplicates(f.readlines())
+ if dups:
+ print(f"Duplicate changelog entries found for {filename}: {dups}")
+ failed = True
+ return 1 if failed else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/scripts/ci/prek/check_airflow_bug_report_template.py
b/scripts/ci/prek/check_airflow_bug_report_template.py
index dd36a06b3af..88358654424 100755
--- a/scripts/ci/prek/check_airflow_bug_report_template.py
+++ b/scripts/ci/prek/check_airflow_bug_report_template.py
@@ -26,11 +26,8 @@
from __future__ import annotations
import sys
-from pathlib import Path
import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, check_list_sorted, console
BUG_REPORT_TEMPLATE = AIRFLOW_ROOT_PATH / ".github" / "ISSUE_TEMPLATE" /
"3-airflow_providers_bug_report.yml"
diff --git a/scripts/ci/prek/check_airflow_imports.py
b/scripts/ci/prek/check_airflow_imports.py
index 9513cb06976..74d4097366d 100755
--- a/scripts/ci/prek/check_airflow_imports.py
+++ b/scripts/ci/prek/check_airflow_imports.py
@@ -29,7 +29,6 @@ import re
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import console, get_imports_from_file
diff --git a/scripts/ci/prek/check_airflow_imports_in_shared.py
b/scripts/ci/prek/check_airflow_imports_in_shared.py
index 19b974cd1fc..63dd94e090f 100755
--- a/scripts/ci/prek/check_airflow_imports_in_shared.py
+++ b/scripts/ci/prek/check_airflow_imports_in_shared.py
@@ -29,7 +29,6 @@ import ast
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console
diff --git a/scripts/ci/prek/check_airflow_v_imports_in_tests.py
b/scripts/ci/prek/check_airflow_v_imports_in_tests.py
index 43c6f1f7936..866da90e866 100755
--- a/scripts/ci/prek/check_airflow_v_imports_in_tests.py
+++ b/scripts/ci/prek/check_airflow_v_imports_in_tests.py
@@ -31,10 +31,7 @@ import ast
import sys
from pathlib import Path
-from common_prek_utils import AIRFLOW_ROOT_PATH
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
-from common_prek_utils import console
+from common_prek_utils import AIRFLOW_ROOT_PATH, console
def check_airflow_v_imports_and_fix(test_file: Path) -> list[str]:
diff --git a/scripts/ci/prek/check_airflowctl_command_coverage.py
b/scripts/ci/prek/check_airflowctl_command_coverage.py
index 8a81dd098c6..2b808db9c00 100755
--- a/scripts/ci/prek/check_airflowctl_command_coverage.py
+++ b/scripts/ci/prek/check_airflowctl_command_coverage.py
@@ -31,9 +31,7 @@ from __future__ import annotations
import ast
import re
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, console
OPERATIONS_FILE = AIRFLOW_ROOT_PATH / "airflow-ctl" / "src" / "airflowctl" /
"api" / "operations.py"
diff --git a/scripts/ci/prek/check_base_operator_partial_arguments.py
b/scripts/ci/prek/check_base_operator_partial_arguments.py
index 6c6201e0134..ee2c7639f08 100755
--- a/scripts/ci/prek/check_base_operator_partial_arguments.py
+++ b/scripts/ci/prek/check_base_operator_partial_arguments.py
@@ -26,11 +26,9 @@ from __future__ import annotations
import ast
import itertools
-import pathlib
import sys
import typing
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_TASK_SDK_SOURCES_PATH, console
SDK_BASEOPERATOR_PY = AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" /
"bases" / "operator.py"
diff --git a/scripts/ci/prek/check_cli_definition_imports.py
b/scripts/ci/prek/check_cli_definition_imports.py
index ba14b8e2f71..c8a9d1730af 100755
--- a/scripts/ci/prek/check_cli_definition_imports.py
+++ b/scripts/ci/prek/check_cli_definition_imports.py
@@ -36,7 +36,6 @@ import argparse
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import console, get_imports_from_file
# Allowed modules that can be imported in CLI definition files
diff --git a/scripts/ci/prek/check_common_sql_dependency.py
b/scripts/ci/prek/check_common_sql_dependency.py
index a59445a0a15..4677464da10 100755
--- a/scripts/ci/prek/check_common_sql_dependency.py
+++ b/scripts/ci/prek/check_common_sql_dependency.py
@@ -32,12 +32,10 @@ import sys
from collections.abc import Iterable
import yaml
+from common_prek_utils import get_provider_base_dir_from_path
from packaging.specifiers import SpecifierSet
from rich.console import Console
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))
-from common_prek_utils import get_provider_base_dir_from_path
-
console = Console(color_system="standard", width=200)
diff --git a/scripts/ci/prek/check_core_imports_in_sdk.py
b/scripts/ci/prek/check_core_imports_in_sdk.py
index 393ae5a4c7b..700d81f5ea1 100755
--- a/scripts/ci/prek/check_core_imports_in_sdk.py
+++ b/scripts/ci/prek/check_core_imports_in_sdk.py
@@ -29,7 +29,6 @@ import ast
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console
diff --git a/scripts/ci/prek/check_core_imports_in_shared.py
b/scripts/ci/prek/check_core_imports_in_shared.py
index f7b16153410..738d2a8ba5b 100644
--- a/scripts/ci/prek/check_core_imports_in_shared.py
+++ b/scripts/ci/prek/check_core_imports_in_shared.py
@@ -29,7 +29,6 @@ import ast
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console
diff --git a/scripts/ci/prek/check_default_configuration.py
b/scripts/ci/prek/check_default_configuration.py
index 7288a7a0644..17e7731201f 100755
--- a/scripts/ci/prek/check_default_configuration.py
+++ b/scripts/ci/prek/check_default_configuration.py
@@ -23,10 +23,6 @@
# ///
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_execution_api_versions.py
b/scripts/ci/prek/check_execution_api_versions.py
index 6ce1f5b7644..749f4020e02 100755
--- a/scripts/ci/prek/check_execution_api_versions.py
+++ b/scripts/ci/prek/check_execution_api_versions.py
@@ -26,9 +26,7 @@ from __future__ import annotations
import os
import subprocess
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import console
DATAMODELS_PREFIX =
"airflow-core/src/airflow/api_fastapi/execution_api/datamodels/"
diff --git a/scripts/ci/prek/check_extra_packages_ref.py
b/scripts/ci/prek/check_extra_packages_ref.py
index 004e3b4699a..6e3bd8bcd6b 100755
--- a/scripts/ci/prek/check_extra_packages_ref.py
+++ b/scripts/ci/prek/check_extra_packages_ref.py
@@ -32,9 +32,8 @@ from __future__ import annotations
import re
import sys
-from pathlib import Path
-from common_prek_utils import AIRFLOW_ROOT_PATH
+from common_prek_utils import AIRFLOW_ROOT_PATH, console
from tabulate import tabulate
try:
@@ -42,16 +41,9 @@ try:
except ImportError:
import tomli as tomllib
-
-COMMON_PREK_PATH = Path(__file__).parent.resolve()
EXTRA_PACKAGES_REF_FILE = AIRFLOW_ROOT_PATH / "airflow-core" / "docs" /
"extra-packages-ref.rst"
PYPROJECT_TOML_FILE_PATH = AIRFLOW_ROOT_PATH / "pyproject.toml"
-sys.path.insert(0, COMMON_PREK_PATH.as_posix()) # make sure common_prek_utils
is imported
-from common_prek_utils import console
-
-sys.path.insert(0, AIRFLOW_ROOT_PATH.as_posix()) # make sure airflow root is
imported
-
doc_ref_content = EXTRA_PACKAGES_REF_FILE.read_text()
errors: list[str] = []
diff --git a/scripts/ci/prek/check_i18n_json.py
b/scripts/ci/prek/check_i18n_json.py
index e093632c2e1..79aa43a8fc7 100755
--- a/scripts/ci/prek/check_i18n_json.py
+++ b/scripts/ci/prek/check_i18n_json.py
@@ -33,9 +33,6 @@ import json
import sys
from pathlib import Path
-COMMON_PREK_PATH = Path(__file__).parent.resolve()
-
-sys.path.insert(0, COMMON_PREK_PATH.as_posix()) # make sure common_prek_utils
is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, console
LOCALES_DIR = AIRFLOW_ROOT_PATH / "airflow-core" / "src" / "airflow" / "ui" /
"public" / "i18n" / "locales"
diff --git a/scripts/ci/prek/check_imports_in_providers.py
b/scripts/ci/prek/check_imports_in_providers.py
index 113e4479321..dbd533e2db2 100755
--- a/scripts/ci/prek/check_imports_in_providers.py
+++ b/scripts/ci/prek/check_imports_in_providers.py
@@ -24,10 +24,6 @@
# ///
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_init_decorator_arguments.py
b/scripts/ci/prek/check_init_decorator_arguments.py
index 3032a06a21f..683fca120a7 100755
--- a/scripts/ci/prek/check_init_decorator_arguments.py
+++ b/scripts/ci/prek/check_init_decorator_arguments.py
@@ -28,11 +28,9 @@ from __future__ import annotations
import ast
import collections.abc
import itertools
-import pathlib
import sys
from typing import TYPE_CHECKING
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_TASK_SDK_SOURCES_PATH, console
SDK_DEFINITIONS_PKG = AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" /
"definitions"
diff --git a/scripts/ci/prek/check_integrations_list.py
b/scripts/ci/prek/check_integrations_list.py
index bf42d3f76b2..161049175ca 100755
--- a/scripts/ci/prek/check_integrations_list.py
+++ b/scripts/ci/prek/check_integrations_list.py
@@ -41,7 +41,6 @@ from typing import Any
import yaml
# make sure common_prek_utils is imported
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
AIRFLOW_ROOT_PATH,
console,
diff --git a/scripts/ci/prek/check_k8s_schemas_published.py
b/scripts/ci/prek/check_k8s_schemas_published.py
index 53648fb4aa5..c7ec8f36f12 100755
--- a/scripts/ci/prek/check_k8s_schemas_published.py
+++ b/scripts/ci/prek/check_k8s_schemas_published.py
@@ -26,11 +26,9 @@ If any version returns non-200 the hook fails with
instructions.
from __future__ import annotations
import sys
-from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console, read_allowed_kubernetes_versions
PROBE_URL_TEMPLATE =
"https://airflow.apache.org/k8s-schemas/v{version}-standalone-strict/configmap-v1.json"
diff --git a/scripts/ci/prek/check_kubeconform.py
b/scripts/ci/prek/check_kubeconform.py
index 2162ff6139d..18b03aa6aef 100755
--- a/scripts/ci/prek/check_kubeconform.py
+++ b/scripts/ci/prek/check_kubeconform.py
@@ -28,9 +28,7 @@ from __future__ import annotations
import os
import subprocess
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, console,
initialize_breeze_prek
initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/check_min_python_version.py
b/scripts/ci/prek/check_min_python_version.py
index 809fc37546f..adaba0c81a3 100755
--- a/scripts/ci/prek/check_min_python_version.py
+++ b/scripts/ci/prek/check_min_python_version.py
@@ -25,9 +25,6 @@ from __future__ import annotations
import subprocess
import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console
diff --git a/scripts/ci/prek/check_order_dockerfile_extras.py
b/scripts/ci/prek/check_order_dockerfile_extras.py
index e239dec08ae..a5202449e92 100755
--- a/scripts/ci/prek/check_order_dockerfile_extras.py
+++ b/scripts/ci/prek/check_order_dockerfile_extras.py
@@ -31,10 +31,8 @@ from __future__ import annotations
import sys
from pathlib import Path
-from rich import print
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, check_list_sorted
+from rich import print
errors: list[str] = []
diff --git a/scripts/ci/prek/check_provider_docs.py
b/scripts/ci/prek/check_provider_docs.py
index 1255e3be422..fb41ad4c191 100755
--- a/scripts/ci/prek/check_provider_docs.py
+++ b/scripts/ci/prek/check_provider_docs.py
@@ -29,16 +29,13 @@ import sys
from collections import defaultdict
from pathlib import Path
-from rich.console import Console
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure common
utils are importable
-
from common_prek_utils import (
AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_PROVIDERS_ROOT_PATH,
AIRFLOW_ROOT_PATH,
get_all_provider_info_dicts,
)
+from rich.console import Console
sys.path.insert(0, str(AIRFLOW_CORE_SOURCES_PATH)) # make sure setup is
imported from Airflow
diff --git a/scripts/ci/prek/check_provider_yaml_files.py
b/scripts/ci/prek/check_provider_yaml_files.py
index 7348d4f0bb1..46f5e942ad4 100755
--- a/scripts/ci/prek/check_provider_yaml_files.py
+++ b/scripts/ci/prek/check_provider_yaml_files.py
@@ -24,9 +24,7 @@
from __future__ import annotations
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_providers_subpackages_all_have_init.py
b/scripts/ci/prek/check_providers_subpackages_all_have_init.py
index b50b3485cb2..c1511f21319 100755
--- a/scripts/ci/prek/check_providers_subpackages_all_have_init.py
+++ b/scripts/ci/prek/check_providers_subpackages_all_have_init.py
@@ -27,7 +27,6 @@ import os
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import (
AIRFLOW_PROVIDERS_ROOT_PATH,
AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/check_revision_heads_map.py
b/scripts/ci/prek/check_revision_heads_map.py
index 02a87c7e56b..a223d07e740 100755
--- a/scripts/ci/prek/check_revision_heads_map.py
+++ b/scripts/ci/prek/check_revision_heads_map.py
@@ -28,9 +28,7 @@ from __future__ import annotations
import os
import re
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_PROVIDERS_ROOT_PATH, console
DB_FILE = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "utils" / "db.py"
diff --git a/scripts/ci/prek/check_schema_defaults.py
b/scripts/ci/prek/check_schema_defaults.py
index 3a1fb75cfbe..dcfa4b462f5 100755
--- a/scripts/ci/prek/check_schema_defaults.py
+++ b/scripts/ci/prek/check_schema_defaults.py
@@ -23,10 +23,6 @@
# ///
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_sdk_imports.py
b/scripts/ci/prek/check_sdk_imports.py
index f266fd3480e..f900390695f 100755
--- a/scripts/ci/prek/check_sdk_imports.py
+++ b/scripts/ci/prek/check_sdk_imports.py
@@ -29,7 +29,6 @@ import ast
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console
diff --git a/scripts/ci/prek/check_shared_distributions_structure.py
b/scripts/ci/prek/check_shared_distributions_structure.py
index 41a9fb8112e..ac08c28924b 100755
--- a/scripts/ci/prek/check_shared_distributions_structure.py
+++ b/scripts/ci/prek/check_shared_distributions_structure.py
@@ -37,7 +37,6 @@ try:
except ImportError:
import tomli as tomllib
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, console
SHARED_DIR = AIRFLOW_ROOT_PATH / "shared"
diff --git a/scripts/ci/prek/check_shared_distributions_usage.py
b/scripts/ci/prek/check_shared_distributions_usage.py
index c9cd41e92dc..c5759d76d73 100755
--- a/scripts/ci/prek/check_shared_distributions_usage.py
+++ b/scripts/ci/prek/check_shared_distributions_usage.py
@@ -41,7 +41,6 @@ try:
except ImportError:
import tomli as tomllib
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # for
common_prek_utils import
from common_prek_utils import AIRFLOW_ROOT_PATH, console, insert_documentation
SHARED_DIR = AIRFLOW_ROOT_PATH / "shared"
diff --git a/scripts/ci/prek/check_system_tests_hidden_in_index.py
b/scripts/ci/prek/check_system_tests_hidden_in_index.py
index cbb81c7435e..65edabe155d 100755
--- a/scripts/ci/prek/check_system_tests_hidden_in_index.py
+++ b/scripts/ci/prek/check_system_tests_hidden_in_index.py
@@ -35,7 +35,6 @@ if __name__ not in ("__main__", "__mp_main__"):
)
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_PROVIDERS_ROOT_PATH, console
errors: list[Any] = []
diff --git a/scripts/ci/prek/check_template_context_variable_in_sync.py
b/scripts/ci/prek/check_template_context_variable_in_sync.py
index 61f4445f274..a6fdef35f28 100755
--- a/scripts/ci/prek/check_template_context_variable_in_sync.py
+++ b/scripts/ci/prek/check_template_context_variable_in_sync.py
@@ -26,13 +26,10 @@
from __future__ import annotations
import ast
-import pathlib
import re
import sys
import typing
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
-
from common_prek_utils import AIRFLOW_CORE_ROOT_PATH,
AIRFLOW_TASK_SDK_SOURCES_PATH
TASKRUNNER_PY = AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" /
"execution_time" / "task_runner.py"
diff --git a/scripts/ci/prek/check_template_fields.py
b/scripts/ci/prek/check_template_fields.py
index deed0d25900..fb68c8dec22 100755
--- a/scripts/ci/prek/check_template_fields.py
+++ b/scripts/ci/prek/check_template_fields.py
@@ -24,9 +24,7 @@
from __future__ import annotations
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_test_only_imports_in_src.py
b/scripts/ci/prek/check_test_only_imports_in_src.py
index 848cd82df94..0670409a18f 100755
--- a/scripts/ci/prek/check_test_only_imports_in_src.py
+++ b/scripts/ci/prek/check_test_only_imports_in_src.py
@@ -42,7 +42,6 @@ import re
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console
# Top-level modules that are dev-only and must never be imported at runtime.
diff --git a/scripts/ci/prek/check_tests_in_right_folders.py
b/scripts/ci/prek/check_tests_in_right_folders.py
index 1756e61474e..1a2bf5b53bc 100755
--- a/scripts/ci/prek/check_tests_in_right_folders.py
+++ b/scripts/ci/prek/check_tests_in_right_folders.py
@@ -26,9 +26,7 @@ from __future__ import annotations
import re
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console, initialize_breeze_prek
initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/check_ti_vs_tis_attributes.py
b/scripts/ci/prek/check_ti_vs_tis_attributes.py
index bca2d521e49..3ae595c7398 100755
--- a/scripts/ci/prek/check_ti_vs_tis_attributes.py
+++ b/scripts/ci/prek/check_ti_vs_tis_attributes.py
@@ -25,9 +25,7 @@ from __future__ import annotations
import ast
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, console
TI_PATH = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "models" / "taskinstance.py"
diff --git a/scripts/ci/prek/check_version_consistency.py
b/scripts/ci/prek/check_version_consistency.py
index c7bfaee9450..b068cd0c49d 100755
--- a/scripts/ci/prek/check_version_consistency.py
+++ b/scripts/ci/prek/check_version_consistency.py
@@ -28,24 +28,20 @@ from __future__ import annotations
import ast
import re
import sys
-from pathlib import Path
try:
import tomllib
except ImportError:
import tomli as tomllib
-from packaging.specifiers import SpecifierSet
-from packaging.version import Version
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
from common_prek_utils import (
AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_ROOT_PATH,
AIRFLOW_TASK_SDK_SOURCES_PATH,
console,
)
+from packaging.specifiers import SpecifierSet
+from packaging.version import Version
def read_airflow_version() -> str:
diff --git a/scripts/ci/prek/common_prek_utils.py
b/scripts/ci/prek/common_prek_utils.py
index 5a9024e00ac..4bfb9b09ac2 100644
--- a/scripts/ci/prek/common_prek_utils.py
+++ b/scripts/ci/prek/common_prek_utils.py
@@ -133,7 +133,7 @@ def read_allowed_kubernetes_versions() -> list[str]:
raise RuntimeError("ALLOWED_KUBERNETES_VERSIONS not found in
global_constants.py")
-def pre_process_files(files: list[str]) -> list[str]:
+def pre_process_mypy_files(files: list[str]) -> list[str]:
"""Pre-process files passed to mypy.
* Exclude conftest.py files and __init__.py files
diff --git a/scripts/ci/prek/compile_ui_assets.py
b/scripts/ci/prek/compile_ui_assets.py
index d9c2ba4f32c..054845fe78d 100755
--- a/scripts/ci/prek/compile_ui_assets.py
+++ b/scripts/ci/prek/compile_ui_assets.py
@@ -29,8 +29,6 @@ from pathlib import Path
# Cannot have additional Python dependencies installed. We should not import
any of the libraries
# here that are not available in stdlib! You should not import
common_prek_utils.py here because
# They are importing the rich library which is not available in the node
environment.
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_ROOT_PATH
MAIN_UI_DIRECTORY = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "ui"
diff --git a/scripts/ci/prek/compile_ui_assets_dev.py
b/scripts/ci/prek/compile_ui_assets_dev.py
index 903e7f5e98d..8f7760f0bc8 100755
--- a/scripts/ci/prek/compile_ui_assets_dev.py
+++ b/scripts/ci/prek/compile_ui_assets_dev.py
@@ -20,10 +20,7 @@ from __future__ import annotations
import os
import signal
import subprocess
-import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_ROOT_PATH
# NOTE!. This script is executed from a node environment created by a prek
hook, and this environment
diff --git a/scripts/ci/prek/download_k8s_schemas.py
b/scripts/ci/prek/download_k8s_schemas.py
index 60069ba9e65..2576d5d0754 100755
--- a/scripts/ci/prek/download_k8s_schemas.py
+++ b/scripts/ci/prek/download_k8s_schemas.py
@@ -38,14 +38,11 @@ from __future__ import annotations
import argparse
import json
import subprocess
-import sys
from pathlib import Path
from tempfile import NamedTemporaryFile
import requests
import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, console,
read_allowed_kubernetes_versions
KUBERNETES_VERSIONS = read_allowed_kubernetes_versions()
diff --git a/scripts/ci/prek/generate_airflow_diagrams.py
b/scripts/ci/prek/generate_airflow_diagrams.py
index 87c69839e67..cbdecb5a9bf 100755
--- a/scripts/ci/prek/generate_airflow_diagrams.py
+++ b/scripts/ci/prek/generate_airflow_diagrams.py
@@ -30,7 +30,6 @@ import subprocess
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import console
diff --git a/scripts/ci/prek/generate_openapi_spec.py
b/scripts/ci/prek/generate_openapi_spec.py
index be28facec19..b6ddcb64aed 100755
--- a/scripts/ci/prek/generate_openapi_spec.py
+++ b/scripts/ci/prek/generate_openapi_spec.py
@@ -23,10 +23,6 @@
# ///
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/generate_openapi_spec_providers.py
b/scripts/ci/prek/generate_openapi_spec_providers.py
index 4cd8382fd9e..05424307edd 100755
--- a/scripts/ci/prek/generate_openapi_spec_providers.py
+++ b/scripts/ci/prek/generate_openapi_spec_providers.py
@@ -24,9 +24,7 @@
from __future__ import annotations
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/generate_volumes_for_sources.py
b/scripts/ci/prek/generate_volumes_for_sources.py
index c1921fa8488..1c4bb2daa9b 100755
--- a/scripts/ci/prek/generate_volumes_for_sources.py
+++ b/scripts/ci/prek/generate_volumes_for_sources.py
@@ -24,10 +24,6 @@
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, get_all_provider_ids,
insert_documentation
START_MARKER = " # START automatically generated volumes by
generate-volumes-for-sources prek hook"
diff --git a/scripts/ci/prek/lint_helm.py b/scripts/ci/prek/lint_helm.py
index 789e04cccf9..4235398a503 100755
--- a/scripts/ci/prek/lint_helm.py
+++ b/scripts/ci/prek/lint_helm.py
@@ -28,9 +28,7 @@ from __future__ import annotations
import os
import subprocess
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, console,
initialize_breeze_prek
initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/lint_json_schema.py
b/scripts/ci/prek/lint_json_schema.py
index 43b6c7e2f19..ab6dc8bff37 100755
--- a/scripts/ci/prek/lint_json_schema.py
+++ b/scripts/ci/prek/lint_json_schema.py
@@ -32,7 +32,6 @@ import json
import os
import re
import sys
-from pathlib import Path
import requests
import yaml
@@ -45,7 +44,6 @@ if __name__ != "__main__":
"To run this script, run the ./build_docs.py command"
)
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH
diff --git a/scripts/ci/prek/local_yml_mounts.py
b/scripts/ci/prek/local_yml_mounts.py
index dcbca62b0e8..5f699e56599 100755
--- a/scripts/ci/prek/local_yml_mounts.py
+++ b/scripts/ci/prek/local_yml_mounts.py
@@ -18,10 +18,7 @@
from __future__ import annotations
import subprocess
-import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console, initialize_breeze_prek
initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/migration_reference.py
b/scripts/ci/prek/migration_reference.py
index 27a1a130b3d..c4dfe5e0c85 100755
--- a/scripts/ci/prek/migration_reference.py
+++ b/scripts/ci/prek/migration_reference.py
@@ -23,10 +23,6 @@
# ///
from __future__ import annotations
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
initialize_breeze_prek,
run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/mypy.py b/scripts/ci/prek/mypy.py
index 8405840eaff..98fe89c0ea2 100755
--- a/scripts/ci/prek/mypy.py
+++ b/scripts/ci/prek/mypy.py
@@ -28,19 +28,17 @@ import shlex
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
from common_prek_utils import (
AIRFLOW_ROOT_PATH,
console,
initialize_breeze_prek,
- pre_process_files,
+ pre_process_mypy_files,
run_command_via_breeze_shell,
)
initialize_breeze_prek(__name__, __file__)
-files_to_test = pre_process_files(sys.argv[1:])
+files_to_test = pre_process_mypy_files(sys.argv[1:])
if not files_to_test:
print("No files to tests. Quitting")
sys.exit(0)
diff --git a/scripts/ci/prek/mypy_folder.py b/scripts/ci/prek/mypy_folder.py
index a84139dfedb..c4e422450c0 100755
--- a/scripts/ci/prek/mypy_folder.py
+++ b/scripts/ci/prek/mypy_folder.py
@@ -27,9 +27,6 @@ import os
import re
import shlex
import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import (
AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/newsfragments.py b/scripts/ci/prek/newsfragments.py
index ecd03a6d8e3..cb994b44c13 100755
--- a/scripts/ci/prek/newsfragments.py
+++ b/scripts/ci/prek/newsfragments.py
@@ -28,42 +28,53 @@ from pathlib import Path
VALID_CHANGE_TYPES = {"significant", "feature", "improvement", "bugfix",
"doc", "misc"}
-files = sys.argv[1:]
-failed = False
-for filename in files:
- with open(filename) as f:
- lines = [line.strip() for line in f.readlines()]
+def validate_newsfragment(filename: str, lines: list[str]) -> list[str]:
+ """Validate a single newsfragment file. Returns a list of error
messages."""
+ errors: list[str] = []
num_lines = len(lines)
name_parts = Path(filename).name.split(".")
if len(name_parts) != 3:
- print(f"Newsfragment {filename} has an unexpected filename. Should be
{{pr_number}}.{{type}}.rst.")
- failed = True
- continue
+ errors.append(
+ f"Newsfragment {filename} has an unexpected filename. Should be
{{pr_number}}.{{type}}.rst."
+ )
+ return errors
change_type = name_parts[1]
if change_type not in VALID_CHANGE_TYPES:
- print(f"Newsfragment {filename} has an unexpected type. Should be one
of {VALID_CHANGE_TYPES}.")
- failed = True
- continue
+ errors.append(
+ f"Newsfragment {filename} has an unexpected type. Should be one of
{VALID_CHANGE_TYPES}."
+ )
+ return errors
if change_type != "significant":
if num_lines != 1:
- print(f"Newsfragment {filename} can only have a single line.")
- failed = True
+ errors.append(f"Newsfragment {filename} can only have a single
line.")
else:
# significant newsfragment
if num_lines == 1:
- continue
- if num_lines == 2:
- print(f"Newsfragment {filename} can have 1, or 3+ lines.")
- failed = True
- continue
- if lines[1] != "":
- print(f"Newsfragment {filename} must have an empty second line.")
+ pass # OK
+ elif num_lines == 2:
+ errors.append(f"Newsfragment {filename} can have 1, or 3+ lines.")
+ elif lines[1] != "":
+ errors.append(f"Newsfragment {filename} must have an empty second
line.")
+
+ return errors
+
+
+def main(filenames: list[str]) -> int:
+ failed = False
+ for filename in filenames:
+ with open(filename) as f:
+ lines = [line.strip() for line in f.readlines()]
+ errors = validate_newsfragment(filename, lines)
+ for error in errors:
+ print(error)
+ if errors:
failed = True
- continue
+ return 1 if failed else 0
+
-if failed:
- sys.exit(1)
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/scripts/ci/prek/sync_translation_namespaces.py
b/scripts/ci/prek/sync_translation_namespaces.py
index 192aab49fa2..1dd9df6eb61 100755
--- a/scripts/ci/prek/sync_translation_namespaces.py
+++ b/scripts/ci/prek/sync_translation_namespaces.py
@@ -20,9 +20,7 @@
from __future__ import annotations
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, insert_documentation
EN_LOCALE_DIR = (
diff --git a/scripts/ci/prek/ts_compile_lint_common_ai.py
b/scripts/ci/prek/ts_compile_lint_common_ai.py
index e47632428b9..ae0032feb31 100755
--- a/scripts/ci/prek/ts_compile_lint_common_ai.py
+++ b/scripts/ci/prek/ts_compile_lint_common_ai.py
@@ -20,7 +20,6 @@ from __future__ import annotations
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import (
AIRFLOW_PROVIDERS_ROOT_PATH,
AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/ts_compile_lint_edge.py
b/scripts/ci/prek/ts_compile_lint_edge.py
index 8dfd847128f..229b890da18 100755
--- a/scripts/ci/prek/ts_compile_lint_edge.py
+++ b/scripts/ci/prek/ts_compile_lint_edge.py
@@ -20,7 +20,6 @@ from __future__ import annotations
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import (
AIRFLOW_PROVIDERS_ROOT_PATH,
AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
b/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
index 697d6675225..1a3727bc910 100755
--- a/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
+++ b/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
@@ -20,7 +20,6 @@ from __future__ import annotations
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import (
AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/ts_compile_lint_ui.py
b/scripts/ci/prek/ts_compile_lint_ui.py
index b6fdd783788..17514f9be22 100755
--- a/scripts/ci/prek/ts_compile_lint_ui.py
+++ b/scripts/ci/prek/ts_compile_lint_ui.py
@@ -20,7 +20,6 @@ from __future__ import annotations
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import (
AIRFLOW_CORE_ROOT_PATH,
AIRFLOW_CORE_SOURCES_PATH,
diff --git a/scripts/ci/prek/update_airflow_pyproject_toml.py
b/scripts/ci/prek/update_airflow_pyproject_toml.py
index 924b183f2e9..cccae46867e 100755
--- a/scripts/ci/prek/update_airflow_pyproject_toml.py
+++ b/scripts/ci/prek/update_airflow_pyproject_toml.py
@@ -37,10 +37,8 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
-from packaging.version import Version, parse as parse_version
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, console,
get_all_provider_ids, insert_documentation
+from packaging.version import Version, parse as parse_version
AIRFLOW_PYPROJECT_TOML_FILE = AIRFLOW_ROOT_PATH / "pyproject.toml"
AIRFLOW_CORE_ROOT_PATH = AIRFLOW_ROOT_PATH / "airflow-core"
diff --git a/scripts/ci/prek/update_chart_dependencies.py
b/scripts/ci/prek/update_chart_dependencies.py
index daf01404002..d65642bbac2 100755
--- a/scripts/ci/prek/update_chart_dependencies.py
+++ b/scripts/ci/prek/update_chart_dependencies.py
@@ -27,12 +27,9 @@ from __future__ import annotations
import json
import sys
-from pathlib import Path
import requests
import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_ROOT_PATH, console
VALUES_YAML_FILE = AIRFLOW_ROOT_PATH / "chart" / "values.yaml"
diff --git a/scripts/ci/prek/update_example_dags_paths.py
b/scripts/ci/prek/update_example_dags_paths.py
index fe22ca9889a..6f4e5bd1dcc 100755
--- a/scripts/ci/prek/update_example_dags_paths.py
+++ b/scripts/ci/prek/update_example_dags_paths.py
@@ -37,7 +37,6 @@ if __name__ not in ("__main__", "__mp_main__"):
)
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_PROVIDERS_ROOT_PATH, console
EXAMPLE_DAGS_URL_MATCHER = re.compile(
diff --git a/scripts/ci/prek/update_providers_build_files.py
b/scripts/ci/prek/update_providers_build_files.py
index cc0b42a668a..37159909917 100755
--- a/scripts/ci/prek/update_providers_build_files.py
+++ b/scripts/ci/prek/update_providers_build_files.py
@@ -30,7 +30,6 @@ from pathlib import Path
AIRFLOW_ROOT_PATH = Path(__file__).parents[3].resolve()
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import console, initialize_breeze_prek
initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/update_providers_dependencies.py
b/scripts/ci/prek/update_providers_dependencies.py
index af52f2975f0..9e89084f7b0 100755
--- a/scripts/ci/prek/update_providers_dependencies.py
+++ b/scripts/ci/prek/update_providers_dependencies.py
@@ -33,8 +33,6 @@ from pathlib import Path
from typing import Any
import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import (
AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_PROVIDERS_ROOT_PATH,
diff --git a/scripts/ci/prek/update_source_date_epoch.py
b/scripts/ci/prek/update_source_date_epoch.py
index cbb040bb403..97bb1c74821 100755
--- a/scripts/ci/prek/update_source_date_epoch.py
+++ b/scripts/ci/prek/update_source_date_epoch.py
@@ -24,15 +24,11 @@
# ///
from __future__ import annotations
-import sys
from hashlib import md5
from pathlib import Path
from time import time
import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is importable
-
from common_prek_utils import AIRFLOW_ROOT_PATH
CHART_DIR = AIRFLOW_ROOT_PATH / "chart"
diff --git a/scripts/ci/prek/update_versions.py
b/scripts/ci/prek/update_versions.py
index 4207f0f66d5..fe2c2fd30aa 100755
--- a/scripts/ci/prek/update_versions.py
+++ b/scripts/ci/prek/update_versions.py
@@ -24,11 +24,8 @@
from __future__ import annotations
import re
-import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is importable
-
from common_prek_utils import AIRFLOW_ROOT_PATH, read_airflow_version
diff --git a/scripts/ci/prek/upgrade_important_versions.py
b/scripts/ci/prek/upgrade_important_versions.py
index 4a90c3c3d1b..8041de4c891 100755
--- a/scripts/ci/prek/upgrade_important_versions.py
+++ b/scripts/ci/prek/upgrade_important_versions.py
@@ -41,10 +41,8 @@ from enum import Enum
from pathlib import Path
import requests
-from packaging.version import Version
-
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
from common_prek_utils import AIRFLOW_CORE_ROOT_PATH, AIRFLOW_ROOT_PATH,
console, retrieve_gh_token
+from packaging.version import Version
DOCKER_IMAGES_EXAMPLE_DIR_PATH = AIRFLOW_ROOT_PATH / "docker-stack-docs" /
"docker-examples"
diff --git a/scripts/ci/prek/validate_chart_annotations.py
b/scripts/ci/prek/validate_chart_annotations.py
index 20d16474ed7..f7671cac267 100755
--- a/scripts/ci/prek/validate_chart_annotations.py
+++ b/scripts/ci/prek/validate_chart_annotations.py
@@ -26,11 +26,8 @@
from __future__ import annotations
import sys
-from pathlib import Path
import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from common_prek_utils import AIRFLOW_ROOT_PATH, console
CHART_YAML_FILE = AIRFLOW_ROOT_PATH / "chart" / "Chart.yaml"
diff --git a/scripts/cov/cli_coverage.py b/scripts/cov/cli_coverage.py
index 6c235245806..0dbaa9495b9 100644
--- a/scripts/cov/cli_coverage.py
+++ b/scripts/cov/cli_coverage.py
@@ -16,13 +16,8 @@
# under the License.
from __future__ import annotations
-import sys
-from pathlib import Path
-
from cov_runner import run_tests
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
source_files = ["airflow-core/src/airflow/cli"]
cli_files = ["airflow-core/tests/unit/cli"]
diff --git a/scripts/cov/core_coverage.py b/scripts/cov/core_coverage.py
index 2b5aff9fcf9..2d812fef533 100644
--- a/scripts/cov/core_coverage.py
+++ b/scripts/cov/core_coverage.py
@@ -16,13 +16,8 @@
# under the License.
from __future__ import annotations
-import sys
-from pathlib import Path
-
from cov_runner import run_tests
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
source_files = [
"airflow-core/src/airflow/executors",
"airflow-core/src/airflow/jobs",
diff --git a/scripts/cov/other_coverage.py b/scripts/cov/other_coverage.py
index 914f3b047d8..4b648a2e1a8 100644
--- a/scripts/cov/other_coverage.py
+++ b/scripts/cov/other_coverage.py
@@ -16,13 +16,8 @@
# under the License.
from __future__ import annotations
-import sys
-from pathlib import Path
-
from cov_runner import run_tests
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
source_files = [
"airflow-core/src/airflow/dag_processing",
"airflow-core/src/airflow/triggers",
diff --git a/scripts/cov/restapi_coverage.py b/scripts/cov/restapi_coverage.py
index cc80db9241d..1478c066436 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/cov/restapi_coverage.py
@@ -16,13 +16,8 @@
# under the License.
from __future__ import annotations
-import sys
-from pathlib import Path
-
from cov_runner import run_tests
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
source_files = ["airflow-core/tests/unit/api_fastapi"]
files_not_fully_covered: list[str] = []
diff --git a/scripts/in_container/install_airflow_and_providers.py
b/scripts/in_container/install_airflow_and_providers.py
index 5337643f152..df7443271af 100755
--- a/scripts/in_container/install_airflow_and_providers.py
+++ b/scripts/in_container/install_airflow_and_providers.py
@@ -27,7 +27,6 @@ from functools import cache
from pathlib import Path
from typing import NamedTuple
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from in_container_utils import (
AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_DIST_PATH,
diff --git a/scripts/in_container/install_airflow_python_client.py
b/scripts/in_container/install_airflow_python_client.py
index 36657f8768a..c528b772067 100644
--- a/scripts/in_container/install_airflow_python_client.py
+++ b/scripts/in_container/install_airflow_python_client.py
@@ -19,9 +19,7 @@
from __future__ import annotations
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from in_container_utils import AIRFLOW_DIST_PATH, click, console, run_command
ALLOWED_DISTRIBUTION_FORMAT = ["wheel", "sdist", "both"]
diff --git a/scripts/in_container/install_development_dependencies.py
b/scripts/in_container/install_development_dependencies.py
index 6e14c60995d..ee5831b0199 100755
--- a/scripts/in_container/install_development_dependencies.py
+++ b/scripts/in_container/install_development_dependencies.py
@@ -30,9 +30,7 @@ from __future__ import annotations
import json
import sys
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from in_container_utils import AIRFLOW_ROOT_PATH, click, console, run_command
from packaging.requirements import Requirement
diff --git a/scripts/in_container/run_capture_airflowctl_help.py
b/scripts/in_container/run_capture_airflowctl_help.py
index b35472bffca..3fac46ebd56 100644
--- a/scripts/in_container/run_capture_airflowctl_help.py
+++ b/scripts/in_container/run_capture_airflowctl_help.py
@@ -28,11 +28,9 @@ from pathlib import Path
from airflowctl import __file__ as AIRFLOW_CTL_SRC_PATH
from rich.console import Console
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
AIRFLOW_CTL_ROOT_PATH = Path(AIRFLOW_CTL_SRC_PATH).parents[2]
AIRFLOW_CTL_SOURCES_PATH = AIRFLOW_CTL_ROOT_PATH / "src"
-sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure
common_prek_utils is imported
AIRFLOWCTL_IMAGES_PATH = AIRFLOW_CTL_ROOT_PATH / "docs" / "images"
HASH_FILE = AIRFLOW_CTL_ROOT_PATH / "docs" / "images" / "command_hashes.txt"
COMMANDS = [
diff --git a/scripts/in_container/run_check_imports_in_providers.py
b/scripts/in_container/run_check_imports_in_providers.py
index a946bba5c85..3e6d467a151 100755
--- a/scripts/in_container/run_check_imports_in_providers.py
+++ b/scripts/in_container/run_check_imports_in_providers.py
@@ -23,7 +23,6 @@ import subprocess
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from in_container_utils import console, get_provider_base_dir_from_path,
get_provider_id_from_path
diff --git a/scripts/in_container/run_generate_constraints.py
b/scripts/in_container/run_generate_constraints.py
index dc32ff03dc5..b22ade34305 100755
--- a/scripts/in_container/run_generate_constraints.py
+++ b/scripts/in_container/run_generate_constraints.py
@@ -28,8 +28,6 @@ from typing import TextIO
import requests
from click import Choice
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
from in_container_utils import AIRFLOW_DIST_PATH, AIRFLOW_ROOT_PATH, click,
console, run_command
DEFAULT_BRANCH = os.environ.get("DEFAULT_BRANCH", "main")
diff --git a/scripts/in_container/run_generate_openapi_spec.py
b/scripts/in_container/run_generate_openapi_spec.py
index cd8c0d5d4f4..1bc436e7002 100755
--- a/scripts/in_container/run_generate_openapi_spec.py
+++ b/scripts/in_container/run_generate_openapi_spec.py
@@ -21,14 +21,13 @@ import os
import sys
from pathlib import Path
+from in_container_utils import console, generate_openapi_file,
validate_openapi_file
+
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX, create_app
from airflow.api_fastapi.auth.managers.simple import __file__ as
SIMPLE_AUTH_MANAGER_PATH
from airflow.api_fastapi.auth.managers.simple.simple_auth_manager import
SimpleAuthManager
from airflow.api_fastapi.core_api import __file__ as CORE_API_PATH
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-from in_container_utils import console, generate_openapi_file,
validate_openapi_file
-
OPENAPI_SPEC_FILE = Path(CORE_API_PATH).parent / "openapi" /
"v2-rest-api-generated.yaml"
# We need a "combined" spec file to generate the UI code with, but we don't
want to include this in the repo
# nor in the rendered docs, so we make this a separate file which is gitignored
diff --git a/scripts/in_container/run_generate_openapi_spec_providers.py
b/scripts/in_container/run_generate_openapi_spec_providers.py
index 4461d6ebf73..f7ae0095eb5 100755
--- a/scripts/in_container/run_generate_openapi_spec_providers.py
+++ b/scripts/in_container/run_generate_openapi_spec_providers.py
@@ -36,7 +36,6 @@ class ProviderDef(NamedTuple):
prefix: str
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
ProvidersManager().initialize_providers_configuration()
diff --git a/scripts/in_container/run_provider_yaml_files_check.py
b/scripts/in_container/run_provider_yaml_files_check.py
index 138d640fd1d..7dbb485ca48 100755
--- a/scripts/in_container/run_provider_yaml_files_check.py
+++ b/scripts/in_container/run_provider_yaml_files_check.py
@@ -37,6 +37,11 @@ from typing import Any
import jsonschema
import yaml
+from in_container_utils import (
+ AIRFLOW_CORE_SOURCES_PATH,
+ AIRFLOW_PROVIDERS_PATH,
+ AIRFLOW_ROOT_PATH,
+)
from jsonpath_ng.ext import parse
from rich.console import Console
from tabulate import tabulate
@@ -45,13 +50,6 @@ from airflow.cli.commands.info_command import Architecture
from airflow.exceptions import AirflowOptionalProviderFeatureException,
AirflowProviderDeprecationWarning
from airflow.providers_manager import ProvidersManager
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))
-from in_container_utils import (
- AIRFLOW_CORE_SOURCES_PATH,
- AIRFLOW_PROVIDERS_PATH,
- AIRFLOW_ROOT_PATH,
-)
-
# Those are deprecated modules that contain removed Hooks/Sensors/Operators
that we left in the code
# so that users can get a very specific error message when they try to use
them.
diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml
new file mode 100644
index 00000000000..bbcc8749bd2
--- /dev/null
+++ b/scripts/pyproject.toml
@@ -0,0 +1,76 @@
+
+# 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.
+
+[build-system]
+requires = [
+ "hatchling==1.29.0",
+ "packaging==26.0",
+ "pathspec==1.0.4",
+ "pluggy==1.6.0",
+ "tomli==2.4.0; python_version < '3.11'",
+ "trove-classifiers==2026.1.14.14",
+]
+build-backend = "hatchling.build"
+
+[project]
+name = "apache-airflow-scripts"
+description = "Scripts and utilities for Apache Airflow CI, Docker, and
development"
+classifiers = [
+ "Private :: Do Not Upload",
+]
+requires-python = ">=3.10,!=3.14"
+authors = [
+ { name = "Apache Software Foundation", email = "[email protected]" },
+]
+maintainers = [
+ { name = "Apache Software Foundation", email = "[email protected]" },
+]
+version = "0.0.1"
+
+dependencies = [
+ "astor>=0.8.1",
+ "jsonschema>=4.19.1",
+ "libcst>=1.1.0",
+ "packaging>=25.0",
+ "python-dateutil>=2.8.2",
+ "pyyaml>=6.0.3",
+ "requests>=2.31.0",
+ "rich>=13.6.0",
+ "tabulate>=0.9.0",
+ "termcolor>=2.3.0",
+]
+
+[dependency-groups]
+dev = [
+ "apache-airflow-devel-common",
+]
+
+[tool.uv.sources]
+apache-airflow-devel-common = {workspace = true}
+
+[tool.hatch.build.targets.sdist]
+exclude = ["*"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["ci", "cov", "docker", "in_container", "tools"]
+
+[tool.pytest.ini_options]
+# "." makes ci.prek.* importable as packages; "ci/prek" makes bare
+# "from common_prek_utils import ..." inside those modules resolve correctly
+# (mirroring what Python does automatically when scripts are run directly).
+pythonpath = [".", "ci/prek"]
diff --git a/scripts/cov/restapi_coverage.py b/scripts/tests/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/tests/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/tests/__init__.py
@@ -14,19 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
- args = ["-qq"] + source_files
- run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/cov/restapi_coverage.py b/scripts/tests/ci/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/tests/ci/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/tests/ci/__init__.py
@@ -14,19 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
- args = ["-qq"] + source_files
- run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/cov/restapi_coverage.py b/scripts/tests/ci/prek/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/tests/ci/prek/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/tests/ci/prek/__init__.py
@@ -14,19 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
- args = ["-qq"] + source_files
- run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/tests/ci/prek/conftest.py
b/scripts/tests/ci/prek/conftest.py
new file mode 100644
index 00000000000..6ef372f3481
--- /dev/null
+++ b/scripts/tests/ci/prek/conftest.py
@@ -0,0 +1,76 @@
+# 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
+import yaml
+
+
[email protected]
+def write_python_file(tmp_path):
+ """Factory fixture: write dedented Python code to a temp .py file and
return its Path."""
+
+ def _write(code: str) -> Path:
+ path = tmp_path / "code.py"
+ path.write_text(textwrap.dedent(code))
+ return path
+
+ return _write
+
+
[email protected]
+def write_text_file(tmp_path):
+ """Factory fixture: write text content to a temp file and return its
Path."""
+
+ def _write(content: str) -> Path:
+ path = tmp_path / "content.txt"
+ path.write_text(content)
+ return path
+
+ return _write
+
+
[email protected]
+def write_workflow_file(tmp_path):
+ """Factory fixture: write a workflow dict as YAML to a temp file and
return its Path."""
+
+ def _write(content: dict) -> Path:
+ path = tmp_path / "workflow.yml"
+ path.write_text(yaml.dump(content))
+ return path
+
+ return _write
+
+
[email protected]
+def create_provider_tree(tmp_path):
+ """Factory fixture: create a directory tree with provider.yaml and return
a file path inside it."""
+
+ def _create(relative_path: str) -> Path:
+ provider_dir = tmp_path / relative_path
+ provider_dir.mkdir(parents=True, exist_ok=True)
+ (provider_dir / "provider.yaml").touch()
+ hooks_dir = provider_dir / "hooks"
+ hooks_dir.mkdir(exist_ok=True)
+ test_file = hooks_dir / "hook.py"
+ test_file.touch()
+ return test_file
+
+ return _create
diff --git a/scripts/tests/ci/prek/test_changelog_duplicates.py
b/scripts/tests/ci/prek/test_changelog_duplicates.py
new file mode 100644
index 00000000000..8cb097e3b28
--- /dev/null
+++ b/scripts/tests/ci/prek/test_changelog_duplicates.py
@@ -0,0 +1,101 @@
+# 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 pytest
+from ci.prek.changelog_duplicates import find_duplicates, known_exceptions,
pr_number_re
+
+
+class TestPrNumberRegex:
+ @pytest.mark.parametrize(
+ "line, expected_pr",
+ [
+ ("* Fix something (#12345)", "12345"),
+ ("* Fix something (#1)", "1"),
+ ("* Fix something (#123456)", "123456"),
+ ("Some change (#99999)`", "99999"),
+ ("Some change (#99999)``", "99999"),
+ ],
+ )
+ def test_matches_valid_pr_numbers(self, line, expected_pr):
+ match = pr_number_re.search(line)
+ assert match is not None
+ assert match.group(1) == expected_pr
+
+ @pytest.mark.parametrize(
+ "line",
+ [
+ "* Fix something without PR number",
+ "* Fix something (#1234567)", # 7 digits, too many
+ "* Fix something (#abc)",
+ "",
+ "Just some text",
+ ],
+ )
+ def test_no_match(self, line):
+ assert pr_number_re.search(line) is None
+
+
+class TestFindDuplicates:
+ def test_no_duplicates(self):
+ lines = [
+ "* Fix A (#1001)",
+ "* Fix B (#1002)",
+ "* Fix C (#1003)",
+ ]
+ assert find_duplicates(lines) == []
+
+ def test_with_duplicate(self):
+ lines = [
+ "* Fix A (#1001)",
+ "* Fix B (#1001)",
+ ]
+ assert find_duplicates(lines) == ["1001"]
+
+ def test_known_exception_not_reported(self):
+ lines = [
+ "* Fix A (#14738)",
+ "* Fix B (#14738)",
+ ]
+ assert find_duplicates(lines) == []
+
+ def test_mixed_lines(self):
+ lines = [
+ "# Changelog",
+ "",
+ "* Fix A (#1001)",
+ "Some description",
+ "* Fix B (#1002)",
+ ]
+ assert find_duplicates(lines) == []
+
+ def test_multiple_duplicates(self):
+ lines = [
+ "* Fix A (#1001)",
+ "* Fix B (#1002)",
+ "* Fix C (#1001)",
+ "* Fix D (#1002)",
+ ]
+ assert find_duplicates(lines) == ["1001", "1002"]
+
+ def test_empty_input(self):
+ assert find_duplicates([]) == []
+
+ def test_all_known_exceptions_are_strings(self):
+ for exc in known_exceptions:
+ assert isinstance(exc, str)
+ assert exc.isdigit()
diff --git a/scripts/tests/ci/prek/test_check_deprecations.py
b/scripts/tests/ci/prek/test_check_deprecations.py
new file mode 100644
index 00000000000..e94e3c9854c
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_deprecations.py
@@ -0,0 +1,206 @@
+# 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 ast
+
+from ci.prek.check_deprecations import (
+ built_import,
+ built_import_from,
+ found_compatible_decorators,
+ get_decorator_argument,
+ is_file_under_eol_deprecation,
+ resolve_decorator_name,
+ resolve_name,
+)
+
+GOOGLE_BIGQUERY_HOOK_PATH = "airflow/providers/google/cloud/hooks/bigquery.py"
+AIRFLOW_PROVIDER_DEPRECATION_WARNING = "AirflowProviderDeprecationWarning"
+
+
+class TestResolveName:
+ def test_simple_name(self):
+ expr = ast.parse("foo", mode="eval").body
+ assert resolve_name(expr) == "foo"
+
+ def test_attribute(self):
+ expr = ast.parse("foo.bar", mode="eval").body
+ assert resolve_name(expr) == "foo.bar"
+
+ def test_nested_attribute(self):
+ expr = ast.parse("foo.bar.baz", mode="eval").body
+ assert resolve_name(expr) == "foo.bar.baz"
+
+
+class TestResolveDecoratorName:
+ def test_name_decorator(self):
+ func = ast.parse("@deprecated\ndef foo(): pass").body[0]
+ assert resolve_decorator_name(func.decorator_list[0]) == "deprecated"
+
+ def test_call_decorator(self):
+ func = ast.parse("@deprecated(category=X)\ndef foo(): pass").body[0]
+ assert resolve_decorator_name(func.decorator_list[0]) == "deprecated"
+
+ def test_attribute_decorator(self):
+ func = ast.parse("@warnings.deprecated\ndef foo(): pass").body[0]
+ assert resolve_decorator_name(func.decorator_list[0]) ==
"warnings.deprecated"
+
+ def test_attribute_call_decorator(self):
+ func = ast.parse("@warnings.deprecated(category=X)\ndef foo():
pass").body[0]
+ assert resolve_decorator_name(func.decorator_list[0]) ==
"warnings.deprecated"
+
+
+class TestBuiltImportFrom:
+ def test_from_warnings_import_deprecated(self):
+ node = ast.parse("from warnings import deprecated").body[0]
+ result = built_import_from(node)
+ assert "deprecated" in result
+
+ def test_from_typing_extensions_import_deprecated(self):
+ node = ast.parse("from typing_extensions import deprecated").body[0]
+ result = built_import_from(node)
+ assert "deprecated" in result
+
+ def test_from_deprecated_import_deprecated(self):
+ node = ast.parse("from deprecated import deprecated").body[0]
+ result = built_import_from(node)
+ assert "deprecated" in result
+
+ def test_from_deprecated_classic_import_deprecated(self):
+ node = ast.parse("from deprecated.classic import deprecated").body[0]
+ result = built_import_from(node)
+ assert "deprecated" in result
+
+ def test_unrelated_import(self):
+ node = ast.parse("from os import path").body[0]
+ result = built_import_from(node)
+ assert result == []
+
+ def test_aliased_import(self):
+ node = ast.parse("from warnings import deprecated as dep").body[0]
+ result = built_import_from(node)
+ assert "dep" in result
+
+ def test_no_module_name(self):
+ # relative import with no module
+ node = ast.parse("from . import something").body[0]
+ result = built_import_from(node)
+ assert result == []
+
+ def test_import_parent_module(self):
+ node = ast.parse("from deprecated import classic").body[0]
+ result = built_import_from(node)
+ assert "classic.deprecated" in result
+
+
+class TestBuiltImport:
+ def test_import_warnings(self):
+ node = ast.parse("import warnings").body[0]
+ result = built_import(node)
+ assert "warnings.deprecated" in result
+
+ def test_import_typing_extensions(self):
+ node = ast.parse("import typing_extensions").body[0]
+ result = built_import(node)
+ assert "typing_extensions.deprecated" in result
+
+ def test_import_deprecated(self):
+ node = ast.parse("import deprecated").body[0]
+ result = built_import(node)
+ assert "deprecated.deprecated" in result
+
+ def test_import_unrelated(self):
+ node = ast.parse("import os").body[0]
+ result = built_import(node)
+ assert result == []
+
+ def test_import_with_alias(self):
+ node = ast.parse("import warnings as w").body[0]
+ result = built_import(node)
+ assert "w.deprecated" in result
+
+
+class TestFoundCompatibleDecorators:
+ def test_no_imports(self):
+ mod = ast.parse("x = 1")
+ assert found_compatible_decorators(mod) == ()
+
+ def test_with_warnings_import(self):
+ mod = ast.parse("from warnings import deprecated")
+ result = found_compatible_decorators(mod)
+ assert "deprecated" in result
+
+ def test_with_multiple_imports(self):
+ code = "from warnings import deprecated\nfrom deprecated import
deprecated as dep"
+ mod = ast.parse(code)
+ result = found_compatible_decorators(mod)
+ assert "dep" in result
+ assert "deprecated" in result
+
+ def test_deduplication(self):
+ code = "from warnings import deprecated\nfrom typing_extensions import
deprecated"
+ mod = ast.parse(code)
+ result = found_compatible_decorators(mod)
+ assert result.count("deprecated") == 1
+
+
+class TestGetDecoratorArgument:
+ def test_finds_keyword(self):
+ func = ast.parse("@deprecated(category=DeprecationWarning)\ndef foo():
pass").body[0]
+ decorator = func.decorator_list[0]
+ result = get_decorator_argument(decorator, "category")
+ assert result is not None
+ assert result.arg == "category"
+
+ def test_missing_keyword(self):
+ func = ast.parse("@deprecated(message='old')\ndef foo(): pass").body[0]
+ decorator = func.decorator_list[0]
+ result = get_decorator_argument(decorator, "category")
+ assert result is None
+
+ def test_multiple_keywords(self):
+ code = "@deprecated(message='old', category=DeprecationWarning)\ndef
foo(): pass"
+ func = ast.parse(code).body[0]
+ decorator = func.decorator_list[0]
+ result = get_decorator_argument(decorator, "category")
+ assert result is not None
+
+
+class TestIsFileUnderEolDeprecation:
+ def test_google_provider_with_matching_warning(self):
+ assert is_file_under_eol_deprecation(
+ GOOGLE_BIGQUERY_HOOK_PATH,
+ AIRFLOW_PROVIDER_DEPRECATION_WARNING,
+ )
+
+ def test_google_provider_with_non_matching_warning(self):
+ assert not is_file_under_eol_deprecation(
+ GOOGLE_BIGQUERY_HOOK_PATH,
+ "DeprecationWarning",
+ )
+
+ def test_non_google_provider(self):
+ assert not is_file_under_eol_deprecation(
+ "airflow/providers/amazon/aws/hooks/s3.py",
+ AIRFLOW_PROVIDER_DEPRECATION_WARNING,
+ )
+
+ def test_core_airflow_file(self):
+ assert not is_file_under_eol_deprecation(
+ "airflow/models/dag.py",
+ AIRFLOW_PROVIDER_DEPRECATION_WARNING,
+ )
diff --git a/scripts/tests/ci/prek/test_check_order_dockerfile_extras.py
b/scripts/tests/ci/prek/test_check_order_dockerfile_extras.py
new file mode 100644
index 00000000000..8cbe556a31b
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_order_dockerfile_extras.py
@@ -0,0 +1,118 @@
+# 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 ci.prek.check_order_dockerfile_extras import get_replaced_content
+
+
+class TestGetReplacedContent:
+ def test_replaces_between_markers(self):
+ content = [
+ "before\n",
+ "# START\n",
+ "old_item_1\n",
+ "old_item_2\n",
+ "# END\n",
+ "after\n",
+ ]
+ result = get_replaced_content(
+ content,
+ ["new_a", "new_b"],
+ "# START",
+ "# END",
+ prefix='"',
+ suffix='",',
+ add_empty_lines=False,
+ )
+ assert result == [
+ "before\n",
+ "# START\n",
+ '"new_a",\n',
+ '"new_b",\n',
+ "# END\n",
+ "after\n",
+ ]
+
+ def test_replaces_with_empty_lines(self):
+ content = [
+ "before\n",
+ ".. START\n",
+ "old\n",
+ ".. END\n",
+ "after\n",
+ ]
+ result = get_replaced_content(
+ content,
+ ["item1", "item2"],
+ ".. START",
+ ".. END",
+ prefix="* ",
+ suffix="",
+ add_empty_lines=True,
+ )
+ assert result == [
+ "before\n",
+ ".. START\n",
+ "\n",
+ "* item1\n",
+ "* item2\n",
+ "\n",
+ ".. END\n",
+ "after\n",
+ ]
+
+ def test_preserves_content_outside_markers(self):
+ content = [
+ "line1\n",
+ "line2\n",
+ "# START\n",
+ "old\n",
+ "# END\n",
+ "line3\n",
+ "line4\n",
+ ]
+ result = get_replaced_content(
+ content, ["new"], "# START", "# END", prefix="", suffix="",
add_empty_lines=False
+ )
+ assert result[0] == "line1\n"
+ assert result[1] == "line2\n"
+ assert result[-2] == "line3\n"
+ assert result[-1] == "line4\n"
+
+ def test_empty_extras_list(self):
+ content = [
+ "# START\n",
+ "old\n",
+ "# END\n",
+ ]
+ result = get_replaced_content(
+ content, [], "# START", "# END", prefix="", suffix="",
add_empty_lines=False
+ )
+ assert result == ["# START\n", "# END\n"]
+
+ def test_no_markers_returns_content_unchanged(self):
+ content = ["line1\n", "line2\n", "line3\n"]
+ result = get_replaced_content(
+ content,
+ ["new"],
+ "# NONEXISTENT START",
+ "# NONEXISTENT END",
+ prefix="",
+ suffix="",
+ add_empty_lines=False,
+ )
+ assert result == content
diff --git a/scripts/tests/ci/prek/test_checkout_no_credentials.py
b/scripts/tests/ci/prek/test_checkout_no_credentials.py
new file mode 100644
index 00000000000..bd94d6f9fd6
--- /dev/null
+++ b/scripts/tests/ci/prek/test_checkout_no_credentials.py
@@ -0,0 +1,251 @@
+# 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.
+"""Tests for checkout_no_credentials.py workflow validation logic.
+
+The script has a module-level guard preventing import, so we replicate
+the core check_file logic here and test it against the same rules.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import yaml
+
+ACTIONS_CHECKOUT_V4 = "actions/checkout@v4"
+
+
+def check_file(the_file: Path) -> int:
+ """Replicate the check_file logic from checkout_no_credentials.py."""
+ error_num = 0
+ res = yaml.safe_load(the_file.read_text())
+ for job in res["jobs"].values():
+ if job.get("steps") is None:
+ continue
+ for step in job["steps"]:
+ uses = step.get("uses")
+ if uses is not None and uses.startswith("actions/checkout"):
+ with_clause = step.get("with")
+ if with_clause is None:
+ error_num += 1
+ continue
+ path = with_clause.get("path")
+ if path == "constraints":
+ continue
+ if step.get("id") == "checkout-for-backport":
+ continue
+ persist_credentials = with_clause.get("persist-credentials")
+ if persist_credentials is None:
+ error_num += 1
+ continue
+ if persist_credentials:
+ error_num += 1
+ continue
+ return error_num
+
+
+class TestCheckFile:
+ def test_checkout_with_persist_credentials_false(self,
write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"persist-credentials": False},
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 0
+
+ def test_checkout_without_with_clause(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout",
+ "uses": ACTIONS_CHECKOUT_V4,
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 1
+
+ def test_checkout_without_persist_credentials(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"fetch-depth": 0},
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 1
+
+ def test_checkout_with_persist_credentials_true(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"persist-credentials": True},
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 1
+
+ def test_constraints_path_exception(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout constraints",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"path": "constraints"},
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 0
+
+ def test_backport_id_exception(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout for backport",
+ "id": "checkout-for-backport",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"fetch-depth": 0},
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 0
+
+ def test_non_checkout_step_ignored(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Setup Python",
+ "uses": "actions/setup-python@v5",
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 0
+
+ def test_job_without_steps(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "uses": "./.github/workflows/reusable.yml",
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 0
+
+ def test_multiple_errors(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout 1",
+ "uses": ACTIONS_CHECKOUT_V4,
+ },
+ {
+ "name": "Checkout 2",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"persist-credentials": True},
+ },
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 2
+
+ def test_multiple_jobs(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Checkout",
+ "uses": ACTIONS_CHECKOUT_V4,
+ "with": {"persist-credentials": False},
+ }
+ ]
+ },
+ "test": {
+ "steps": [
+ {
+ "name": "Checkout",
+ "uses": ACTIONS_CHECKOUT_V4,
+ }
+ ]
+ },
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 1
+
+ def test_run_step_without_uses(self, write_workflow_file):
+ workflow = {
+ "jobs": {
+ "build": {
+ "steps": [
+ {
+ "name": "Run tests",
+ "run": "pytest",
+ }
+ ]
+ }
+ }
+ }
+ path = write_workflow_file(workflow)
+ assert check_file(path) == 0
diff --git a/scripts/tests/ci/prek/test_common_prek_utils.py
b/scripts/tests/ci/prek/test_common_prek_utils.py
new file mode 100644
index 00000000000..e6da2f74be0
--- /dev/null
+++ b/scripts/tests/ci/prek/test_common_prek_utils.py
@@ -0,0 +1,425 @@
+# 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 ci.prek.common_prek_utils import (
+ ConsoleDiff,
+ check_list_sorted,
+ get_imports_from_file,
+ get_provider_base_dir_from_path,
+ get_provider_id_from_path,
+ initialize_breeze_prek,
+ insert_documentation,
+ pre_process_mypy_files,
+ read_airflow_version,
+ read_allowed_kubernetes_versions,
+ temporary_tsc_project,
+)
+
+PROVIDERS_AMAZON_S3_PATH = "providers/amazon/hooks/s3.py"
+AIRFLOW_MODELS_DAG_PATH = "airflow/models/dag.py"
+
+
+class TestPreProcessMypyFiles:
+ def test_excludes_conftest(self):
+ files = ["tests/conftest.py", "tests/test_foo.py"]
+ result = pre_process_mypy_files(files)
+ assert "tests/conftest.py" not in result
+ assert "tests/test_foo.py" in result
+
+ def test_excludes_init(self):
+ files = ["airflow/__init__.py", AIRFLOW_MODELS_DAG_PATH]
+ result = pre_process_mypy_files(files)
+ assert "airflow/__init__.py" not in result
+ assert AIRFLOW_MODELS_DAG_PATH in result
+
+ def test_excludes_both(self):
+ files = ["conftest.py", "__init__.py", "test_foo.py"]
+ result = pre_process_mypy_files(files)
+ assert result == ["test_foo.py"]
+
+ def test_empty_list(self):
+ assert pre_process_mypy_files([]) == []
+
+ def test_on_non_main_branch_excludes_providers(self, monkeypatch):
+ monkeypatch.setenv("DEFAULT_BRANCH", "v2-10-stable")
+ files = [PROVIDERS_AMAZON_S3_PATH, AIRFLOW_MODELS_DAG_PATH]
+ result = pre_process_mypy_files(files)
+ assert PROVIDERS_AMAZON_S3_PATH not in result
+ assert AIRFLOW_MODELS_DAG_PATH in result
+
+ def test_on_main_branch_keeps_providers(self, monkeypatch):
+ monkeypatch.setenv("DEFAULT_BRANCH", "main")
+ files = [PROVIDERS_AMAZON_S3_PATH, AIRFLOW_MODELS_DAG_PATH]
+ result = pre_process_mypy_files(files)
+ assert PROVIDERS_AMAZON_S3_PATH in result
+ assert AIRFLOW_MODELS_DAG_PATH in result
+
+ def test_no_default_branch_keeps_providers(self, monkeypatch):
+ monkeypatch.delenv("DEFAULT_BRANCH", raising=False)
+ files = [PROVIDERS_AMAZON_S3_PATH]
+ result = pre_process_mypy_files(files)
+ assert PROVIDERS_AMAZON_S3_PATH in result
+
+
+class TestGetImportsFromFile:
+ def test_simple_import(self, write_python_file):
+ path = write_python_file("import os\nimport sys\n")
+ result = get_imports_from_file(path, only_top_level=True)
+ assert "os" in result
+ assert "sys" in result
+
+ def test_from_import(self, write_python_file):
+ path = write_python_file("from collections import defaultdict\n")
+ result = get_imports_from_file(path, only_top_level=True)
+ assert "collections.defaultdict" in result
+
+ def test_skips_future_imports(self, write_python_file):
+ path = write_python_file("from __future__ import annotations\nimport
os\n")
+ result = get_imports_from_file(path, only_top_level=True)
+ assert not any("__future__" in imp for imp in result)
+ assert "os" in result
+
+ def test_top_level_only_excludes_nested(self, write_python_file):
+ code = """\
+ import os
+
+ def inner():
+ import json
+ """
+ path = write_python_file(code)
+ top_level = get_imports_from_file(path, only_top_level=True)
+ assert "os" in top_level
+ assert "json" not in top_level
+
+ def test_all_levels_includes_nested(self, write_python_file):
+ code = """\
+ import os
+
+ def inner():
+ import json
+ """
+ path = write_python_file(code)
+ all_level = get_imports_from_file(path, only_top_level=False)
+ assert "os" in all_level
+ assert "json" in all_level
+
+ def test_multiple_from_imports(self, write_python_file):
+ path = write_python_file("from pathlib import Path, PurePath\n")
+ result = get_imports_from_file(path, only_top_level=True)
+ assert "pathlib.Path" in result
+ assert "pathlib.PurePath" in result
+
+ def test_empty_file(self, write_python_file):
+ path = write_python_file("")
+ result = get_imports_from_file(path, only_top_level=True)
+ assert result == []
+
+
+class TestInsertDocumentation:
+ def test_replaces_content_between_header_and_footer(self, write_text_file):
+ path = write_text_file("before\n<!-- START -->\nold content\n<!-- END
-->\nafter\n")
+ result = insert_documentation(
+ path,
+ content=["new line 1\n", "new line 2\n"],
+ header="<!-- START -->",
+ footer="<!-- END -->",
+ )
+ assert result is True
+ text = path.read_text()
+ assert "new line 1" in text
+ assert "new line 2" in text
+ assert "old content" not in text
+ assert "before" in text
+ assert "after" in text
+
+ def test_returns_false_when_content_unchanged(self, write_text_file):
+ path = write_text_file("before\n<!-- START -->\nkept\n<!-- END
-->\nafter\n")
+ result = insert_documentation(
+ path,
+ content=["kept\n"],
+ header="<!-- START -->",
+ footer="<!-- END -->",
+ )
+ assert result is False
+
+ def test_exits_when_header_not_found(self, write_text_file):
+ path = write_text_file("no markers here\n")
+ with pytest.raises(SystemExit):
+ insert_documentation(
+ path,
+ content=["anything\n"],
+ header="<!-- MISSING -->",
+ footer="<!-- END -->",
+ )
+
+ def test_add_comment_prefixes_lines(self, write_text_file):
+ path = write_text_file("before\n# START\nold\n# END\nafter\n")
+ result = insert_documentation(
+ path,
+ content=["line one\n", "line two\n"],
+ header="# START",
+ footer="# END",
+ add_comment=True,
+ )
+ assert result is True
+ text = path.read_text()
+ assert "# line one\n" in text
+ assert "# line two\n" in text
+
+ def test_add_comment_handles_blank_lines(self, write_text_file):
+ path = write_text_file("# START\nold\n# END\n")
+ result = insert_documentation(
+ path,
+ content=["\n"],
+ header="# START",
+ footer="# END",
+ add_comment=True,
+ )
+ assert result is True
+ text = path.read_text()
+ assert "#\n" in text
+
+ def test_preserves_header_and_footer_lines(self, write_text_file):
+ path = write_text_file("<!-- START -->\nold\n<!-- END -->\n")
+ insert_documentation(
+ path,
+ content=["new\n"],
+ header="<!-- START -->",
+ footer="<!-- END -->",
+ )
+ text = path.read_text()
+ assert "<!-- START -->" in text
+ assert "<!-- END -->" in text
+
+ def test_header_with_leading_whitespace(self, write_text_file):
+ path = write_text_file(" <!-- START -->\nold\n <!-- END -->\n")
+ result = insert_documentation(
+ path,
+ content=["new\n"],
+ header="<!-- START -->",
+ footer="<!-- END -->",
+ )
+ assert result is True
+ assert "new" in path.read_text()
+
+ def test_multiple_content_lines(self, write_text_file):
+ path = write_text_file("header line\n## BEGIN\nreplaced\n##
FINISH\nfooter line\n")
+ insert_documentation(
+ path,
+ content=["a\n", "b\n", "c\n"],
+ header="## BEGIN",
+ footer="## FINISH",
+ )
+ text = path.read_text()
+ assert "a\nb\nc\n" in text
+ assert "replaced" not in text
+ assert "header line" in text
+ assert "footer line" in text
+
+
+class TestReadAirflowVersion:
+ def test_returns_version_string(self):
+ version = read_airflow_version()
+ assert isinstance(version, str)
+ # Airflow version should look like X.Y.Z or X.Y.Z.devN
+ parts = version.split(".")
+ assert len(parts) >= 3
+ assert parts[0].isdigit()
+ assert parts[1].isdigit()
+
+
+class TestReadAllowedKubernetesVersions:
+ def test_returns_list_of_versions(self):
+ versions = read_allowed_kubernetes_versions()
+ assert isinstance(versions, list)
+ assert len(versions) > 0
+
+ def test_versions_have_no_v_prefix(self):
+ versions = read_allowed_kubernetes_versions()
+ for v in versions:
+ assert not v.startswith("v"), f"Version {v!r} should not have 'v'
prefix"
+
+ def test_versions_look_like_semver(self):
+ versions = read_allowed_kubernetes_versions()
+ for v in versions:
+ parts = v.split(".")
+ assert len(parts) >= 2, f"Version {v!r} should have at least
major.minor"
+ assert parts[0].isdigit()
+ assert parts[1].isdigit()
+
+
+class TestConsoleDiff:
+ def test_dump_added_lines(self):
+ diff = ConsoleDiff()
+ lines = list(diff._dump("+", ["line1", "line2"], 0, 2))
+ assert lines == ["[green]+ line1[/]", "[green]+ line2[/]"]
+
+ def test_dump_removed_lines(self):
+ diff = ConsoleDiff()
+ lines = list(diff._dump("-", ["line1"], 0, 1))
+ assert lines == ["[red]- line1[/]"]
+
+ def test_dump_unchanged_lines(self):
+ diff = ConsoleDiff()
+ lines = list(diff._dump(" ", ["line1", "line2"], 0, 2))
+ assert lines == [" line1", " line2"]
+
+ def test_dump_range(self):
+ diff = ConsoleDiff()
+ lines = list(diff._dump("+", ["a", "b", "c", "d"], 1, 3))
+ assert lines == ["[green]+ b[/]", "[green]+ c[/]"]
+
+
+class TestCheckListSorted:
+ def test_sorted_list_returns_true(self):
+ errors: list[str] = []
+ result = check_list_sorted(["a", "b", "c"], "test list", errors)
+ assert result is True
+ assert errors == []
+
+ def test_unsorted_list_returns_false(self):
+ errors: list[str] = []
+ result = check_list_sorted(["c", "a", "b"], "test list", errors)
+ assert result is False
+ assert len(errors) == 1
+ assert "not sorted" in errors[0]
+
+ def test_duplicates_returns_false(self):
+ errors: list[str] = []
+ result = check_list_sorted(["a", "a", "b"], "test list", errors)
+ assert result is False
+ assert len(errors) == 1
+
+ def test_empty_list_returns_true(self):
+ errors: list[str] = []
+ result = check_list_sorted([], "empty", errors)
+ assert result is True
+ assert errors == []
+
+ def test_single_element_returns_true(self):
+ errors: list[str] = []
+ result = check_list_sorted(["only"], "single", errors)
+ assert result is True
+ assert errors == []
+
+
+class TestGetProviderIdFromPath:
+ def test_simple_provider(self, create_provider_tree):
+ file_path = create_provider_tree("providers/amazon")
+ result = get_provider_id_from_path(file_path)
+ assert result == "amazon"
+
+ def test_nested_provider(self, create_provider_tree):
+ file_path = create_provider_tree("providers/apache/hive")
+ result = get_provider_id_from_path(file_path)
+ assert result == "apache.hive"
+
+ def test_no_provider_yaml(self, tmp_path):
+ some_dir = tmp_path / "no_provider"
+ some_dir.mkdir()
+ test_file = some_dir / "file.py"
+ test_file.touch()
+ result = get_provider_id_from_path(test_file)
+ assert result is None
+
+ def test_no_providers_parent(self, tmp_path):
+ # provider.yaml exists but no "providers" parent directory
+ some_dir = tmp_path / "something" / "else"
+ some_dir.mkdir(parents=True)
+ (some_dir / "provider.yaml").touch()
+ test_file = some_dir / "file.py"
+ test_file.touch()
+ result = get_provider_id_from_path(test_file)
+ assert result is None
+
+
+class TestGetProviderBaseDirFromPath:
+ def test_finds_provider_dir(self, tmp_path):
+ provider_dir = tmp_path / "providers" / "amazon"
+ provider_dir.mkdir(parents=True)
+ (provider_dir / "provider.yaml").touch()
+ sub_file = provider_dir / "hooks" / "s3.py"
+ sub_file.parent.mkdir()
+ sub_file.touch()
+ result = get_provider_base_dir_from_path(sub_file)
+ assert result == provider_dir
+
+ def test_returns_none_without_provider_yaml(self, tmp_path):
+ some_dir = tmp_path / "no_provider"
+ some_dir.mkdir()
+ test_file = some_dir / "file.py"
+ test_file.touch()
+ result = get_provider_base_dir_from_path(test_file)
+ assert result is None
+
+ def test_finds_nearest_provider_yaml(self, tmp_path):
+ outer = tmp_path / "providers" / "google"
+ inner = outer / "cloud"
+ inner.mkdir(parents=True)
+ (outer / "provider.yaml").touch()
+ test_file = inner / "hooks.py"
+ test_file.touch()
+ result = get_provider_base_dir_from_path(test_file)
+ assert result == outer
+
+
+class TestInitializeBreezePrek:
+ def test_raises_when_not_main(self):
+ with pytest.raises(SystemExit, match="intended to be executed"):
+ initialize_breeze_prek("some_module", "script.py")
+
+ def test_exits_when_skip_env_set(self, monkeypatch):
+ monkeypatch.setenv("SKIP_BREEZE_PREK_HOOKS", "1")
+ with pytest.raises(SystemExit) as exc_info:
+ initialize_breeze_prek("__main__", "script.py")
+ assert exc_info.value.code == 0
+
+ def test_exits_when_breeze_not_found(self, monkeypatch):
+ monkeypatch.delenv("SKIP_BREEZE_PREK_HOOKS", raising=False)
+ monkeypatch.setattr("shutil.which", lambda _: None)
+ with pytest.raises(SystemExit) as exc_info:
+ initialize_breeze_prek("__main__", "script.py")
+ assert exc_info.value.code == 1
+
+
+class TestTemporaryTscProject:
+ def test_creates_temp_tsconfig(self, tmp_path):
+ tsconfig = tmp_path / "tsconfig.json"
+ tsconfig.write_text("{}")
+ with temporary_tsc_project(tsconfig, ["src/app.ts", "src/main.ts"]) as
temp:
+ content = Path(temp.name).read_text()
+ assert '"src/app.ts"' in content
+ assert '"src/main.ts"' in content
+ assert f"./{tsconfig.name}" in content
+
+ def test_raises_when_tsconfig_missing(self, tmp_path):
+ missing = tmp_path / "nonexistent.json"
+ with pytest.raises(RuntimeError, match="Cannot find"):
+ with temporary_tsc_project(missing, []):
+ pass
+
+ def test_extends_original_tsconfig(self, tmp_path):
+ tsconfig = tmp_path / "tsconfig.base.json"
+ tsconfig.write_text("{}")
+ with temporary_tsc_project(tsconfig, ["file.ts"]) as temp:
+ content = Path(temp.name).read_text()
+ assert f'"extends": "./{tsconfig.name}"' in content
+ assert '"include": ["file.ts"]' in content
diff --git a/scripts/tests/ci/prek/test_new_session_in_provide_session.py
b/scripts/tests/ci/prek/test_new_session_in_provide_session.py
new file mode 100644
index 00000000000..e59c200e5a8
--- /dev/null
+++ b/scripts/tests/ci/prek/test_new_session_in_provide_session.py
@@ -0,0 +1,243 @@
+# 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 ast
+import textwrap
+
+import pytest
+from ci.prek.new_session_in_provide_session import (
+ _annotation_has_none,
+ _get_session_arg_and_default,
+ _is_decorated_correctly,
+ _is_new_session_or_none,
+ _iter_incorrect_new_session_usages,
+ _SessionDefault,
+)
+
+
+def _parse_func_args(code: str) -> ast.arguments:
+ """Parse a function definition and return its arguments node."""
+ node = ast.parse(textwrap.dedent(code)).body[0]
+ assert isinstance(node, ast.FunctionDef)
+ return node.args
+
+
+def _parse_expr(code: str) -> ast.expr:
+ """Parse a single expression."""
+ node = ast.parse(code, mode="eval").body
+ return node
+
+
[email protected]
+def check_session_code(write_python_file):
+ """Factory fixture: write code to a temp file and check for incorrect
NEW_SESSION usages."""
+
+ def _check(code: str) -> list[ast.FunctionDef]:
+ path = write_python_file(code)
+ return list(_iter_incorrect_new_session_usages(path))
+
+ return _check
+
+
+class TestGetSessionArgAndDefault:
+ def test_no_session_arg(self):
+ args = _parse_func_args("def foo(x, y): pass")
+ assert _get_session_arg_and_default(args) is None
+
+ def test_session_positional_no_default(self):
+ args = _parse_func_args("def foo(session): pass")
+ result = _get_session_arg_and_default(args)
+ assert result is not None
+ assert result.argument.arg == "session"
+ assert result.default is None
+
+ def test_session_positional_with_default_none(self):
+ args = _parse_func_args("def foo(session=None): pass")
+ result = _get_session_arg_and_default(args)
+ assert result is not None
+ assert result.argument.arg == "session"
+ assert isinstance(result.default, ast.Constant)
+ assert result.default.value is None
+
+ def test_session_kwonly_with_default(self):
+ args = _parse_func_args("def foo(*, session=NEW_SESSION): pass")
+ result = _get_session_arg_and_default(args)
+ assert result is not None
+ assert result.argument.arg == "session"
+ assert isinstance(result.default, ast.Name)
+
+ def test_session_among_other_args(self):
+ args = _parse_func_args("def foo(x, y, session=None, z=5): pass")
+ result = _get_session_arg_and_default(args)
+ assert result is not None
+ assert result.argument.arg == "session"
+
+ def test_kwonly_session_among_other_kwargs(self):
+ args = _parse_func_args("def foo(x, *, timeout=30, session=None):
pass")
+ result = _get_session_arg_and_default(args)
+ assert result is not None
+ assert result.argument.arg == "session"
+
+
+class TestIsNewSessionOrNone:
+ def test_none_constant(self):
+ expr = _parse_expr("None")
+ assert _is_new_session_or_none(expr) == _SessionDefault.none
+
+ def test_new_session_name(self):
+ expr = _parse_expr("NEW_SESSION")
+ assert _is_new_session_or_none(expr) == _SessionDefault.new_session
+
+ def test_other_name(self):
+ expr = _parse_expr("SOMETHING_ELSE")
+ assert _is_new_session_or_none(expr) is None
+
+ def test_integer_constant(self):
+ expr = _parse_expr("42")
+ assert _is_new_session_or_none(expr) is None
+
+ def test_string_constant(self):
+ expr = _parse_expr("'hello'")
+ assert _is_new_session_or_none(expr) is None
+
+
+class TestIsDecoratedCorrectly:
+ def test_provide_session_decorator(self):
+ func = ast.parse("@provide_session\ndef foo(): pass").body[0]
+ assert _is_decorated_correctly(func.decorator_list) is True
+
+ def test_overload_decorator(self):
+ func = ast.parse("@overload\ndef foo(): pass").body[0]
+ assert _is_decorated_correctly(func.decorator_list) is True
+
+ def test_abstractmethod_decorator(self):
+ func = ast.parse("@abstractmethod\ndef foo(): pass").body[0]
+ assert _is_decorated_correctly(func.decorator_list) is True
+
+ def test_no_decorator(self):
+ func = ast.parse("def foo(): pass").body[0]
+ assert _is_decorated_correctly(func.decorator_list) is False
+
+ def test_unrelated_decorator(self):
+ func = ast.parse("@staticmethod\ndef foo(): pass").body[0]
+ assert _is_decorated_correctly(func.decorator_list) is False
+
+ def test_multiple_decorators_with_provide_session(self):
+ code = "@staticmethod\n@provide_session\ndef foo(): pass"
+ func = ast.parse(code).body[0]
+ assert _is_decorated_correctly(func.decorator_list) is True
+
+
+class TestAnnotationHasNone:
+ def test_none_value(self):
+ assert _annotation_has_none(None) is False
+
+ def test_none_constant(self):
+ expr = _parse_expr("None")
+ assert _annotation_has_none(expr) is True
+
+ def test_non_none_constant(self):
+ expr = _parse_expr("42")
+ assert _annotation_has_none(expr) is False
+
+ def test_union_with_none(self):
+ expr = _parse_expr("int | None")
+ assert _annotation_has_none(expr) is True
+
+ def test_union_without_none(self):
+ expr = _parse_expr("int | str")
+ assert _annotation_has_none(expr) is False
+
+ def test_nested_union_with_none(self):
+ expr = _parse_expr("int | str | None")
+ assert _annotation_has_none(expr) is True
+
+ def test_name_type(self):
+ expr = _parse_expr("Session")
+ assert _annotation_has_none(expr) is False
+
+
+class TestIterIncorrectNewSessionUsages:
+ def test_correct_provide_session(self, check_session_code):
+ code = """\
+ @provide_session
+ def foo(session=NEW_SESSION):
+ pass
+ """
+ assert check_session_code(code) == []
+
+ def test_incorrect_new_session_without_decorator(self, check_session_code):
+ code = """\
+ def foo(session=NEW_SESSION):
+ pass
+ """
+ errors = check_session_code(code)
+ assert len(errors) == 1
+ assert errors[0].name == "foo"
+
+ def test_no_session_arg(self, check_session_code):
+ code = """\
+ def foo(x, y):
+ pass
+ """
+ assert check_session_code(code) == []
+
+ def test_session_no_default(self, check_session_code):
+ code = """\
+ def foo(session):
+ pass
+ """
+ assert check_session_code(code) == []
+
+ def test_none_default_with_none_annotation(self, check_session_code):
+ code = """\
+ def foo(session: Session | None = None):
+ pass
+ """
+ assert check_session_code(code) == []
+
+ def test_none_default_without_none_annotation(self, check_session_code):
+ code = """\
+ def foo(session: Session = None):
+ pass
+ """
+ errors = check_session_code(code)
+ assert len(errors) == 1
+
+ def test_overload_allows_new_session(self, check_session_code):
+ code = """\
+ @overload
+ def foo(session=NEW_SESSION):
+ pass
+ """
+ assert check_session_code(code) == []
+
+ def test_abstractmethod_allows_new_session(self, check_session_code):
+ code = """\
+ @abstractmethod
+ def foo(session=NEW_SESSION):
+ pass
+ """
+ assert check_session_code(code) == []
+
+ def test_other_default_value_is_ignored(self, check_session_code):
+ code = """\
+ def foo(session="default"):
+ pass
+ """
+ assert check_session_code(code) == []
diff --git a/scripts/tests/ci/prek/test_newsfragments.py
b/scripts/tests/ci/prek/test_newsfragments.py
new file mode 100644
index 00000000000..bd249e45405
--- /dev/null
+++ b/scripts/tests/ci/prek/test_newsfragments.py
@@ -0,0 +1,81 @@
+# 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 pytest
+from ci.prek.newsfragments import VALID_CHANGE_TYPES, validate_newsfragment
+
+ALL_CHANGE_TYPES = sorted(VALID_CHANGE_TYPES)
+NON_SIGNIFICANT_CHANGE_TYPES = sorted(VALID_CHANGE_TYPES - {"significant"})
+
+
+class TestNewsfragmentFilenameValidation:
+ @pytest.mark.parametrize("change_type", ALL_CHANGE_TYPES)
+ def test_valid_filename_all_types(self, change_type):
+ errors = validate_newsfragment(f"12345.{change_type}.rst", ["A
change"])
+ assert errors == []
+
+ def test_too_few_parts(self):
+ errors = validate_newsfragment("12345.rst", ["A change"])
+ assert len(errors) == 1
+ assert "unexpected filename" in errors[0]
+
+ def test_too_many_parts(self):
+ errors = validate_newsfragment("12345.bugfix.extra.rst", ["A change"])
+ assert len(errors) == 1
+ assert "unexpected filename" in errors[0]
+
+ def test_invalid_change_type(self):
+ errors = validate_newsfragment("12345.invalid.rst", ["A change"])
+ assert len(errors) == 1
+ assert "unexpected type" in errors[0]
+
+
+class TestNewsfragmentContentValidation:
+ @pytest.mark.parametrize("change_type", NON_SIGNIFICANT_CHANGE_TYPES)
+ def test_non_significant_single_line_ok(self, change_type):
+ errors = validate_newsfragment(f"123.{change_type}.rst", ["Fix
something"])
+ assert errors == []
+
+ @pytest.mark.parametrize("change_type", NON_SIGNIFICANT_CHANGE_TYPES)
+ def test_non_significant_multi_line_fails(self, change_type):
+ errors = validate_newsfragment(f"123.{change_type}.rst", ["Fix
something", "More details"])
+ assert len(errors) == 1
+ assert "single line" in errors[0]
+
+ def test_significant_single_line_ok(self):
+ errors = validate_newsfragment("123.significant.rst", ["Big change"])
+ assert errors == []
+
+ def test_significant_two_lines_fails(self):
+ errors = validate_newsfragment("123.significant.rst", ["Big change",
"Second line"])
+ assert len(errors) == 1
+ assert "1, or 3+ lines" in errors[0]
+
+ def test_significant_three_lines_with_blank_second_ok(self):
+ errors = validate_newsfragment("123.significant.rst", ["Big change",
"", "Details here"])
+ assert errors == []
+
+ def test_significant_three_lines_without_blank_second_fails(self):
+ errors = validate_newsfragment("123.significant.rst", ["Big change",
"Not blank", "Details"])
+ assert len(errors) == 1
+ assert "empty second line" in errors[0]
+
+ def test_significant_many_lines_ok(self):
+ lines = ["Big change", "", "Details here", "More details", "Even more"]
+ errors = validate_newsfragment("123.significant.rst", lines)
+ assert errors == []
diff --git a/scripts/tests/ci/prek/test_unittest_testcase.py
b/scripts/tests/ci/prek/test_unittest_testcase.py
new file mode 100644
index 00000000000..dfc03f29dc1
--- /dev/null
+++ b/scripts/tests/ci/prek/test_unittest_testcase.py
@@ -0,0 +1,93 @@
+# 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 ci.prek.unittest_testcase import check_test_file
+
+
+class TestCheckTestFile:
+ def test_no_testcase_inheritance(self, write_python_file):
+ path = write_python_file("""\
+ class TestFoo:
+ def test_something(self):
+ pass
+ """)
+ assert check_test_file(str(path)) == 0
+
+ def test_direct_testcase_inheritance(self, write_python_file):
+ path = write_python_file("""\
+ from unittest import TestCase
+
+ class TestFoo(TestCase):
+ def test_something(self):
+ pass
+ """)
+ assert check_test_file(str(path)) == 1
+
+ def test_attribute_testcase_inheritance(self, write_python_file):
+ path = write_python_file("""\
+ import unittest
+
+ class TestFoo(unittest.TestCase):
+ def test_something(self):
+ pass
+ """)
+ assert check_test_file(str(path)) == 1
+
+ def test_multiple_testcase_classes(self, write_python_file):
+ path = write_python_file("""\
+ from unittest import TestCase
+
+ class TestFoo(TestCase):
+ pass
+
+ class TestBar(TestCase):
+ pass
+ """)
+ assert check_test_file(str(path)) == 2
+
+ def test_inherited_from_local_testcase_class(self, write_python_file):
+ path = write_python_file("""\
+ from unittest import TestCase
+
+ class TestBase(TestCase):
+ pass
+
+ class TestChild(TestBase):
+ pass
+ """)
+ # TestBase is detected first, then TestChild inherits from known class
+ assert check_test_file(str(path)) == 2
+
+ def test_no_classes(self, write_python_file):
+ path = write_python_file("""\
+ def test_something():
+ pass
+ """)
+ assert check_test_file(str(path)) == 0
+
+ def test_class_with_other_base(self, write_python_file):
+ path = write_python_file("""\
+ class TestFoo(SomeOtherBase):
+ def test_something(self):
+ pass
+ """)
+ assert check_test_file(str(path)) == 0
+
+ def test_empty_file(self, write_python_file):
+ path = write_python_file("")
+ assert check_test_file(str(path)) == 0