From 9f614f370eae3b36b822803cb308d774785ba962 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Tue, 30 Jun 2026 22:42:23 +0200 Subject: [PATCH] Resolve #19 -- Backport Pickel support from Django 6.1 --- .github/workflows/ci.yml | 1 + pyproject.toml | 3 ++- tests/test_executor.py | 35 +++++++++++++++-------------------- threadmill/backends/base.py | 23 +++++++++++++++++++++++ 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0fd6ae..c5caa81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ jobs: - "3.13" - "3.14" django-version: + - "6.0.0" - "6.1a1" runs-on: ${{ matrix.os }} services: diff --git a/pyproject.toml b/pyproject.toml index b1c93f9..fee79cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,11 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Framework :: Django", + "Framework :: Django :: 6.0", "Framework :: Django :: 6.1", ] requires-python = ">=3.12" -dependencies = ["django>=6.1a1"] +dependencies = ["django>=6.0"] [project.optional-dependencies] redis = ["redis>=5.0"] diff --git a/tests/test_executor.py b/tests/test_executor.py index 520d060..19e1c09 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -7,7 +7,13 @@ import time import uuid -from django.tasks import Task, TaskResult, TaskResultStatus, default_task_backend +from django.tasks import ( + TaskContext, + TaskResult, + TaskResultStatus, + default_task_backend, + task, +) from django.utils import timezone from tests.testapp.tasks import boom, echo @@ -15,29 +21,21 @@ from threadmill.executor import TaskExecutor, WorkerProcess, WorkerThread # noqa: E402 +@task(queue_name="default") def _add(x, y): return x + y -ADD_TASK = Task(func=_add, queue_name="default") - - +@task(queue_name="default", takes_context=True) def _context_captor(context): - """Task function that captures the context it receives.""" - _context_captor.captured = context - return 42 - - -CONTEXT_TASK = Task(func=_context_captor, queue_name="default", takes_context=True) + return context +@task(queue_name="default") async def _async_task(): return 99 -ASYNC_TASK = Task(func=_async_task, queue_name="default") - - def _task_result(task, *args, **kwargs) -> TaskResult: """Build a READY `TaskResult` without touching Redis.""" return TaskResult( @@ -290,20 +288,17 @@ def test_execute_task_result__preserves_worker_ids(self): def test_call_task__calls_function_with_args(self): """call_task invokes the task function with args and kwargs.""" - result = WorkerThread.call_task(_task_result(ADD_TASK, 1, y=2)) + result = WorkerThread.call_task(_task_result(_add, 1, y=2)) assert result == 3 def test_call_task__passes_context_when_takes_context(self): """call_task passes TaskContext when task.takes_context is True.""" - _context_captor.captured = None - result = WorkerThread.call_task(_task_result(CONTEXT_TASK)) - assert result == 42 - assert _context_captor.captured is not None - assert _context_captor.captured.task_result.task is CONTEXT_TASK + result = WorkerThread.call_task(_task_result(_context_captor)) + assert isinstance(result, TaskContext) def test_call_task__runs_async_function(self): """call_task runs async task functions with asyncio.run.""" - result = WorkerThread.call_task(_task_result(ASYNC_TASK)) + result = WorkerThread.call_task(_task_result(_async_task)) assert result == 99 def test_create_task_error__builds_task_error(self): diff --git a/threadmill/backends/base.py b/threadmill/backends/base.py index ceb6537..14e9195 100644 --- a/threadmill/backends/base.py +++ b/threadmill/backends/base.py @@ -7,12 +7,34 @@ import threading from abc import ABC +import django from django.core.serializers.json import DjangoJSONEncoder from django.tasks import DEFAULT_TASK_QUEUE_NAME, Task, TaskResult, TaskResultStatus from django.tasks.backends.base import BaseTaskBackend from django.tasks.base import TaskError from django.utils.module_loading import import_string +if django.VERSION == (6, 0): + # https://github.com/django/django/commit/8c8b833d32c02d3ae6f43b04bb1e45968796b402 + @dataclasses.dataclass(frozen=True, slots=True, kw_only=True) + class Task(Task): + @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 dataclasses.fields(self)} + kwargs["func"] = self.module_path + + return (self.__class__._reconstruct, (kwargs,)) + @dataclasses.dataclass(kw_only=True, slots=True) class QueueCounts: @@ -106,6 +128,7 @@ def default(self, o): class ThreadmillTaskBackend(BaseTaskBackend, ABC): """Interface for task queues to be processed by the executor.""" + task_class = Task # can be removed in the future when Django 6.0 support is dropped supports_async_task = True supports_get_result = True broker_class: type[Broker] | None = None