Skip to content

Commit 7d6a18a

Browse files
authored
Fix default_args application in operator partial (#50525)
1 parent d6e8418 commit 7d6a18a

2 files changed

Lines changed: 80 additions & 58 deletions

File tree

task-sdk/src/airflow/sdk/bases/operator.py

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
)
5656
from airflow.sdk.definitions._internal.decorators import fixup_decorator_warning_stack
5757
from airflow.sdk.definitions._internal.node import validate_key
58-
from airflow.sdk.definitions._internal.types import NOTSET, ArgNotSet, validate_instance_args
58+
from airflow.sdk.definitions._internal.types import NOTSET, validate_instance_args
5959
from airflow.sdk.definitions.edges import EdgeModifier
6060
from airflow.sdk.definitions.mappedoperator import OperatorPartial, validate_mapping_kwargs
6161
from airflow.sdk.definitions.param import ParamsDict
@@ -232,63 +232,48 @@ def partial(
232232
task_id: str,
233233
dag: DAG | None = None,
234234
task_group: TaskGroup | None = None,
235-
start_date: datetime | ArgNotSet = NOTSET,
236-
end_date: datetime | ArgNotSet = NOTSET,
237-
owner: str | ArgNotSet = NOTSET,
238-
email: None | str | Iterable[str] | ArgNotSet = NOTSET,
235+
start_date: datetime = ...,
236+
end_date: datetime = ...,
237+
owner: str = ...,
238+
email: None | str | Iterable[str] = ...,
239239
params: collections.abc.MutableMapping | None = None,
240-
resources: dict[str, Any] | None | ArgNotSet = NOTSET,
241-
trigger_rule: str | ArgNotSet = NOTSET,
242-
depends_on_past: bool | ArgNotSet = NOTSET,
243-
ignore_first_depends_on_past: bool | ArgNotSet = NOTSET,
244-
wait_for_past_depends_before_skipping: bool | ArgNotSet = NOTSET,
245-
wait_for_downstream: bool | ArgNotSet = NOTSET,
246-
retries: int | None | ArgNotSet = NOTSET,
247-
queue: str | ArgNotSet = NOTSET,
248-
pool: str | ArgNotSet = NOTSET,
249-
pool_slots: int | ArgNotSet = NOTSET,
250-
execution_timeout: timedelta | None | ArgNotSet = NOTSET,
251-
max_retry_delay: None | timedelta | float | ArgNotSet = NOTSET,
252-
retry_delay: timedelta | float | ArgNotSet = NOTSET,
253-
retry_exponential_backoff: bool | ArgNotSet = NOTSET,
254-
priority_weight: int | ArgNotSet = NOTSET,
255-
weight_rule: str | PriorityWeightStrategy | ArgNotSet = NOTSET,
256-
sla: timedelta | None | ArgNotSet = NOTSET,
257-
map_index_template: str | None | ArgNotSet = NOTSET,
258-
max_active_tis_per_dag: int | None | ArgNotSet = NOTSET,
259-
max_active_tis_per_dagrun: int | None | ArgNotSet = NOTSET,
260-
on_execute_callback: None
261-
| TaskStateChangeCallback
262-
| list[TaskStateChangeCallback]
263-
| ArgNotSet = NOTSET,
264-
on_failure_callback: None
265-
| TaskStateChangeCallback
266-
| list[TaskStateChangeCallback]
267-
| ArgNotSet = NOTSET,
268-
on_success_callback: None
269-
| TaskStateChangeCallback
270-
| list[TaskStateChangeCallback]
271-
| ArgNotSet = NOTSET,
272-
on_retry_callback: None
273-
| TaskStateChangeCallback
274-
| list[TaskStateChangeCallback]
275-
| ArgNotSet = NOTSET,
276-
on_skipped_callback: None
277-
| TaskStateChangeCallback
278-
| list[TaskStateChangeCallback]
279-
| ArgNotSet = NOTSET,
280-
run_as_user: str | None | ArgNotSet = NOTSET,
281-
executor: str | None | ArgNotSet = NOTSET,
282-
executor_config: dict | None | ArgNotSet = NOTSET,
283-
inlets: Any | None | ArgNotSet = NOTSET,
284-
outlets: Any | None | ArgNotSet = NOTSET,
285-
doc: str | None | ArgNotSet = NOTSET,
286-
doc_md: str | None | ArgNotSet = NOTSET,
287-
doc_json: str | None | ArgNotSet = NOTSET,
288-
doc_yaml: str | None | ArgNotSet = NOTSET,
289-
doc_rst: str | None | ArgNotSet = NOTSET,
290-
task_display_name: str | None | ArgNotSet = NOTSET,
291-
logger_name: str | None | ArgNotSet = NOTSET,
240+
resources: dict[str, Any] | None = ...,
241+
trigger_rule: str = ...,
242+
depends_on_past: bool = ...,
243+
ignore_first_depends_on_past: bool = ...,
244+
wait_for_past_depends_before_skipping: bool = ...,
245+
wait_for_downstream: bool = ...,
246+
retries: int | None = ...,
247+
queue: str = ...,
248+
pool: str = ...,
249+
pool_slots: int = ...,
250+
execution_timeout: timedelta | None = ...,
251+
max_retry_delay: None | timedelta | float = ...,
252+
retry_delay: timedelta | float = ...,
253+
retry_exponential_backoff: bool = ...,
254+
priority_weight: int = ...,
255+
weight_rule: str | PriorityWeightStrategy = ...,
256+
sla: timedelta | None = ...,
257+
map_index_template: str | None = ...,
258+
max_active_tis_per_dag: int | None = ...,
259+
max_active_tis_per_dagrun: int | None = ...,
260+
on_execute_callback: None | TaskStateChangeCallback | list[TaskStateChangeCallback] = ...,
261+
on_failure_callback: None | TaskStateChangeCallback | list[TaskStateChangeCallback] = ...,
262+
on_success_callback: None | TaskStateChangeCallback | list[TaskStateChangeCallback] = ...,
263+
on_retry_callback: None | TaskStateChangeCallback | list[TaskStateChangeCallback] = ...,
264+
on_skipped_callback: None | TaskStateChangeCallback | list[TaskStateChangeCallback] = ...,
265+
run_as_user: str | None = ...,
266+
executor: str | None = ...,
267+
executor_config: dict | None = ...,
268+
inlets: Any | None = ...,
269+
outlets: Any | None = ...,
270+
doc: str | None = ...,
271+
doc_md: str | None = ...,
272+
doc_json: str | None = ...,
273+
doc_yaml: str | None = ...,
274+
doc_rst: str | None = ...,
275+
task_display_name: str | None = ...,
276+
logger_name: str | None = ...,
292277
allow_nested_operators: bool = True,
293278
**kwargs,
294279
) -> OperatorPartial: ...
@@ -330,8 +315,12 @@ def partial(
330315
}
331316

332317
# Inject DAG-level default args into args provided to this function.
318+
# Most of the default args will be retrieved during unmapping; here we
319+
# only ensure base properties are correctly set for the scheduler.
333320
partial_kwargs.update(
334-
(k, v) for k, v in dag_default_args.items() if partial_kwargs.get(k, NOTSET) is NOTSET
321+
(k, v)
322+
for k, v in dag_default_args.items()
323+
if k not in partial_kwargs and k in BaseOperator.__init__._BaseOperatorMeta__param_names
335324
)
336325

337326
# Fill fields not provided by the user with default values.

task-sdk/tests/task_sdk/bases/test_baseoperator.py renamed to task-sdk/tests/task_sdk/bases/test_operator.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,3 +898,36 @@ def say_hello(**context):
898898
"timestamp": mock.ANY,
899899
},
900900
]
901+
902+
903+
def test_partial_default_args():
904+
class MockOperator(BaseOperator):
905+
def __init__(self, arg1, arg2, arg3, **kwargs):
906+
self.arg1 = arg1
907+
self.arg2 = arg2
908+
self.arg3 = arg3
909+
self.kwargs = kwargs
910+
super().__init__(**kwargs)
911+
912+
with DAG(
913+
dag_id="test_partial_default_args",
914+
default_args={"queue": "THIS", "arg1": 1, "arg2": 2, "arg3": 3, "arg4": 4},
915+
):
916+
t1 = BaseOperator(task_id="t1")
917+
t2 = MockOperator.partial(task_id="t2", arg2="b").expand(arg1=t1.output)
918+
919+
# Only default_args recognized by BaseOperator are applied.
920+
assert t2.partial_kwargs["queue"] == "THIS"
921+
assert "arg1" not in t2.partial_kwargs
922+
assert t2.partial_kwargs["arg2"] == "b"
923+
assert "arg3" not in t2.partial_kwargs
924+
assert "arg4" not in t2.partial_kwargs
925+
926+
# Simulate resolving mapped operator. This should apply all default_args.
927+
op = t2.unmap({"arg1": "a"})
928+
assert isinstance(op, MockOperator)
929+
assert "arg4" not in op.kwargs # Not recognized by any class; never passed.
930+
assert op.arg1 == "a"
931+
assert op.arg2 == "b"
932+
assert op.arg3 == 3
933+
assert op.queue == "THIS"

0 commit comments

Comments
 (0)