From 8c8b833d32c02d3ae6f43b04bb1e45968796b402 Mon Sep 17 00:00:00 2001 From: varunkasyap Date: Thu, 19 Feb 2026 14:17:27 +0530 Subject: Fixed #36919 -- Allowed Task and TaskResult to be pickled. --- django/tasks/base.py | 19 ++++++++++++++++++- docs/releases/6.1.txt | 3 +++ tests/tasks/test_tasks.py | 26 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) 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): -- cgit v1.3