blob: 0084cd9943fafdf342e3b84a3ca086b36f9b7698 [file] [log] [blame]
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# /// script
# requires-python = ">=3.10,<3.11"
# dependencies = [
# "rich>=13.6.0",
# ]
# ///
from __future__ import annotations
import 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,
KNOWN_SECOND_LEVEL_PATHS,
console,
get_all_provider_ids,
initialize_breeze_prek,
run_command_via_breeze_shell,
)
initialize_breeze_prek(__name__, __file__)
ALLOWED_FOLDERS = [
"airflow-core",
*[f"providers/{provider_id.replace('.', '/')}" for provider_id in get_all_provider_ids()],
"dev",
"scripts",
"devel-common",
"task-sdk",
"airflow-ctl",
"providers",
]
if len(sys.argv) < 2:
console.print(f"[yellow]You need to specify the folder to test as parameter: {ALLOWED_FOLDERS}\n")
sys.exit(1)
mypy_folders = sys.argv[1:]
show_unused_warnings = os.environ.get("SHOW_UNUSED_MYPY_WARNINGS", "false")
show_unreachable_warnings = os.environ.get("SHOW_UNREACHABLE_MYPY_WARNINGS", "false")
for mypy_folder in mypy_folders:
if mypy_folder not in ALLOWED_FOLDERS:
console.print(
f"\n[red]ERROR: Folder `{mypy_folder}` is wrong.[/]\n\n"
f"All folders passed should be one of those: {ALLOWED_FOLDERS}\n"
)
sys.exit(1)
arguments = mypy_folders.copy()
MYPY_FILE_LIST = AIRFLOW_ROOT_PATH / "files" / "mypy_files.txt"
MYPY_FILE_LIST.parent.mkdir(parents=True, exist_ok=True)
FILE_ARGUMENT = "@/files/mypy_files.txt"
all_provider_duplicated_path_to_ignore = []
for second_level_path in KNOWN_SECOND_LEVEL_PATHS:
all_provider_duplicated_path_to_ignore.extend(
[
rf"^.*/providers/{second_level_path}/.*/src/airflow/providers/{second_level_path}/__init__.py$",
rf"^.*/providers/{second_level_path}/.*/tests/unit/{second_level_path}/__init__.py$",
rf"^.*/providers/{second_level_path}/.*/tests/integration/{second_level_path}/__init__.py$",
rf"^.*/providers/{second_level_path}/.*/tests/system/{second_level_path}/__init__.py$",
]
)
exclude_regexps = [
re.compile(x)
for x in [
r"^.*/node_modules/.*",
r"^.*\\..*",
r"^.*/src/airflow/__init__.py$",
# Remove duplicated (legacy namespace packages) __init__.py files from providers
r"^.*/providers/src/airflow/__init__.py$",
r"^.*/providers/src/airflow/providers/__init__.py$",
r"^.*/providers/.*/tests/unit/__init__.py$",
r"^.*/providers/.*/tests/integration/__init__.py$",
r"^.*/providers/.*/tests/system/__init__.py$",
*all_provider_duplicated_path_to_ignore,
]
]
def get_all_files(folder: str) -> list[str]:
files_to_check = []
python_file_paths = (AIRFLOW_ROOT_PATH / folder).resolve().rglob("*.py")
for file in python_file_paths:
if (
(file.name not in ("conftest.py",) and not any(x.match(file.as_posix()) for x in exclude_regexps))
and not any(part.startswith(".") for part in file.parts)
) and not file.as_posix().endswith("src/airflow/providers/__init__.py"):
files_to_check.append(file.relative_to(AIRFLOW_ROOT_PATH).as_posix())
file_spec = "@/files/mypy_files.txt"
if console:
console.print(f"[info]Running mypy with {file_spec}")
else:
print(f"info: Running mypy with {file_spec}")
return files_to_check
all_files_to_check = []
for mypy_folder in mypy_folders:
all_files_to_check.extend(get_all_files(mypy_folder))
MYPY_FILE_LIST.write_text("\n".join(all_files_to_check))
if console:
if os.environ.get("CI"):
console.print("[info]The content of the file is:")
console.print(all_files_to_check)
else:
console.print(f"[info]You cand check the list of files in:[/] {MYPY_FILE_LIST}")
else:
if os.environ.get("CI"):
print("info: The content of the file is:")
print(all_files_to_check)
else:
print(f"info: You cand check the list of files in: {MYPY_FILE_LIST}")
print(f"Running mypy with {FILE_ARGUMENT}")
mypy_cmd_parts = [f"TERM=ansi mypy {shlex.quote(FILE_ARGUMENT)}"]
if show_unused_warnings == "true":
if console:
console.print(
"[info]Running mypy with --warn-unused-ignores to display unused ignores, unset environment variable: SHOW_UNUSED_MYPY_WARNINGS to runoff this behaviour"
)
else:
print(
"info: Running mypy with --warn-unused-ignores to display unused ignores, unset environment variable: SHOW_UNUSED_MYPY_WARNINGS to runoff this behaviour"
)
mypy_cmd_parts.append("--warn-unused-ignores")
if show_unreachable_warnings == "true":
if console:
console.print(
"[info]Running mypy with --warn-unreachable to display unreachable code, unset environment variable: SHOW_UNREACHABLE_MYPY_WARNINGS to runoff this behaviour"
)
else:
print(
"info: Running mypy with --warn-unreachable to display unreachable code, unset environment variable: SHOW_UNREACHABLE_MYPY_WARNINGS to runoff this behaviour"
)
mypy_cmd_parts.append("--warn-unreachable")
mypy_cmd = " ".join(mypy_cmd_parts)
cmd = ["bash", "-c", mypy_cmd]
res = run_command_via_breeze_shell(
cmd=cmd,
warn_image_upgrade_needed=True,
extra_env={
"INCLUDE_MYPY_VOLUME": os.environ.get("INCLUDE_MYPY_VOLUME", "true"),
# Need to mount local sources when running it - to not have to rebuild the image
# and to let CI work on it when running on PRs from forks - because mypy-dev uses files
# that are not available at the time when image is built in CI
"MOUNT_SOURCES": "selected",
},
)
ci_environment = os.environ.get("CI")
if res.returncode != 0:
if console:
if ci_environment:
console.print(
"[yellow]You are running mypy with the folders selected. If you want to "
"reproduce it locally, you need to run the following command:\n"
)
console.print(f"prek --hook-stage manual mypy-{mypy_folders[0]} --all-files\n")
upgrading = os.environ.get("UPGRADE_TO_NEWER_DEPENDENCIES", "false") != "false"
if upgrading:
console.print(
"[yellow]You are running mypy with the image that has dependencies upgraded automatically.\n"
)
flag = " --upgrade-to-newer-dependencies" if upgrading else ""
console.print(
"[yellow]If you see strange stacktraces above, and can't reproduce it, please run"
" this command and try again:\n"
)
console.print(f"breeze ci-image build --python 3.10{flag}\n")
console.print(
"[yellow]You can also run `breeze down --cleanup-mypy-cache` to clean up the cache used.\n"
)
else:
if ci_environment:
print(
"You are running mypy with the folders selected. If you want to "
"reproduce it locally, you need to run the following command:\n"
)
print(f"prek --hook-stage manual mypy-{mypy_folders[0]} --all-files\n")
upgrading = os.environ.get("UPGRADE_TO_NEWER_DEPENDENCIES", "false") != "false"
if upgrading:
print("You are running mypy with the image that has dependencies upgraded automatically.\n")
flag = " --upgrade-to-newer-dependencies" if upgrading else ""
print(
"If you see strange stacktraces above, and can't reproduce it, please run"
" this command and try again:\n"
)
print(f"breeze ci-image build --python 3.10{flag}\n")
print("You can also run `breeze down --cleanup-mypy-cache` to clean up the cache used.\n")
sys.exit(res.returncode)