Skip to content

Commit b8c297d

Browse files
committed
Test standard provider with Airflow 2.8 and 2.9
The standard provider has now min version of Airflow = 2.8 since apache#43553, but we have not tested it for Airflow 2.8 and 2.9.
1 parent 01302a1 commit b8c297d

12 files changed

Lines changed: 253 additions & 108 deletions

File tree

dev/breeze/src/airflow_breeze/global_constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,13 +574,13 @@ def get_airflow_extras():
574574
{
575575
"python-version": "3.9",
576576
"airflow-version": "2.8.4",
577-
"remove-providers": "cloudant fab edge standard",
577+
"remove-providers": "cloudant fab edge",
578578
"run-tests": "true",
579579
},
580580
{
581581
"python-version": "3.9",
582582
"airflow-version": "2.9.3",
583-
"remove-providers": "cloudant edge standard",
583+
"remove-providers": "cloudant edge",
584584
"run-tests": "true",
585585
},
586586
{

docs/apache-airflow/howto/operator/python.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ With some limitations, you can also use ``Context`` in virtual environments.
253253

254254
You can also use ``get_current_context()`` in the same way as before, but with some limitations.
255255

256-
* Requires ``pydantic>=2``.
256+
* Requires ``apache-airflow>=3.0.0``.
257257

258258
* Set ``use_airflow_context`` to ``True`` to call ``get_current_context()`` in the virtual environment.
259259

providers/src/airflow/providers/standard/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@
1515
# KIND, either express or implied. See the License for the
1616
# specific language governing permissions and limitations
1717
# under the License.
18+
from __future__ import annotations
19+
20+
from packaging.version import Version
21+
22+
from airflow import __version__ as airflow_version
23+
24+
AIRFLOW_VERSION = Version(airflow_version)
25+
AIRFLOW_V_2_10_PLUS = Version(AIRFLOW_VERSION.base_version) >= Version("2.10.0")
26+
AIRFLOW_V_3_0_PLUS = Version(AIRFLOW_VERSION.base_version) >= Version("3.0.0")

providers/src/airflow/providers/standard/operators/python.py

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
from typing import TYPE_CHECKING, Any, Callable, Collection, Iterable, Mapping, NamedTuple, Sequence, cast
3636

3737
import lazy_object_proxy
38-
from packaging.version import Version
3938

40-
from airflow import __version__ as airflow_version
4139
from airflow.exceptions import (
4240
AirflowConfigException,
4341
AirflowException,
@@ -50,21 +48,19 @@
5048
from airflow.models.taskinstance import _CURRENT_CONTEXT
5149
from airflow.models.variable import Variable
5250
from airflow.operators.branch import BranchMixIn
51+
from airflow.providers.standard import AIRFLOW_V_2_10_PLUS, AIRFLOW_V_3_0_PLUS
5352
from airflow.providers.standard.utils.python_virtualenv import prepare_virtualenv, write_python_script
5453
from airflow.settings import _ENABLE_AIP_44
5554
from airflow.typing_compat import Literal
5655
from airflow.utils import hashlib_wrapper
57-
from airflow.utils.context import context_copy_partial, context_get_outlet_events, context_merge
56+
from airflow.utils.context import context_copy_partial, context_merge
5857
from airflow.utils.file import get_unique_dag_module_name
59-
from airflow.utils.operator_helpers import ExecutionCallableRunner, KeywordParameters
60-
from airflow.utils.process_utils import execute_in_subprocess
58+
from airflow.utils.operator_helpers import KeywordParameters
59+
from airflow.utils.process_utils import execute_in_subprocess, execute_in_subprocess_with_kwargs
6160
from airflow.utils.session import create_session
6261

6362
log = logging.getLogger(__name__)
6463

65-
AIRFLOW_VERSION = Version(airflow_version)
66-
AIRFLOW_V_3_0_PLUS = Version(AIRFLOW_VERSION.base_version) >= Version("3.0.0")
67-
6864
if TYPE_CHECKING:
6965
from pendulum.datetime import DateTime
7066

@@ -187,7 +183,15 @@ def __init__(
187183
def execute(self, context: Context) -> Any:
188184
context_merge(context, self.op_kwargs, templates_dict=self.templates_dict)
189185
self.op_kwargs = self.determine_kwargs(context)
190-
self._asset_events = context_get_outlet_events(context)
186+
187+
if AIRFLOW_V_3_0_PLUS:
188+
from airflow.utils.context import context_get_outlet_events
189+
190+
self._asset_events = context_get_outlet_events(context)
191+
elif AIRFLOW_V_2_10_PLUS:
192+
from airflow.utils.context import context_get_outlet_events
193+
194+
self._dataset_events = context_get_outlet_events(context)
191195

192196
return_value = self.execute_callable()
193197
if self.show_return_value_in_logs:
@@ -206,7 +210,15 @@ def execute_callable(self) -> Any:
206210
207211
:return: the return value of the call.
208212
"""
209-
runner = ExecutionCallableRunner(self.python_callable, self._asset_events, logger=self.log)
213+
try:
214+
from airflow.utils.operator_helpers import ExecutionCallableRunner
215+
216+
asset_events = self._asset_events if AIRFLOW_V_3_0_PLUS else self._dataset_events
217+
218+
runner = ExecutionCallableRunner(self.python_callable, asset_events, logger=self.log)
219+
except ImportError:
220+
# Handle Pre Airflow 3.10 case where ExecutionCallableRunner was not available
221+
return self.python_callable(*self.op_args, **self.op_kwargs)
210222
return runner.run(*self.op_args, **self.op_kwargs)
211223

212224

@@ -348,7 +360,6 @@ class _BasePythonVirtualenvOperator(PythonOperator, metaclass=ABCMeta):
348360
"ds_nodash",
349361
"expanded_ti_count",
350362
"inlets",
351-
"map_index_template",
352363
"next_ds",
353364
"next_ds_nodash",
354365
"outlets",
@@ -551,18 +562,25 @@ def _execute_python_callable_in_subprocess(self, python_path: Path):
551562
env_vars.update(self.env_vars)
552563

553564
try:
554-
execute_in_subprocess(
555-
cmd=[
556-
os.fspath(python_path),
557-
os.fspath(script_path),
558-
os.fspath(input_path),
559-
os.fspath(output_path),
560-
os.fspath(string_args_path),
561-
os.fspath(termination_log_path),
562-
os.fspath(airflow_context_path),
563-
],
564-
env=env_vars,
565-
)
565+
cmd: list[str] = [
566+
os.fspath(python_path),
567+
os.fspath(script_path),
568+
os.fspath(input_path),
569+
os.fspath(output_path),
570+
os.fspath(string_args_path),
571+
os.fspath(termination_log_path),
572+
os.fspath(airflow_context_path),
573+
]
574+
if AIRFLOW_V_2_10_PLUS:
575+
execute_in_subprocess(
576+
cmd=cmd,
577+
env=env_vars,
578+
)
579+
else:
580+
execute_in_subprocess_with_kwargs(
581+
cmd=cmd,
582+
env=env_vars,
583+
)
566584
except subprocess.CalledProcessError as e:
567585
if e.returncode in self.skip_on_exit_code:
568586
raise AirflowSkipException(f"Process exited with code {e.returncode}. Skipping.")
@@ -697,10 +715,15 @@ def __init__(
697715
raise AirflowException(
698716
"Passing non-string types (e.g. int or float) as python_version not supported"
699717
)
700-
718+
if use_airflow_context and not AIRFLOW_V_3_0_PLUS:
719+
raise AirflowException(
720+
"The `use_airflow_context=True` is only supported in Airflow 3.0.0 and later."
721+
)
701722
if use_airflow_context and (not expect_airflow and not system_site_packages):
702-
error_msg = "use_airflow_context is set to True, but expect_airflow and system_site_packages are set to False."
703-
raise AirflowException(error_msg)
723+
raise AirflowException(
724+
"The `use_airflow_context` parameter is set to True, but "
725+
"expect_airflow and system_site_packages are set to False."
726+
)
704727
if not requirements:
705728
self.requirements: list[str] = []
706729
elif isinstance(requirements, str):
@@ -976,9 +999,14 @@ def __init__(
976999
):
9771000
if not python:
9781001
raise ValueError("Python Path must be defined in ExternalPythonOperator")
1002+
if use_airflow_context and not AIRFLOW_V_3_0_PLUS:
1003+
raise AirflowException(
1004+
"The `use_airflow_context=True` is only supported in Airflow 3.0.0 and later."
1005+
)
9791006
if use_airflow_context and not expect_airflow:
980-
error_msg = "use_airflow_context is set to True, but expect_airflow is set to False."
981-
raise AirflowException(error_msg)
1007+
raise AirflowException(
1008+
"The `use_airflow_context` parameter is set to True, but expect_airflow is set to False."
1009+
)
9821010
self.python = python
9831011
self.expect_pendulum = expect_pendulum
9841012
super().__init__(

providers/src/airflow/providers/standard/sensors/date_time.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,27 @@
1818
from __future__ import annotations
1919

2020
import datetime
21+
from dataclasses import dataclass
2122
from typing import TYPE_CHECKING, Any, NoReturn, Sequence
2223

24+
from airflow.providers.standard import AIRFLOW_V_3_0_PLUS
2325
from airflow.sensors.base import BaseSensorOperator
24-
from airflow.triggers.base import StartTriggerArgs
26+
27+
try:
28+
from airflow.triggers.base import StartTriggerArgs
29+
except ImportError:
30+
# TODO: Remove this when min airflow version is 2.10.0 for standard provider
31+
@dataclass
32+
class StartTriggerArgs: # type: ignore[no-redef]
33+
"""Arguments required for start task execution from triggerer."""
34+
35+
trigger_cls: str
36+
next_method: str
37+
trigger_kwargs: dict[str, Any] | None = None
38+
next_kwargs: dict[str, Any] | None = None
39+
timeout: datetime.timedelta | None = None
40+
41+
2542
from airflow.triggers.temporal import DateTimeTrigger
2643
from airflow.utils import timezone
2744

@@ -125,7 +142,9 @@ def execute(self, context: Context) -> NoReturn:
125142
trigger=DateTimeTrigger(
126143
moment=timezone.parse(self.target_time),
127144
end_from_trigger=self.end_from_trigger,
128-
),
145+
)
146+
if AIRFLOW_V_3_0_PLUS
147+
else DateTimeTrigger(moment=timezone.parse(self.target_time)),
129148
)
130149

131150
def execute_complete(self, context: Context, event: Any = None) -> None:

providers/src/airflow/providers/standard/sensors/time.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,27 @@
1818
from __future__ import annotations
1919

2020
import datetime
21+
from dataclasses import dataclass
2122
from typing import TYPE_CHECKING, Any, NoReturn
2223

24+
from airflow.providers.standard import AIRFLOW_V_2_10_PLUS
2325
from airflow.sensors.base import BaseSensorOperator
24-
from airflow.triggers.base import StartTriggerArgs
26+
27+
try:
28+
from airflow.triggers.base import StartTriggerArgs
29+
except ImportError:
30+
# TODO: Remove this when min airflow version is 2.10.0 for standard provider
31+
@dataclass
32+
class StartTriggerArgs: # type: ignore[no-redef]
33+
"""Arguments required for start task execution from triggerer."""
34+
35+
trigger_cls: str
36+
next_method: str
37+
trigger_kwargs: dict[str, Any] | None = None
38+
next_kwargs: dict[str, Any] | None = None
39+
timeout: datetime.timedelta | None = None
40+
41+
2542
from airflow.triggers.temporal import DateTimeTrigger
2643
from airflow.utils import timezone
2744

@@ -102,7 +119,9 @@ def __init__(
102119

103120
def execute(self, context: Context) -> NoReturn:
104121
self.defer(
105-
trigger=DateTimeTrigger(moment=self.target_datetime, end_from_trigger=self.end_from_trigger),
122+
trigger=DateTimeTrigger(moment=self.target_datetime, end_from_trigger=self.end_from_trigger)
123+
if AIRFLOW_V_2_10_PLUS
124+
else DateTimeTrigger(moment=self.target_datetime),
106125
method_name="execute_complete",
107126
)
108127

providers/src/airflow/providers/standard/sensors/time_delta.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from airflow.configuration import conf
2525
from airflow.exceptions import AirflowSkipException
26+
from airflow.providers.standard import AIRFLOW_V_3_0_PLUS
2627
from airflow.sensors.base import BaseSensorOperator
2728
from airflow.triggers.temporal import DateTimeTrigger, TimeDeltaTrigger
2829
from airflow.utils import timezone
@@ -81,7 +82,10 @@ def execute(self, context: Context) -> bool | NoReturn:
8182
# If the target datetime is in the past, return immediately
8283
return True
8384
try:
84-
trigger = DateTimeTrigger(moment=target_dttm, end_from_trigger=self.end_from_trigger)
85+
if AIRFLOW_V_3_0_PLUS:
86+
trigger = DateTimeTrigger(moment=target_dttm, end_from_trigger=self.end_from_trigger)
87+
else:
88+
trigger = DateTimeTrigger(moment=target_dttm)
8589
except (TypeError, ValueError) as e:
8690
if self.soft_fail:
8791
raise AirflowSkipException("Skipping due to soft_fail is set to True.") from e
@@ -121,7 +125,9 @@ def __init__(
121125
def execute(self, context: Context) -> None:
122126
if self.deferrable:
123127
self.defer(
124-
trigger=TimeDeltaTrigger(self.time_to_wait, end_from_trigger=True),
128+
trigger=TimeDeltaTrigger(self.time_to_wait, end_from_trigger=True)
129+
if AIRFLOW_V_3_0_PLUS
130+
else TimeDeltaTrigger(self.time_to_wait),
125131
method_name="execute_complete",
126132
)
127133
else:

providers/tests/common/sql/operators/test_sql.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353

5454
pytestmark = [
5555
pytest.mark.db_test,
56-
pytest.mark.skipif(reason="Tests for Airflow 2.8.0+ only"),
5756
pytest.mark.skip_if_database_isolation_mode,
5857
]
5958

providers/tests/openlineage/plugins/test_utils.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@
5757
if AIRFLOW_V_3_0_PLUS:
5858
from airflow.utils.types import DagRunTriggeredByType
5959

60-
BASH_OPERATOR_PATH = "airflow.providers.standard.operators.bash"
61-
PYTHON_OPERATOR_PATH = "airflow.providers.standard.operators.python"
62-
if not AIRFLOW_V_2_10_PLUS:
63-
BASH_OPERATOR_PATH = "airflow.operators.bash"
64-
PYTHON_OPERATOR_PATH = "airflow.operators.python"
65-
6660

6761
class SafeStrDict(dict):
6862
def __str__(self):
@@ -276,7 +270,7 @@ def test_get_fully_qualified_class_name():
276270
from airflow.providers.openlineage.plugins.adapter import OpenLineageAdapter
277271

278272
result = get_fully_qualified_class_name(BashOperator(task_id="test", bash_command="exit 0;"))
279-
assert result == f"{BASH_OPERATOR_PATH}.BashOperator"
273+
assert result == "airflow.providers.standard.operators.bash.BashOperator"
280274

281275
result = get_fully_qualified_class_name(OpenLineageAdapter())
282276
assert result == "airflow.providers.openlineage.plugins.adapter.OpenLineageAdapter"
@@ -292,8 +286,8 @@ def test_is_operator_disabled(mock_disabled_operators):
292286
assert is_operator_disabled(op) is False
293287

294288
mock_disabled_operators.return_value = {
295-
f"{BASH_OPERATOR_PATH}.BashOperator",
296-
f"{PYTHON_OPERATOR_PATH}.PythonOperator",
289+
"airflow.providers.standard.operators.bash.BashOperator",
290+
"airflow.providers.standard.operators.python.PythonOperator",
297291
}
298292
assert is_operator_disabled(op) is True
299293

providers/tests/openlineage/utils/test_utils.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,11 @@
4343
from airflow.utils.task_group import TaskGroup
4444
from airflow.utils.types import DagRunType
4545

46-
from tests_common.test_utils.compat import AIRFLOW_V_2_10_PLUS, BashOperator, PythonOperator
46+
from tests_common.test_utils.compat import BashOperator, PythonOperator
4747
from tests_common.test_utils.mock_operators import MockOperator
4848

4949
BASH_OPERATOR_PATH = "airflow.providers.standard.operators.bash"
5050
PYTHON_OPERATOR_PATH = "airflow.providers.standard.operators.python"
51-
if not AIRFLOW_V_2_10_PLUS:
52-
BASH_OPERATOR_PATH = "airflow.operators.bash"
53-
PYTHON_OPERATOR_PATH = "airflow.operators.python"
5451

5552

5653
class CustomOperatorForTest(BashOperator):

0 commit comments

Comments
 (0)