Skip to content

Commit bc25384

Browse files
committed
recommit without precommit changes
1 parent 9dff03b commit bc25384

File tree

9 files changed

+197
-4
lines changed

9 files changed

+197
-4
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,4 +1525,12 @@ repos:
15251525
additional_dependencies: ['rich>=12.4.4', 'ruff==0.11.13']
15261526
files: ^providers/.*/src/airflow/providers/.*version_compat.*\.py$
15271527
require_serial: true
1528+
- id: verify-signatures
1529+
name: Verify decorators of missing operator params
1530+
language: python
1531+
entry: ./scripts/ci/pre_commit/verify_signature_consistency.py
1532+
pass_filenames: false
1533+
additional_dependencies: ['rich>=12.4.4']
1534+
always_run: true
1535+
files: ^(providers/.*/)?airflow/.*/(sensors|operators)/.*\.py$
15281536
## ONLY ADD PRE-COMMITS HERE THAT REQUIRE CI IMAGE

contributing-docs/08_static_code_checks.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,8 @@ require Breeze Docker image to be built locally.
412412
+-----------------------------------------------------------+--------------------------------------------------------+---------+
413413
| validate-operators-init | No templated field logic checks in operator __init__ | |
414414
+-----------------------------------------------------------+--------------------------------------------------------+---------+
415+
| verify-signatures | Verify decorators of missing operator params | * |
416+
+-----------------------------------------------------------+--------------------------------------------------------+---------+
415417
| yamllint | Check YAML files with yamllint | |
416418
+-----------------------------------------------------------+--------------------------------------------------------+---------+
417419
| zizmor | Run zizmor to check for github workflow syntax errors | |

dev/breeze/doc/images/output_setup.svg

Lines changed: 1 addition & 1 deletion
Loading

dev/breeze/doc/images/output_static-checks.svg

Lines changed: 1 addition & 1 deletion
Loading
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
f31561bb0408a8cab278aad420d260b3
1+
9564095f946432b06413132ab87cac1d

dev/breeze/src/airflow_breeze/pre_commit_ids.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"update-vendored-in-k8s-json-schema",
159159
"update-version",
160160
"validate-operators-init",
161+
"verify-signatures",
161162
"yamllint",
162163
"zizmor",
163164
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
from __future__ import annotations
21+
22+
import sys
23+
from pathlib import Path
24+
25+
sys.path.insert(0, str(Path(__file__).parent.resolve()))
26+
from common_precommit_utils import (
27+
initialize_breeze_precommit,
28+
run_command_via_breeze_shell,
29+
validate_cmd_result,
30+
)
31+
32+
initialize_breeze_precommit(__name__, __file__)
33+
34+
cmd_result = run_command_via_breeze_shell(
35+
["python3", "/opt/airflow/scripts/in_container/run_signature_consistency_verify.py"],
36+
backend="sqlite",
37+
skip_environment_initialization=False,
38+
)
39+
40+
validate_cmd_result(cmd_result)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env python
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
from __future__ import annotations
20+
21+
import inspect
22+
import sys
23+
24+
import libcst as cst
25+
from in_container_utils import AIRFLOW_ROOT_PATH, console
26+
27+
DECORATOR_OPERATOR_MAP = {
28+
"kubernetes": "airflow.providers.cncf.kubernetes.operators.pod.KubernetesPodOperator",
29+
"sensor": "airflow.sdk.bases.sensor.BaseSensorOperator",
30+
"virtualenv": "airflow.providers.standard.operators.python.PythonVirtualenvOperator",
31+
"branch_virtualenv": "airflow.providers.standard.operators.python.BranchPythonVirtualenvOperator",
32+
# Add more here...
33+
}
34+
DECORATOR_PYI_PATH = (
35+
AIRFLOW_ROOT_PATH / "task-sdk" / "src" / "airflow" / "sdk" / "definitions" / "decorators" / "__init__.pyi"
36+
)
37+
decorator_pyi_file_content = DECORATOR_PYI_PATH.read_text()
38+
39+
40+
def extract_function_params(code, function_name, return_type):
41+
"""Extracts parameters from a specific function definition in the given code.
42+
43+
Args:
44+
code (str): The Python code to parse.
45+
function_name (str): The name of the function to extract parameters from.
46+
return_type (str): As the pyi file has multiple @overload decorator, extract function param based on return type.
47+
48+
Returns:
49+
list: A list of parameter names, or None if the function is not found.
50+
"""
51+
module = cst.parse_module(code)
52+
53+
class FunctionParamExtractor(cst.CSTVisitor):
54+
def __init__(self, target_function_name, target_return_type):
55+
self.target_function_name = target_function_name
56+
self.target_return_type = target_return_type
57+
self.params: list[str] = []
58+
59+
def visit_FunctionDef(self, node):
60+
# Match function name
61+
if node.name.value == self.target_function_name:
62+
if node.returns:
63+
annotation = node.returns.annotation
64+
if isinstance(annotation, cst.Name) and annotation.value == self.target_return_type:
65+
parameters_node = node.params
66+
self.params.extend(param.name.value for param in parameters_node.params)
67+
self.params.extend(param.name.value for param in parameters_node.kwonly_params)
68+
self.params.extend(param.name.value for param in parameters_node.posonly_params)
69+
if parameters_node.star_kwarg:
70+
self.params.append(parameters_node.star_kwarg.name.value)
71+
return False # Stop traversing after finding the real function
72+
return True # Keep traversing
73+
74+
extractor = FunctionParamExtractor(function_name, return_type)
75+
module.visit(extractor)
76+
return extractor.params
77+
78+
79+
def get_decorator_params(decorator_name: str):
80+
params = extract_function_params(decorator_pyi_file_content, decorator_name, "TaskDecorator")
81+
return set(params)
82+
83+
84+
def get_operator_params(operator_path: str):
85+
console.print("Operator path:", operator_path)
86+
module_path, class_name = operator_path.rsplit(".", 1)
87+
module = __import__(module_path, fromlist=[class_name])
88+
operator_cls = getattr(module, class_name)
89+
sig = inspect.signature(operator_cls.__init__)
90+
return set(p for p in sig.parameters.keys() if p not in ("self", "args", "kwargs"))
91+
92+
93+
def verify_signature_consistency():
94+
failure = False
95+
console.print("Verify signature consistency")
96+
for decorator, operator_path in DECORATOR_OPERATOR_MAP.items():
97+
decorator_params = get_decorator_params(decorator)
98+
operator_params = get_operator_params(operator_path)
99+
missing_in_decorator = operator_params - decorator_params
100+
101+
ignored = {"kwargs", "args", "self", "python_callable", "op_args", "op_kwargs"}
102+
missing_in_decorator -= ignored
103+
if missing_in_decorator:
104+
failure = True
105+
console.print(f"[yellow]Missing params in[/] [bold]__init__.py[/]: {missing_in_decorator}")
106+
if failure:
107+
console.print("[red]Some of the decorator signatures are missing in __init__.py[/]")
108+
sys.exit(1)
109+
console.print("[green]All decorator signature matches[/]")
110+
sys.exit(0)
111+
112+
113+
if __name__ == "__main__":
114+
verify_signature_consistency()

0 commit comments

Comments
 (0)