summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/tasks/base.py19
-rw-r--r--docs/releases/6.1.txt3
-rw-r--r--tests/tasks/test_tasks.py26
3 files changed, 47 insertions, 1 deletions
diff --git a/django/tasks/base.py b/django/tasks/base.py
index 94eb29f2ea..b5c960a7cf 100644
--- a/django/tasks/base.py
+++ b/django/tasks/base.py
@@ -1,5 +1,5 @@
from collections.abc import Callable
-from dataclasses import dataclass, field, replace
+from dataclasses import dataclass, field, fields, replace
from datetime import datetime
from inspect import isclass, iscoroutinefunction
from typing import Any
@@ -55,6 +55,23 @@ class Task:
def __post_init__(self):
self.get_backend().validate_task(self)
+ @classmethod
+ def _reconstruct(cls, kwargs):
+ func_path = kwargs["func"]
+ try:
+ func = import_string(func_path)
+ kwargs["func"] = func.func
+ except (ImportError, AttributeError) as e:
+ msg = f"Expected {func_path!r} to point to a Task instance."
+ raise ValueError(msg) from e
+ return cls(**kwargs)
+
+ def __reduce__(self):
+ kwargs = {f.name: getattr(self, f.name) for f in fields(self)}
+ kwargs["func"] = self.module_path
+
+ return (self.__class__._reconstruct, (kwargs,))
+
@property
def name(self):
return self.func.__name__
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 6253a6fea0..987d46874a 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -378,6 +378,9 @@ Tasks
forwarded to the backend's
:attr:`~django.tasks.backends.base.BaseTaskBackend.task_class`.
+* :class:`~django.tasks.Task` and :class:`~django.tasks.TaskResult` instances
+ can now be pickled and unpickled.
+
Templates
~~~~~~~~~
diff --git a/tests/tasks/test_tasks.py b/tests/tasks/test_tasks.py
index b66c2df058..267169f4fa 100644
--- a/tests/tasks/test_tasks.py
+++ b/tests/tasks/test_tasks.py
@@ -1,4 +1,5 @@
import dataclasses
+import pickle
from datetime import datetime
from django.tasks import (
@@ -269,6 +270,31 @@ class TaskTestCase(SimpleTestCase):
test_tasks.noop_task_async,
)
+ def test_pickle_task(self):
+ pickled_task = pickle.dumps(test_tasks.noop_task)
+ unpickled_task = pickle.loads(pickled_task)
+
+ self.assertEqual(unpickled_task, test_tasks.noop_task)
+
+ def test_unpickle_arbitrary_string(self):
+ kwargs = {"func": "does.not.exist.fake_task"}
+ msg = "Expected 'does.not.exist.fake_task' to point to a Task instance."
+ with self.assertRaisesMessage(ValueError, msg):
+ Task._reconstruct(kwargs)
+
+ def test_unpickle_non_task_object(self):
+ kwargs = {"func": "builtins.any"}
+ msg = "Expected 'builtins.any' to point to a Task instance."
+ with self.assertRaisesMessage(ValueError, msg):
+ Task._reconstruct(kwargs)
+
+ def test_pickle_task_result(self):
+ result = test_tasks.noop_task.enqueue()
+ pickled_result = pickle.dumps(result)
+ unpickled_result = pickle.loads(pickled_result)
+
+ self.assertEqual(unpickled_result, result)
+
@override_settings(TASKS={})
def test_no_backends(self):
with self.assertRaises(InvalidTaskBackend):