summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNilesh Kumar Pahari <nileshpahari@protonmail.com>2026-04-06 23:48:30 +0530
committerJacob Walls <jacobtylerwalls@gmail.com>2026-04-07 14:06:12 -0400
commite27f23b268c520957384054fb236cfc303f95f51 (patch)
tree8e51a4622acfba8827a0fad1553eb9f1051077fb
parent78a3ffbb4cec25ed003f16cf4b1aa0b4bcdc2590 (diff)
Fixed #36816 -- Allowed **kwargs in @task decorator.
The decorator was updated to accept **kwargs and forward them to task_class, allowing additional parameters to be passed to custom Task subclasses.
-rw-r--r--django/tasks/base.py19
-rw-r--r--docs/ref/tasks.txt30
-rw-r--r--docs/releases/6.1.txt4
-rw-r--r--tests/tasks/test_custom_backend.py59
-rw-r--r--tests/tasks/test_tasks.py5
5 files changed, 106 insertions, 11 deletions
diff --git a/django/tasks/base.py b/django/tasks/base.py
index bb37838d2f..94eb29f2ea 100644
--- a/django/tasks/base.py
+++ b/django/tasks/base.py
@@ -43,11 +43,11 @@ class TaskResultStatus(TextChoices):
@dataclass(frozen=True, slots=True, kw_only=True)
class Task:
- priority: int
func: Callable[..., Any] # The Task function.
- backend: str
- queue_name: str
- run_after: datetime | None # The earliest this Task will run.
+ priority: int = DEFAULT_TASK_PRIORITY
+ backend: str = DEFAULT_TASK_BACKEND_ALIAS
+ queue_name: str = DEFAULT_TASK_QUEUE_NAME
+ run_after: datetime | None = None # The earliest this Task will run.
# Whether the Task receives the Task context when executed.
takes_context: bool = False
@@ -138,17 +138,24 @@ def task(
queue_name=DEFAULT_TASK_QUEUE_NAME,
backend=DEFAULT_TASK_BACKEND_ALIAS,
takes_context=False,
+ **kwargs,
):
from . import task_backends
+ if "run_after" in kwargs:
+ raise TypeError(
+ "run_after cannot be defined statically with the @task decorator. "
+ "Use .using(run_after=...) to set it dynamically."
+ )
+
def wrapper(f):
return task_backends[backend].task_class(
- priority=priority,
func=f,
+ priority=priority,
queue_name=queue_name,
backend=backend,
takes_context=takes_context,
- run_after=None,
+ **kwargs,
)
if function:
diff --git a/docs/ref/tasks.txt b/docs/ref/tasks.txt
index 99d8eb6a0a..1850c91cab 100644
--- a/docs/ref/tasks.txt
+++ b/docs/ref/tasks.txt
@@ -17,10 +17,13 @@ Task definition
The ``task`` decorator
----------------------
-.. function:: task(*, priority=0, queue_name="default", backend="default", takes_context=False)
+.. function:: task(*, priority=0, queue_name="default", backend="default", takes_context=False, **kwargs)
- The ``@task`` decorator defines a :class:`Task` instance. This has the
- following optional arguments:
+ The ``@task`` decorator defines a :class:`Task` instance. All keyword
+ arguments are passed directly to the backend's ``task_class`` (which
+ defaults to :class:`Task`).
+
+ The following standard arguments are supported:
* ``priority``: Sets the :attr:`~Task.priority` of the ``Task``. Defaults
to 0.
@@ -32,6 +35,18 @@ The ``task`` decorator
:class:`TaskContext`. Defaults to ``False``. See :ref:`Task context
<task-context>` for details.
+ Custom Task backends may define a custom ``task_class`` that accepts
+ additional arguments. These can be passed through the ``@task`` decorator::
+
+ @task(foo=5, bar=600)
+ def my_task():
+ pass
+
+ .. versionchanged:: 6.1
+
+ Support for passing arbitrary ``**kwargs`` to the ``@task`` decorator
+ is added.
+
If the defined ``Task`` is not valid according to the backend,
:exc:`~django.tasks.exceptions.InvalidTask` is raised.
@@ -75,6 +90,8 @@ The ``task`` decorator
current time, a timezone-aware :class:`datetime <datetime.datetime>`,
or ``None`` if not constrained. Defaults to ``None``.
+ This attribute can be set using :meth:`~Task.using`.
+
The backend must have :attr:`.supports_defer` set to ``True`` to use
this feature. Otherwise,
:exc:`~django.tasks.exceptions.InvalidTask` is raised.
@@ -291,6 +308,13 @@ Base backend
``BaseTaskBackend`` is the parent class for all Task backends.
+ .. attribute:: BaseTaskBackend.task_class
+
+ The :class:`~django.tasks.Task` subclass to use when creating tasks
+ with the :func:`~django.tasks.task` decorator. Defaults to
+ :class:`~django.tasks.Task`. Custom backends can override this to use
+ a custom ``Task`` subclass with additional attributes.
+
.. attribute:: BaseTaskBackend.options
A dictionary of extra parameters for the Task backend. These are
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 5dcdc9c50d..9c8aeea70c 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -348,7 +348,9 @@ Signals
Tasks
~~~~~
-* ...
+* The :func:`~django.tasks.task` decorator now accepts ``**kwargs``, which are
+ forwarded to the backend's
+ :attr:`~django.tasks.backends.base.BaseTaskBackend.task_class`.
Templates
~~~~~~~~~
diff --git a/tests/tasks/test_custom_backend.py b/tests/tasks/test_custom_backend.py
index c8508d5d3b..f3851622ed 100644
--- a/tests/tasks/test_custom_backend.py
+++ b/tests/tasks/test_custom_backend.py
@@ -1,7 +1,8 @@
import logging
+from dataclasses import dataclass
from unittest import mock
-from django.tasks import default_task_backend, task_backends
+from django.tasks import Task, default_task_backend, task, task_backends
from django.tasks.backends.base import BaseTaskBackend
from django.tasks.exceptions import InvalidTask
from django.test import SimpleTestCase, override_settings
@@ -23,6 +24,20 @@ class CustomBackendNoEnqueue(BaseTaskBackend):
pass
+@dataclass(frozen=True, slots=True, kw_only=True)
+class CustomTask(Task):
+ foo: int = 3
+ bar: int = 300
+
+
+class CustomTaskBackend(BaseTaskBackend):
+ task_class = CustomTask
+ supports_priority = True
+
+ def enqueue(self, task, args, kwargs):
+ pass
+
+
@override_settings(
TASKS={
"default": {
@@ -68,3 +83,45 @@ class CustomBackendTestCase(SimpleTestCase):
"without an implementation for abstract method 'enqueue'",
):
test_tasks.noop_task.using(backend="no_enqueue")
+
+
+@override_settings(
+ TASKS={
+ "default": {
+ "BACKEND": f"{CustomTaskBackend.__module__}."
+ f"{CustomTaskBackend.__qualname__}",
+ "QUEUES": ["default", "high"],
+ },
+ }
+)
+class CustomTaskTestCase(SimpleTestCase):
+ def test_custom_task_default_values(self):
+ my_task = task()(test_tasks.noop_task.func)
+
+ self.assertIsInstance(my_task, CustomTask)
+ self.assertEqual(my_task.foo, 3)
+ self.assertEqual(my_task.bar, 300)
+
+ def test_custom_task_with_custom_values(self):
+ my_task = task(foo=5, bar=600)(test_tasks.noop_task.func)
+
+ self.assertIsInstance(my_task, CustomTask)
+ self.assertEqual(my_task.foo, 5)
+ self.assertEqual(my_task.bar, 600)
+
+ def test_custom_task_with_standard_and_custom_values(self):
+ my_task = task(priority=10, queue_name="high", foo=10, bar=1000)(
+ test_tasks.noop_task.func
+ )
+
+ self.assertIsInstance(my_task, CustomTask)
+ self.assertEqual(my_task.priority, 10)
+ self.assertEqual(my_task.queue_name, "high")
+ self.assertEqual(my_task.foo, 10)
+ self.assertEqual(my_task.bar, 1000)
+ self.assertFalse(my_task.takes_context)
+ self.assertIsNone(my_task.run_after)
+
+ def test_custom_task_invalid_kwarg(self):
+ with self.assertRaises(TypeError):
+ task(unknown_param=123)(test_tasks.noop_task.func)
diff --git a/tests/tasks/test_tasks.py b/tests/tasks/test_tasks.py
index 14d47c4cf6..b66c2df058 100644
--- a/tests/tasks/test_tasks.py
+++ b/tests/tasks/test_tasks.py
@@ -312,3 +312,8 @@ class TaskTestCase(SimpleTestCase):
"Task takes context but does not have a first argument of 'context'.",
):
task(takes_context=True)(test_tasks.calculate_meaning_of_life.func)
+
+ def test_run_after_in_decorator(self):
+ msg = "run_after cannot be defined statically with the @task decorator."
+ with self.assertRaisesMessage(TypeError, msg):
+ task(run_after=timezone.now())(test_tasks.calculate_meaning_of_life.func)