mirror of
https://github.com/apache/airflow.git
synced 2026-11-04 07:12:48 +00:00
This is continuation of the separation of the Airflow codebase into
separate distributions. This one splits airflow into two of them:
* apache-airflow - becomes an empty, meta no-code distribution that
only has dependencies to apache-airflow-core and task-sdk
distributions and it has preinstalled provider distributions
added in standard "wheel" distribution. All "extras" lead
either to "apache-airflow-core" extras or to providers - the
dependencies and optional dependencies are calculated differently
depending on "editable" or "standard" mode - in editable mode,
just provider dependencies are installed for preinstalled providers
in standard mode - those preinstalled providers are dependencies.
* the apache-airflow-core distribution contains all airflow core
sources (previously in apache-airflow) and it has no provider
extras. Thanks to that apache-airflow distribution does not
have any dynamically calculated dependencies.
* the apache-airflow-core distribution hs "hatch_build_airflow_core.py"
build hooks that add custom build target and implement custom
cleanup in order to implement compiling assets as part of the build.
* During the move, the following changes were applied for consistency:
* packages when used in context of distribution packages have been
renamed to "distributions" - including all documentations and
commands in breeze to void confusion with import packages
(see
https://packaging.python.org/en/latest/discussions/distribution-package-vs-import-package/)
* all tests in `airflow-core` follow now the same convention
where tests are in `unit`, `system` and `integration` package.
no extra package has been as second level, because all the
provider tests have "<PROVIDER>" there, so we just have to avoid
naming airflow unit."<PROVIDER>" with the same name as provider.
* all tooling in CI/DEV have been updated to follow the new
structure. We should always build to packages now when we
are building them using `breeze`.
145 lines
6.7 KiB
Python
145 lines
6.7 KiB
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.
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import libcst as cst
|
|
from datamodel_code_generator.format import CustomCodeFormatter
|
|
from libcst.helpers import parse_template_statement
|
|
|
|
AIRFLOW_ROOT_PATH = Path(__file__).parents[2].resolve()
|
|
|
|
|
|
def license_text() -> str:
|
|
license = (AIRFLOW_ROOT_PATH / "scripts" / "ci" / "license-templates" / "LICENSE.txt").read_text()
|
|
return "\n".join(f"# {line}" if line else "#" for line in license.splitlines()) + "\n"
|
|
|
|
|
|
class CodeFormatter(CustomCodeFormatter):
|
|
def apply(self, code: str) -> str:
|
|
code = license_text() + code
|
|
|
|
# Swap "class JsonValue[RootValue]:" for the import from pydantic
|
|
class JsonValueNodeRemover(cst.CSTTransformer):
|
|
def leave_ImportFrom(
|
|
self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
|
|
) -> cst.BaseSmallStatement | cst.FlattenSentinel[cst.BaseSmallStatement] | cst.RemovalSentinel:
|
|
if original_node.module and original_node.module.value == "pydantic":
|
|
new_names = updated_node.names + (cst.ImportAlias(name=cst.Name("JsonValue")),) # type: ignore[operator]
|
|
return updated_node.with_changes(names=new_names)
|
|
return super().leave_ImportFrom(original_node, updated_node)
|
|
|
|
def leave_ClassDef(
|
|
self, original_node: cst.ClassDef, updated_node: cst.ClassDef
|
|
) -> cst.BaseStatement | cst.FlattenSentinel[cst.BaseStatement] | cst.RemovalSentinel:
|
|
if original_node.name.value == "JsonValue":
|
|
return cst.RemoveFromParent()
|
|
return super().leave_ClassDef(original_node, updated_node)
|
|
|
|
class VersionConstInjtector(cst.CSTTransformer):
|
|
handled = False
|
|
|
|
def __init__(self, api_version: str) -> None:
|
|
self.api_version = api_version
|
|
super().__init__()
|
|
|
|
def leave_ImportFrom(
|
|
self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom
|
|
) -> cst.BaseSmallStatement | cst.FlattenSentinel[cst.BaseSmallStatement] | cst.RemovalSentinel:
|
|
# Ensure we have `from typing import Final`
|
|
if original_node.module and original_node.module.value == "typing":
|
|
new_names = updated_node.names + (cst.ImportAlias(name=cst.Name("Final")),) # type: ignore[operator]
|
|
return updated_node.with_changes(names=new_names)
|
|
return super().leave_ImportFrom(original_node, updated_node)
|
|
|
|
def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef):
|
|
if self.handled:
|
|
return super().leave_ClassDef(original_node, updated_node)
|
|
|
|
self.handled = True
|
|
|
|
const = parse_template_statement(
|
|
"API_VERSION: Final[str] = {api_version}",
|
|
api_version=cst.SimpleString(f'"{self.api_version}"'),
|
|
)
|
|
return cst.FlattenSentinel([const, updated_node])
|
|
|
|
# Remove Task class that represent a tuple of (task_id, map_index)
|
|
# for `TISkippedDownstreamTasksStatePayload`
|
|
class ModifyTasksAnnotation(cst.CSTTransformer):
|
|
def leave_ClassDef(
|
|
self, original_node: cst.ClassDef, updated_node: cst.ClassDef
|
|
) -> cst.BaseStatement | cst.FlattenSentinel[cst.BaseStatement] | cst.RemovalSentinel:
|
|
if original_node.name.value == "Tasks":
|
|
return cst.RemoveFromParent()
|
|
return super().leave_ClassDef(original_node, updated_node)
|
|
|
|
def leave_AnnAssign(
|
|
self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign
|
|
) -> cst.AnnAssign | cst.RemovalSentinel:
|
|
"""
|
|
Replaces `tasks: Annotated[list[str | Tasks], Field(title="Tasks")]`
|
|
with `tasks: Annotated[list[str | tuple[str, int]], Field(title="Tasks")]`
|
|
only if inside `TISkippedDownstreamTasksStatePayload`.
|
|
"""
|
|
# Check if the target is 'tasks'
|
|
if not isinstance(updated_node.target, cst.Name) or updated_node.target.value != "tasks":
|
|
return updated_node
|
|
|
|
if not isinstance(updated_node.annotation, cst.Annotation):
|
|
return updated_node
|
|
|
|
# Create a replacement for 'Tasks' -> 'tuple[str, int]'
|
|
tuple_type = cst.Subscript(
|
|
value=cst.Name("tuple"),
|
|
slice=[
|
|
cst.SubscriptElement(cst.Index(cst.Name("str"))),
|
|
cst.SubscriptElement(cst.Index(cst.Name("int"))),
|
|
],
|
|
)
|
|
|
|
# Transformer to replace all instances of 'Tasks' with 'tuple[str, int]'
|
|
class TasksReplacer(cst.CSTTransformer):
|
|
def leave_Name(
|
|
self, original_node: cst.Name, updated_node: cst.Name
|
|
) -> cst.BaseExpression:
|
|
if original_node.value == "Tasks":
|
|
return tuple_type
|
|
return updated_node
|
|
|
|
# Apply the transformation to the annotation part only
|
|
new_annotation = updated_node.annotation.visit(TasksReplacer())
|
|
return updated_node.with_changes(annotation=new_annotation)
|
|
|
|
source_tree = cst.parse_module(code)
|
|
modified_tree = source_tree.visit(JsonValueNodeRemover())
|
|
if api_version := self.formatter_kwargs.get("api_version"):
|
|
modified_tree = modified_tree.visit(VersionConstInjtector(api_version))
|
|
modified_tree = modified_tree.visit(ModifyTasksAnnotation())
|
|
code = modified_tree.code
|
|
|
|
result = subprocess.check_output(
|
|
["ruff", "check", "--fix-only", "--unsafe-fixes", "--quiet", "--preview", "-"],
|
|
input=code,
|
|
text=True,
|
|
)
|
|
|
|
return result
|