Skip to content

Commit 1d06374

Browse files
authored
Hook into Mypy to get rid of those cast() (#26023)
1 parent 6a615ee commit 1d06374

20 files changed

Lines changed: 118 additions & 44 deletions

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ repos:
239239
files: \.py$|\.pyi$
240240
# To keep consistent with the global isort skip config defined in setup.cfg
241241
exclude: ^airflow/_vendor/|^build/.*$|^venv/.*$|^\.tox/.*$
242+
args:
243+
# These -p options are duplicated to known_first_party in setup.cfg,
244+
# Please keep these in sync for now. (See comments there for details.)
245+
- -p=airflow
246+
- -p=airflow_breeze
247+
- -p=docker_tests
248+
- -p=docs
249+
- -p=kubernetes_tests
250+
- -p=tests
242251
- repo: https://github.com/pycqa/pydocstyle
243252
rev: 6.1.1
244253
hooks:

airflow/mypy/plugin/outputs.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
from __future__ import annotations
20+
21+
from typing import Callable
22+
23+
from mypy.plugin import AttributeContext, MethodContext, Plugin
24+
from mypy.types import AnyType, Type, TypeOfAny
25+
26+
OUTPUT_PROPERTIES = {
27+
"airflow.models.baseoperator.BaseOperator.output",
28+
"airflow.models.mappedoperator.MappedOperator.output",
29+
}
30+
31+
TASK_CALL_FUNCTIONS = {
32+
"airflow.decorators.base.Task.__call__",
33+
}
34+
35+
36+
class OperatorOutputPlugin(Plugin):
37+
"""Plugin to convert XComArg to the runtime type.
38+
39+
This allows us to pass an *XComArg* to a downstream task, such as::
40+
41+
@task
42+
def f(a: str) -> int:
43+
return len(a)
44+
45+
f(op.output) # "op" is an operator instance.
46+
f(g()) # "g" is a taskflow task.
47+
48+
where the *a* argument of ``f`` should accept a *str* at runtime, but can be
49+
provided with an *XComArg* in the DAG.
50+
51+
In the long run, it is probably a good idea to make *XComArg* a generic that
52+
carries information about the task's return type, and build the entire XCom
53+
mechanism into the type checker. But Python's type system is still limiting
54+
in this regard now, and (using the above example) we yet to have a good way
55+
to convert ``f``'s argument list from ``[str]`` to ``[XComArg[str] | str]``.
56+
Perhaps *ParamSpec* will be extended enough one day to accommodate this.
57+
"""
58+
59+
@staticmethod
60+
def _treat_as_any(context: AttributeContext | MethodContext) -> Type:
61+
"""Pretend *XComArg* is actually *typing.Any*."""
62+
return AnyType(TypeOfAny.special_form, line=context.context.line, column=context.context.column)
63+
64+
def get_attribute_hook(self, fullname: str) -> Callable[[AttributeContext], Type] | None:
65+
if fullname not in OUTPUT_PROPERTIES:
66+
return None
67+
return self._treat_as_any
68+
69+
def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | None:
70+
if fullname not in TASK_CALL_FUNCTIONS:
71+
return None
72+
return self._treat_as_any
73+
74+
75+
def plugin(version: str):
76+
return OperatorOutputPlugin

airflow/providers/amazon/aws/example_dags/example_ecs.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
# under the License.
1717

1818
from datetime import datetime
19-
from typing import cast
2019

2120
from airflow import DAG
2221
from airflow.models.baseoperator import chain
@@ -100,7 +99,7 @@
10099
)
101100
# [END howto_operator_ecs_register_task_definition]
102101

103-
registered_task_definition = cast(str, register_task.output)
102+
registered_task_definition = register_task.output
104103

105104
# [START howto_sensor_ecs_task_definition_state]
106105
await_task_definition = EcsTaskDefinitionStateSensor(

airflow/providers/amazon/aws/example_dags/example_emr.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
# under the License.
1818
import os
1919
from datetime import datetime
20-
from typing import cast
2120

2221
from airflow import DAG
2322
from airflow.models.baseoperator import chain
@@ -80,7 +79,7 @@
8079
)
8180
# [END howto_operator_emr_create_job_flow]
8281

83-
job_flow_id = cast(str, job_flow_creator.output)
82+
job_flow_id = job_flow_creator.output
8483

8584
# [START howto_sensor_emr_job_flow]
8685
job_sensor = EmrJobFlowSensor(task_id='check_job_flow', job_flow_id=job_flow_id)

airflow/providers/google/cloud/example_dags/example_datafusion.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"""
2121
import os
2222
from datetime import datetime
23-
from typing import cast
2423

2524
from airflow import models
2625
from airflow.operators.bash import BashOperator
@@ -222,7 +221,7 @@
222221
start_pipeline_sensor = CloudDataFusionPipelineStateSensor(
223222
task_id="pipeline_state_sensor",
224223
pipeline_name=PIPELINE_NAME,
225-
pipeline_id=cast(str, start_pipeline_async.output),
224+
pipeline_id=start_pipeline_async.output,
226225
expected_statuses=["COMPLETED"],
227226
failure_statuses=["FAILED"],
228227
instance_name=INSTANCE_NAME,

airflow/providers/google/cloud/example_dags/example_looker.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"""
2222

2323
from datetime import datetime
24-
from typing import cast
2524

2625
from airflow import models
2726
from airflow.providers.google.cloud.operators.looker import LookerStartPdtBuildOperator
@@ -44,7 +43,7 @@
4443
check_pdt_task_async_sensor = LookerCheckPdtBuildSensor(
4544
task_id='check_pdt_task_async_sensor',
4645
looker_conn_id='your_airflow_connection_for_looker',
47-
materialization_id=cast(str, start_pdt_task_async.output),
46+
materialization_id=start_pdt_task_async.output,
4847
poke_interval=10,
4948
)
5049
# [END cloud_looker_async_start_pdt_sensor]

airflow/providers/google/cloud/example_dags/example_vision.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333

3434
import os
3535
from datetime import datetime
36-
from typing import cast
3736

3837
from airflow import models
3938
from airflow.operators.bash import BashOperator
@@ -136,7 +135,7 @@
136135
)
137136
# [END howto_operator_vision_product_set_create]
138137

139-
product_set_create_output = cast(str, product_set_create.output)
138+
product_set_create_output = product_set_create.output
140139

141140
# [START howto_operator_vision_product_set_get]
142141
product_set_get = CloudVisionGetProductSetOperator(
@@ -173,7 +172,7 @@
173172
)
174173
# [END howto_operator_vision_product_create]
175174

176-
product_create_output = cast(str, product_create.output)
175+
product_create_output = product_create.output
177176

178177
# [START howto_operator_vision_product_get]
179178
product_get = CloudVisionGetProductOperator(

setup.cfg

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ no_implicit_optional = True
190190
warn_redundant_casts = True
191191
warn_unused_ignores = False
192192
plugins =
193-
airflow.mypy.plugin.decorators
193+
airflow.mypy.plugin.decorators,
194+
airflow.mypy.plugin.outputs
194195
pretty = True
195196
show_error_codes = True
196197

@@ -206,7 +207,10 @@ no_implicit_optional = False
206207
line_length=110
207208
combine_as_imports = true
208209
default_section = THIRDPARTY
209-
known_first_party=airflow,airflow_breeze,tests,docs
210+
# This is duplicated with arguments in .pre-commit-config.yaml because isort is
211+
# having some issues picking up these config files. Please keep these in sync
212+
# for now and track the isort issue: https://github.com/PyCQA/isort/issues/1889
213+
known_first_party = airflow,airflow_breeze,docker_tests,docs,kubernetes_tests,tests
210214
# Need to be consistent with the exclude config defined in pre-commit-config.yaml
211-
skip=build,.tox,venv
215+
skip = build,.tox,venv
212216
profile = black

tests/system/providers/airbyte/example_airbyte_trigger_job.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import os
2222
from datetime import datetime, timedelta
23-
from typing import cast
2423

2524
from airflow import DAG
2625
from airflow.providers.airbyte.operators.airbyte import AirbyteTriggerSyncOperator
@@ -55,7 +54,7 @@
5554

5655
airbyte_sensor = AirbyteJobSensor(
5756
task_id='airbyte_sensor_source_dest_example',
58-
airbyte_job_id=cast(int, async_source_destination.output),
57+
airbyte_job_id=async_source_destination.output,
5958
)
6059
# [END howto_operator_airbyte_asynchronous]
6160

tests/system/providers/amazon/aws/example_athena.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
from datetime import datetime
18-
from typing import cast
1918

2019
import boto3
2120

@@ -122,7 +121,7 @@ def read_results_from_s3(bucket_name, query_execution_id):
122121
# [START howto_sensor_athena]
123122
await_query = AthenaSensor(
124123
task_id='await_query',
125-
query_execution_id=cast(str, read_table.output),
124+
query_execution_id=read_table.output,
126125
)
127126
# [END howto_sensor_athena]
128127

0 commit comments

Comments
 (0)