diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebe891f..ce94133 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,12 @@ jobs: matrix: os: [windows-latest, macos-latest, ubuntu-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] - django-version: ["4.2", "5.0", "5.1", "5.2"] + django-version: ["4.2", "5.0", "5.1", "5.2", "6.0a1"] + exclude: + - django-version: "6.0a1" + python-version: "3.10" + - django-version: "6.0a1" + python-version: "3.11" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/django_tasks/__init__.py b/django_tasks/__init__.py index aa1637a..dede790 100644 --- a/django_tasks/__init__.py +++ b/django_tasks/__init__.py @@ -5,6 +5,7 @@ import importlib.metadata +from django.conf import global_settings from django.utils.connection import BaseConnectionHandler, ConnectionProxy from django.utils.module_loading import import_string @@ -39,16 +40,23 @@ class TaskBackendHandler(BaseConnectionHandler[BaseTaskBackend]): def configure_settings(self, settings: dict | None) -> dict: try: - return super().configure_settings(settings) + task_settings = super().configure_settings(settings) except AttributeError: # HACK: Force a default task backend. # Can be replaced with `django.conf.global_settings` once vendored. - return { + task_settings = None + + if task_settings is None or task_settings is getattr( + global_settings, self.settings_name, None + ): + task_settings = { DEFAULT_TASK_BACKEND_ALIAS: { "BACKEND": "django_tasks.backends.immediate.ImmediateBackend" } } + return task_settings + def create_connection(self, alias: str) -> BaseTaskBackend: params = self.settings[alias] diff --git a/django_tasks/backends/database/migrations/0005_alter_dbtaskresult_priority_and_more.py b/django_tasks/backends/database/migrations/0005_alter_dbtaskresult_priority_and_more.py index a03798c..29eae16 100644 --- a/django_tasks/backends/database/migrations/0005_alter_dbtaskresult_priority_and_more.py +++ b/django_tasks/backends/database/migrations/0005_alter_dbtaskresult_priority_and_more.py @@ -1,7 +1,17 @@ # Generated by Django 4.2.13 on 2024-07-10 15:48 +from django import VERSION from django.db import migrations, models +if VERSION >= (5, 1): + constraint = models.CheckConstraint( + condition=models.Q(("priority__range", (-100, 100))), name="priority_range" + ) +else: + constraint = models.CheckConstraint( + check=models.Q(("priority__range", (-100, 100))), name="priority_range" + ) + class Migration(migrations.Migration): dependencies = [ @@ -16,8 +26,6 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name="dbtaskresult", - constraint=models.CheckConstraint( - check=models.Q(("priority__range", (-100, 100))), name="priority_range" - ), + constraint=constraint, ), ] diff --git a/django_tasks/backends/database/models.py b/django_tasks/backends/database/models.py index ab3c3a7..f2d44e4 100644 --- a/django_tasks/backends/database/models.py +++ b/django_tasks/backends/database/models.py @@ -23,6 +23,7 @@ TaskError, TaskResultStatus, ) +from django_tasks.compat import TASK_CLASSES from django_tasks.utils import get_exception_traceback, get_module_path, retry from .utils import normalize_uuid @@ -155,12 +156,12 @@ class Meta: def task(self) -> Task[P, T]: task = import_string(self.task_path) - if not isinstance(task, Task): + if not isinstance(task, TASK_CLASSES): raise SuspiciousOperation( f"Task {self.id} does not point to a Task ({self.task_path})" ) - return task.using( + return task.using( # type: ignore[no-any-return] priority=self.priority, queue_name=self.queue_name, run_after=None if self.run_after == get_date_max() else self.run_after, diff --git a/django_tasks/backends/rq.py b/django_tasks/backends/rq.py index 281c877..840a8fb 100644 --- a/django_tasks/backends/rq.py +++ b/django_tasks/backends/rq.py @@ -28,6 +28,7 @@ TaskResult, TaskResultStatus, ) +from django_tasks.compat import TASK_CLASSES from django_tasks.exceptions import TaskResultDoesNotExist from django_tasks.signals import task_enqueued, task_finished, task_started from django_tasks.utils import get_module_path, get_random_id @@ -75,12 +76,12 @@ def _execute(self) -> Any: def func(self) -> Task: func = super().func - if not isinstance(func, Task): + if not isinstance(func, TASK_CLASSES): raise SuspiciousOperation( f"Task {self.id} does not point to a Task ({self.func_name})" ) - return func + return func # type: ignore[no-any-return] @cached_property def task_result(self) -> TaskResult: diff --git a/django_tasks/compat.py b/django_tasks/compat.py new file mode 100644 index 0000000..e97e4b8 --- /dev/null +++ b/django_tasks/compat.py @@ -0,0 +1,10 @@ +try: + from django.tasks.base import Task as DjangoTask +except ImportError: + DjangoTask = None + +from .base import Task + +__all__ = ["TASK_CLASSES"] + +TASK_CLASSES = (Task, DjangoTask) if DjangoTask is not None else (Task,) diff --git a/pyproject.toml b/pyproject.toml index 511648c..4cbc5f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", + "Framework :: Django :: 6.0", "Intended Audience :: Developers", "Operating System :: OS Independent", "Natural Language :: English", diff --git a/tests/tests/test_compat.py b/tests/tests/test_compat.py new file mode 100644 index 0000000..b30482f --- /dev/null +++ b/tests/tests/test_compat.py @@ -0,0 +1,50 @@ +from unittest import skipUnless + +from django import VERSION +from django.test import SimpleTestCase, override_settings + +from django_tasks import compat, task_backends +from django_tasks.backends.immediate import ImmediateBackend +from django_tasks.base import Task + +HAS_DJANGO_TASKS = VERSION >= (6, 0) + + +class DjangoCompatTestCase(SimpleTestCase): + def test_uses_lib_tasks_by_default(self) -> None: + self.assertIsInstance(task_backends["default"], ImmediateBackend) + self.assertEqual(task_backends["default"].task_class, Task) + + @skipUnless(HAS_DJANGO_TASKS, "Requires django.tasks") + def test_django_using_lib_backend(self) -> None: + from django.tasks import task_backends + + with override_settings( + TASKS={ + "default": { + "BACKEND": "django_tasks.backends.immediate.ImmediateBackend" + } + } + ): + self.assertIsInstance(task_backends["default"], ImmediateBackend) + + @skipUnless(HAS_DJANGO_TASKS, "Requires django.tasks") + def test_lib_using_django_backend(self) -> None: + from django.tasks.backends.immediate import ImmediateBackend + + with override_settings( + TASKS={ + "default": { + "BACKEND": "django.tasks.backends.immediate.ImmediateBackend" + } + } + ): + self.assertIsInstance(task_backends["default"], ImmediateBackend) + + def test_compat_has_django_task(self) -> None: + self.assertIn(Task, compat.TASK_CLASSES) + + if HAS_DJANGO_TASKS: + from django.tasks.base import Task as DjangoTask + + self.assertIn(DjangoTask, compat.TASK_CLASSES)