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(

Reply via email to