|
31 | 31 | from unittest import mock |
32 | 32 | from unittest.mock import call, patch |
33 | 33 |
|
| 34 | +import attrs |
34 | 35 | import pandas as pd |
35 | 36 | import pytest |
36 | 37 | import structlog |
|
78 | 79 | ) |
79 | 80 | from airflow.sdk.bases.operator import ExecutorSafeguard |
80 | 81 | from airflow.sdk.bases.xcom import BaseXCom |
| 82 | +from airflow.sdk.coordinators._dag_importer import CoordinatorDagImporter |
| 83 | +from airflow.sdk.coordinators._subprocess import SubprocessCoordinator |
81 | 84 | from airflow.sdk.definitions._internal.types import NOTSET, SET_DURING_EXECUTION, is_arg_set |
82 | 85 | from airflow.sdk.definitions.asset import Asset, AssetAlias, AssetUniqueKey, AssetUriRef, Dataset, Model |
83 | 86 | from airflow.sdk.definitions.param import DagParam |
|
175 | 178 | TaskRunnerMarker, |
176 | 179 | _defer_task, |
177 | 180 | _execute_task, |
| 181 | + _is_native_dag_file, |
178 | 182 | _make_task_span, |
179 | 183 | _push_xcom_if_needed, |
180 | 184 | _register_deserialization_allowed_classes, |
@@ -912,6 +916,94 @@ def test_parse_dag_from_bundle_importer(tmp_path: Path, make_ti_context): |
912 | 916 | assert ti.task.dag.relative_fileloc == "listed.dags" |
913 | 917 |
|
914 | 918 |
|
| 919 | +class NativeDagImporter(CoordinatorDagImporter): |
| 920 | + artifact_suffix = ".native" |
| 921 | + supported_extensions = [".native"] |
| 922 | + |
| 923 | + def get_source_code(self, definition): |
| 924 | + return DagSourceCode(source_code=definition.read_text(), language="native") |
| 925 | + |
| 926 | + |
| 927 | +@attrs.define(kw_only=True) |
| 928 | +class NativeCoordinator(SubprocessCoordinator): |
| 929 | + """A coordinator whose Dag importer claims ``.native`` files in every bundle.""" |
| 930 | + |
| 931 | + def get_dag_importer(self): |
| 932 | + return NativeDagImporter(coordinator=self) |
| 933 | + |
| 934 | + |
| 935 | +@patch("airflow.dag_processing.dagbag.BundleDagBag", autospec=True) |
| 936 | +def test_parse_rejects_a_task_of_a_native_dag(mock_bag, tmp_path: Path, make_ti_context): |
| 937 | + tmp_path.joinpath("dag.native").write_text("{}") |
| 938 | + what = StartupDetails( |
| 939 | + ti=TaskInstance( |
| 940 | + id=uuid7(), |
| 941 | + task_id="a", |
| 942 | + dag_id="native_dag", |
| 943 | + run_id="c", |
| 944 | + try_number=1, |
| 945 | + dag_version_id=uuid7(), |
| 946 | + queue="default", |
| 947 | + ), |
| 948 | + dag_rel_path="dag.native", |
| 949 | + bundle_info=BundleInfo(name="my-bundle", version=None), |
| 950 | + ti_context=make_ti_context(), |
| 951 | + start_date=timezone.utcnow(), |
| 952 | + sentry_integration="", |
| 953 | + ) |
| 954 | + bundle_config = [ |
| 955 | + { |
| 956 | + "name": "my-bundle", |
| 957 | + "classpath": "airflow.dag_processing.bundles.local.LocalDagBundle", |
| 958 | + "kwargs": {"path": str(tmp_path), "refresh_interval": 1}, |
| 959 | + } |
| 960 | + ] |
| 961 | + coordinators = {"native": {"classpath": f"{__name__}.NativeCoordinator", "kwargs": {}}} |
| 962 | + log = mock.Mock() |
| 963 | + |
| 964 | + reset_importer_registry() |
| 965 | + try: |
| 966 | + with ( |
| 967 | + patch.dict( |
| 968 | + os.environ, |
| 969 | + { |
| 970 | + "AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST": json.dumps(bundle_config), |
| 971 | + "AIRFLOW__SDK__COORDINATORS": json.dumps(coordinators), |
| 972 | + }, |
| 973 | + ), |
| 974 | + pytest.raises(SystemExit, match="1"), |
| 975 | + ): |
| 976 | + parse(what, log) |
| 977 | + finally: |
| 978 | + reset_importer_registry() |
| 979 | + |
| 980 | + mock_bag.assert_not_called() |
| 981 | + log.error.assert_called_once_with( |
| 982 | + "A task of a native Lang-SDK Dag cannot run in Python. Route its queue to the coordinator " |
| 983 | + "that parses the Dag, with [sdk] queue_to_coordinator", |
| 984 | + dag_id="native_dag", |
| 985 | + task_id="a", |
| 986 | + queue="default", |
| 987 | + path="dag.native", |
| 988 | + ) |
| 989 | + |
| 990 | + |
| 991 | +@pytest.mark.parametrize( |
| 992 | + ("file_name", "expected"), |
| 993 | + [("dag.native", True), ("dag.py", False), ("dag.pyc", False), ("dags.zip", False)], |
| 994 | +) |
| 995 | +@patch.object(NativeDagImporter, "supported_extensions", [".native", ".py", ".pyc", ".zip"]) |
| 996 | +def test_a_python_dag_file_is_never_native(file_name, expected): |
| 997 | + coordinators = {"native": {"classpath": f"{__name__}.NativeCoordinator", "kwargs": {}}} |
| 998 | + |
| 999 | + reset_importer_registry() |
| 1000 | + try: |
| 1001 | + with patch.dict(os.environ, {"AIRFLOW__SDK__COORDINATORS": json.dumps(coordinators)}): |
| 1002 | + assert _is_native_dag_file(f"/bundle/{file_name}", "my-bundle") is expected |
| 1003 | + finally: |
| 1004 | + reset_importer_registry() |
| 1005 | + |
| 1006 | + |
915 | 1007 | @pytest.mark.parametrize("use_queues", [False, True]) |
916 | 1008 | def test_run_deferred_basic(time_machine, create_runtime_ti, mock_supervisor_comms, use_queues: bool): |
917 | 1009 | """Test that a task can transition to a deferred state.""" |
|
0 commit comments