This is an automated email from the ASF dual-hosted git repository.
eladkal pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 6d8e0e1b525 Fix @task decorator to use identity check instead of
truthiness (#63788)
6d8e0e1b525 is described below
commit 6d8e0e1b52542422236b6071ea7a55657e2ee38e
Author: Dev-iL <[email protected]>
AuthorDate: Wed Mar 18 20:17:35 2026 +0200
Fix @task decorator to use identity check instead of truthiness (#63788)
* Avoid invoking truthiness on the `task` callable
Changed the serialized Dag test proxy so task is exposed via a property
returning `factory.dag.task`, instead of binding `task = factory.dag.task` on
the proxy class.
The two fixes address the same Python 3.14 issue surface in serialized-DAG
decorator tests:
- The proxy used by `dag_maker` can leak proxy/binding behavior into
`@dag.task`.
- Python 3.14 is less forgiving when `inspect.signature()` and callable
binding hit those proxy objects.
- The truthiness check in `task_decorator_factory()` made that worse by
evaluating objects that only needed an identity check.
Together, the effect is:
- `@dag.task` gets the real task decorator from the underlying Dag, not a
proxy-bound callable.
- `task_decorator_factory()` no longer triggers proxy resolution via
boolean evaluation.
* Update task-sdk/src/airflow/sdk/bases/decorator.py
Co-authored-by: Tzu-ping Chung <[email protected]>
---------
Co-authored-by: Tzu-ping Chung <[email protected]>
---
devel-common/src/tests_common/pytest_plugin.py | 6 +++++-
.../tests/unit/standard/decorators/test_python.py | 21 ++++++++++++++++++---
task-sdk/src/airflow/sdk/bases/decorator.py | 6 +++---
3 files changed, 26 insertions(+), 7 deletions(-)
diff --git a/devel-common/src/tests_common/pytest_plugin.py
b/devel-common/src/tests_common/pytest_plugin.py
index 9f3aa0a7bc4..53abf77844d 100644
--- a/devel-common/src/tests_common/pytest_plugin.py
+++ b/devel-common/src/tests_common/pytest_plugin.py
@@ -922,7 +922,11 @@ def dag_maker(request) -> Generator[DagMaker, None, None]:
class DAGProxy(lazy_object_proxy.Proxy):
"""Wrapper to make test patterns work with serialized
dag."""
- task = factory.dag.task # Expose the @dag.task decorator.
+ @property
+ def task(self):
+ # Forward explicitly so Proxy binding does not leak
the proxy
+ # instance into the decorator callable on Python 3.14.
+ return factory.dag.task
# When adding a task to the dag, automatically
re-serialize.
def add_task(self, task):
diff --git a/providers/standard/tests/unit/standard/decorators/test_python.py
b/providers/standard/tests/unit/standard/decorators/test_python.py
index 95c96ce7bed..9704daa140b 100644
--- a/providers/standard/tests/unit/standard/decorators/test_python.py
+++ b/providers/standard/tests/unit/standard/decorators/test_python.py
@@ -104,6 +104,24 @@ class TestAirflowTaskDecorator(BasePythonTest):
with pytest.raises(TypeError):
task_decorator(not_callable)
+ @pytest.mark.skipif(not AIRFLOW_V_3_2_PLUS, reason="FalseyCallable fix
only applies to Airflow 3.2+")
+ def test_python_operator_python_callable_can_be_falsey(self):
+ """Tests that @task accepts callable objects without evaluating their
truthiness."""
+
+ class FalseyCallable:
+ __annotations__ = {}
+ __name__ = "falsey_callable"
+
+ def __bool__(self):
+ return False
+
+ def __call__(self):
+ return None
+
+ decorated = task_decorator(FalseyCallable())
+
+ assert isinstance(decorated, _TaskDecorator)
+
@pytest.mark.parametrize(
"resolve",
[
@@ -167,9 +185,6 @@ class TestAirflowTaskDecorator(BasePythonTest):
assert t1().operator.multiple_outputs is False
- @pytest.mark.xfail(
- reason="TODO AIP72: All @task calls now go to __getattr__ in
decorators/__init__.py and this test expects user code to throw the error.
Needs to be handled better, likely by changing `fixup_decorator_warning_stack`"
- )
def test_infer_multiple_outputs_forward_annotation(self):
if typing.TYPE_CHECKING:
diff --git a/task-sdk/src/airflow/sdk/bases/decorator.py
b/task-sdk/src/airflow/sdk/bases/decorator.py
index d76c0298278..28119ced429 100644
--- a/task-sdk/src/airflow/sdk/bases/decorator.py
+++ b/task-sdk/src/airflow/sdk/bases/decorator.py
@@ -768,7 +768,9 @@ def task_decorator_factory(
"""
if multiple_outputs is None:
multiple_outputs = cast("bool", attr.NOTHING)
- if python_callable:
+ if python_callable is not None:
+ if not callable(python_callable):
+ raise TypeError("No positional arguments allowed while using
@task, use named arguments instead")
decorator = _TaskDecorator(
function=python_callable,
multiple_outputs=multiple_outputs,
@@ -776,8 +778,6 @@ def task_decorator_factory(
kwargs=kwargs,
)
return cast("TaskDecorator", decorator)
- if python_callable is not None:
- raise TypeError("No args allowed while using @task, use kwargs
instead")
def decorator_factory(python_callable):
return _TaskDecorator(