diff options
| -rw-r--r-- | django/tasks/base.py | 19 | ||||
| -rw-r--r-- | docs/ref/tasks.txt | 30 | ||||
| -rw-r--r-- | docs/releases/6.1.txt | 4 | ||||
| -rw-r--r-- | tests/tasks/test_custom_backend.py | 59 | ||||
| -rw-r--r-- | tests/tasks/test_tasks.py | 5 |
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) |
