diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e44ff8..ff86b28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ on: jobs: tests: name: "Python ${{ matrix.python-version }}" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" services: redis: image: redis @@ -24,10 +24,10 @@ jobs: - 6379:6379 strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: "actions/checkout@v2" - - uses: "actions/setup-python@v2" + - uses: "actions/checkout@v6" + - uses: "actions/setup-python@v6" with: python-version: "${{ matrix.python-version }}" - name: "Install dependencies" @@ -35,6 +35,7 @@ jobs: set -xe python -m pip install --upgrade pip setuptools python -m pip install --upgrade poetry tox tox-gh-actions + pip install --user poetry-plugin-export - name: "Run tox targets for ${{ matrix.python-version }}" run: "python -m tox" diff --git a/.gitignore b/.gitignore index ea01a39..4e942f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.vagrant sandbox/db.sqlite3 docs/_build dist/* diff --git a/.python-version b/.python-version index cb6a2de..4e638fb 100644 --- a/.python-version +++ b/.python-version @@ -1,3 +1,5 @@ -3.8.5 -3.7.8 -3.6.11 +3.10.19 +3.11.14 +3.12.12 +3.13.11 +3.14.2 diff --git a/.readthedocs.yml b/.readthedocs.yml index a206b3f..a037aac 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,7 @@ build: image: latest python: - version: 3.8 + version: 3.10 pip_install: true extra_requirements: - docs diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3775640..91327a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,16 @@ Changelog ========= +4.0.0 +~~~~~ + +* BREAKING CHANGE: Drop support for Django < 5.2 and Python < 3.10. +* BREAKING CHANGE: Drop support for Celery < 5.0. +* BREAKING CHANGE: Drop support for django-rq < 3.0. +* Add support for Django 5.2. +* Add support for Django 6.0. +* Modernise Python code. + 3.0.0 ~~~~~ diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index adef1a6..0000000 --- a/Vagrantfile +++ /dev/null @@ -1,8 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant::Config.run do |config| - config.vm.box = "ubuntu/focal64" - config.vm.forward_port 8000, 8080 - config.vm.provision "shell", path: "sandbox/provision.sh", privileged: false -end diff --git a/cacheback/__init__.py b/cacheback/__init__.py index 8299797..52709a8 100644 --- a/cacheback/__init__.py +++ b/cacheback/__init__.py @@ -1,8 +1,5 @@ -try: - import importlib.metadata as importlib_metadata -except ModuleNotFoundError: - # This is required for Python versions < 3.8 - import importlib_metadata +import importlib.metadata as importlib_metadata + try: __version__ = importlib_metadata.version('django-cacheback') diff --git a/cacheback/base.py b/cacheback/base.py index a86a40c..0969d16 100644 --- a/cacheback/base.py +++ b/cacheback/base.py @@ -6,7 +6,6 @@ from django.conf import settings from django.core.cache import DEFAULT_CACHE_ALIAS, caches from django.db.models import Model as DjangoModel -from django.utils.itercompat import is_iterable from .utils import enqueue_task, get_job_class @@ -29,7 +28,7 @@ def to_bytestring(value): :returns: a bytestring """ if isinstance(value, DjangoModel): - return ('%s:%s' % (value.__class__, hash(value))).encode('utf-8') + return (f'{value.__class__}:{hash(value)}').encode('utf-8') if isinstance(value, str): return value.encode('utf8') if isinstance(value, bytes): @@ -37,7 +36,7 @@ def to_bytestring(value): return bytes(str(value), 'utf8') -class Job(object): +class Job: """ A cached read job. @@ -59,7 +58,7 @@ class Job(object): #: refresh the cache. refresh_timeout = 60 - #: Secifies which cache to use from your `CACHES` setting. It defaults to + #: Specifies which cache to use from your `CACHES` setting. It defaults to #: `default`. cache_alias = None @@ -91,7 +90,7 @@ class Job(object): @property def class_path(self): - return '%s.%s' % (self.__module__, self.__class__.__name__) + return f'{self.__module__}.{self.__class__.__name__}' def __init__(self): self.cache_alias = self.cache_alias or getattr( @@ -283,6 +282,10 @@ def set(self, *raw_args, **raw_kwargs): # HELPER METHODS # -------------- + @staticmethod + def is_iterable(value): + return isinstance(value, collections.abc.Iterable) + def prepare_args(self, *args): return args @@ -399,11 +402,11 @@ def key(self, *args, **kwargs): return self.class_path try: if args and not kwargs: - return "%s:%s" % (self.class_path, self.hash(args)) + return f"{self.class_path}:{self.hash(args)}" # The line might break if your passed values are un-hashable. If # it does, you need to override this method and implement your own # key algorithm. - return "%s:%s:%s:%s" % ( + return "{}:{}:{}:{}".format( self.class_path, self.hash(args), self.hash([k for k in sorted(kwargs)]), @@ -422,7 +425,7 @@ def hash(self, value): This is for use in a cache key. """ - if is_iterable(value): + if self.is_iterable(value): value = tuple(to_bytestring(v) for v in value) return hashlib.md5(b':'.join(value)).hexdigest() @@ -440,7 +443,7 @@ def process_result(self, result, call, cache_status, sync_fetch=None): :param result: The result to be returned :param call: A named tuple with properties 'args' and 'kwargs that holds the call args and kwargs - :param cache_status: A status integrer, accessible as class constants + :param cache_status: A status integer, accessible as class constants self.MISS, self.HIT, self.STALE :param sync_fetch: A boolean indicating whether a synchronous fetch was performed. A value of None indicates that no fetch diff --git a/cacheback/jobs.py b/cacheback/jobs.py index 7fbe450..e33b90d 100644 --- a/cacheback/jobs.py +++ b/cacheback/jobs.py @@ -1,7 +1,4 @@ -try: - import importlib -except ImportError: - import django.utils.importlib as importlib +import importlib from .base import Job @@ -19,7 +16,7 @@ def __init__( task_options=None, set_data_kwarg=None, ): - super(FunctionJob, self).__init__() + super().__init__() if lifetime is not None: self.lifetime = int(lifetime) if fetch_on_miss is not None: @@ -43,7 +40,7 @@ def get_init_kwargs(self): def prepare_args(self, fn, *args): # Convert function into "module:name" form so that is can be pickled and # then re-imported. - return ("%s:%s" % (fn.__module__, fn.__name__),) + args + return (f"{fn.__module__}:{fn.__name__}",) + args def fetch(self, fn_string, *args, **kwargs): # Import function from string representation @@ -67,7 +64,7 @@ def __init__( """ :model: The model class to use """ - super(QuerySetJob, self).__init__() + super().__init__() self.model = model if lifetime is not None: self.lifetime = lifetime @@ -82,7 +79,7 @@ def get_init_kwargs(self): return {'model': self.model, 'lifetime': self.lifetime, 'cache_alias': self.cache_alias} def key(self, *args, **kwargs): - return "%s-%s" % (self.model.__name__, super(QuerySetJob, self).key(*args, **kwargs)) + return f"{self.model.__name__}-{super().key(*args, **kwargs)}" class QuerySetGetJob(QuerySetJob): diff --git a/cacheback/utils.py b/cacheback/utils.py index 45535e6..44969e0 100644 --- a/cacheback/utils.py +++ b/cacheback/utils.py @@ -1,14 +1,10 @@ +import importlib import logging from django.conf import settings from django.core.exceptions import ImproperlyConfigured -try: - import importlib -except ImportError: - import django.utils.importlib as importlib - try: from .tasks import refresh_cache as celery_refresh_cache except ImportError: @@ -58,4 +54,4 @@ def enqueue_task(kwargs, task_options=None): elif task_queue == 'celery' and celery_refresh_cache is not None: return celery_refresh_cache.apply_async(kwargs=kwargs, **task_options or {}) - raise ImproperlyConfigured('Unkown task queue configured: {0}'.format(task_queue)) + raise ImproperlyConfigured(f'Unknown task queue or backend is not configured: {task_queue}') diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..df8ad88 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +services: + django: &django + build: + context: . + dockerfile: sandbox/Dockerfile + entrypoint: /app/entrypoint + environment: + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_DB: 0 + volumes: + - ./sandbox:/app + ports: + - "8080:8080" + depends_on: + redis: + condition: service_healthy + + redis: + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + + celery: + depends_on: + - django + - redis + <<: *django + ports: [] + entrypoint: celery -A sandbox worker --loglevel=INFO + + rqworker: + depends_on: + - django + - redis + <<: *django + ports: [] + entrypoint: python manage.py rqworker \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index b5be342..87a86a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # django-cacheback documentation build configuration file, created by # sphinx-quickstart on Mon Jul 30 21:40:46 2012. @@ -50,8 +49,8 @@ master_doc = 'index' # General information about the project. -project = u'django-cacheback' -copyright = u'2012, David Winterbottom' +project = 'django-cacheback' +copyright = '2012, David Winterbottom' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -194,8 +193,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-cacheback', u'django-cacheback Documentation', - u'David Winterbottom', 'manual'), + ('index', 'django-cacheback', 'django-cacheback Documentation', + 'David Winterbottom', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -224,8 +223,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-cacheback', u'django-cacheback Documentation', - [u'David Winterbottom'], 1) + ('index', 'django-cacheback', 'django-cacheback Documentation', + ['David Winterbottom'], 1) ] # If true, show URL addresses after external links. @@ -238,8 +237,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-cacheback', u'django-cacheback Documentation', - u'David Winterbottom', 'django-cacheback', 'One line description of project.', + ('index', 'django-cacheback', 'django-cacheback Documentation', + 'David Winterbottom', 'django-cacheback', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/contributing.rst b/docs/contributing.rst index 364b8f0..151ba0c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -38,24 +38,16 @@ all supported Django and Python versions:: Sandbox VM ========== -There is a ``VagrantFile`` for setting up a sandbox VM where you can play around -with the functionality. Bring up the Vagrant box:: +Alternatively, there's a ``docker compose`` stack for setting up a sandbox +environment where you can play around with the functionality. +Bring up the compose stack:: - $ vagrant up + $ docker compose up -This may take a while but will set up a Ubuntu Precise64 VM with RabbitMQ -installed. You can then SSH into the machine:: +The stack will start with Celery as a broker by default. You can Alternatively +make use of rq by supplying the `Q` env var: - $ vagrant ssh - $ cd /vagrant/sandbox - -You can now decide to run the Celery implementation:: - - $ honcho -f Procfile.celery start - -Or you can run the RQ implementation:: - - $ honcho -f Procfile.rq start + $ Q=rq docker compose up The above commands will start a Django runserver and the selected task worker. The dummy site will be available at ``http://localhost:8080`` on your host diff --git a/docs/installation.rst b/docs/installation.rst index 6ce0249..d98f4e2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -35,12 +35,12 @@ up the `django-rq installation guide`_ for more details. Set up a cache ~~~~~~~~~~~~~~ -You also need to ensure you have `a cache set up`_. Most likely, you'll be using -memcache so your settings will include something like:: +You also need to ensure you have `a cache set up`_. Most likely, you'll be using Redis +or memcache so your settings will include something like:: CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': '127.0.0.1:11211', } } diff --git a/pyproject.toml b/pyproject.toml index 7c457cb..cf3a8a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-cacheback" -version = "3.0.0" +version = "4.0.0" description = "Caching library for Django that uses Celery or RQ to refresh cache items asynchronously" authors = [ "David Winterbottom ", @@ -21,38 +21,43 @@ classifiers = [ "Intended Audience :: Developers", "Operating System :: Unix", "Programming Language :: Python", - "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] packages = [{ include = "cacheback" }] include = ["LICENSE"] [tool.poetry.dependencies] -python = ">=3.6.2,<4" -importlib-metadata = {version = "*", python = "<3.8"} +python = ">=3.10.0,<4" -django = ">=2" -celery = {version = ">=4", optional = true} -django-rq = {version = ">=2", optional = true} +django = ">=5.2" +celery = {version = ">=5.0", optional = true} +django-rq = {version = ">=3", optional = true} Sphinx = {version = ">=3.3.0,<4", optional = true} -[tool.poetry.dev-dependencies] -pytest = ">=6.0" -pytest-django = ">=4.1" -pytest-cov = ">=2.10" -pytest-isort = ">=1.2" -pytest-flake8 = ">=1.0" -flake8 = "<5" -pytest-black = {version = ">=0.3"} -freezegun = ">=1.0" -coverage = {version = ">=5.0", extras = ["toml"]} -celery = ">=4" -django-rq = ">=2" -typing_extensions = { version = ">=3.10", python = "<3.10" } +[tool.poetry.requires-plugins] +poetry-plugin-export = ">=1.8" + +[tool.poetry.group.dev.dependencies] +pytest = ">=9.0" +pytest-django = ">=4.11" +pytest-cov = ">=7.0" +pytest-isort = ">=4.0" +pytest-flake8 = ">=1.3" +flake8 = ">=7" +pytest-black =">=0.6" +freezegun = ">=1.5" +coverage = {version = ">=7.0", extras = ["toml"]} +celery = ">=5.0" +django-rq = ">=3" + + +[tool.poetry.group.sandbox.dependencies] +redis = "^7.1.0" +django-debug-toolbar = "^6.1.0" [tool.poetry.extras] celery = ["celery"] @@ -60,7 +65,7 @@ rq = ["django-rq"] docs = ["Sphinx"] [build-system] -requires = ["poetry>=1.1"] +requires = ["poetry>=2.1"] build-backend = "poetry.masonry.api" [tool.pytest.ini_options] diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile new file mode 100644 index 0000000..c95a2d9 --- /dev/null +++ b/sandbox/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV POETRY_VIRTUALENVS_CREATE="false" + +# Install lightweight build tools for any binary Python dependencies. +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --upgrade pip \ + && pip install --no-cache-dir "poetry>=2" + +COPY . /package/ +WORKDIR /package + +RUN poetry install --with sandbox + +WORKDIR /app diff --git a/sandbox/Procfile.celery b/sandbox/Procfile.celery deleted file mode 100644 index 6f3176e..0000000 --- a/sandbox/Procfile.celery +++ /dev/null @@ -1,2 +0,0 @@ -web: env QUEUE=celery poetry run python manage.py runserver 0.0.0.0:8000 -worker: env QUEUE=celery poetry run celery -A sandbox worker --loglevel=INFO diff --git a/sandbox/Procfile.rq b/sandbox/Procfile.rq deleted file mode 100644 index 8ab63bc..0000000 --- a/sandbox/Procfile.rq +++ /dev/null @@ -1,2 +0,0 @@ -web: env QUEUE=rq poetry run python manage.py runserver 0.0.0.0:8000 -worker: env QUEUE=rq poetry run python manage.py rqworker diff --git a/sandbox/dummyapp/migrations/0001_initial.py b/sandbox/dummyapp/migrations/0001_initial.py index a064f47..9ecd274 100644 --- a/sandbox/dummyapp/migrations/0001_initial.py +++ b/sandbox/dummyapp/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9 on 2015-12-12 20:02 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/sandbox/entrypoint b/sandbox/entrypoint new file mode 100755 index 0000000..fd86162 --- /dev/null +++ b/sandbox/entrypoint @@ -0,0 +1,5 @@ +#!/bin/bash + +python manage.py migrate +python manage.py loaddata fixture.json +python manage.py runserver 0.0.0.0:8080 diff --git a/sandbox/provision.sh b/sandbox/provision.sh deleted file mode 100755 index 76367b8..0000000 --- a/sandbox/provision.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -sudo apt-get update -sudo apt-get install -y redis-server memcached python3-pip git -sudo pip3 install -U pip poetry honcho - -cd /vagrant -poetry install -poetry run pip install -r sandbox/requirements.txt - -# Create and fill database -cd /vagrant/sandbox -poetry run python manage.py migrate -poetry run python manage.py loaddata fixture.json diff --git a/sandbox/requirements.txt b/sandbox/requirements.txt deleted file mode 100644 index 365b85e..0000000 --- a/sandbox/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -python-memcached>=1.59 diff --git a/sandbox/settings.py b/sandbox/settings.py index 51248e5..382b07d 100644 --- a/sandbox/settings.py +++ b/sandbox/settings.py @@ -1,5 +1,5 @@ import os - +import socket DEBUG = True @@ -13,6 +13,7 @@ 'PORT': '', # Set to empty string for default. Not used with sqlite3. } } +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -80,11 +81,8 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, 'OPTIONS': { - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', @@ -96,6 +94,7 @@ ] MIDDLEWARE = ( + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -119,9 +118,11 @@ 'dummyapp', 'django_rq', 'cacheback', + 'debug_toolbar', ) -INTERNAL_IPS = ('10.0.2.2',) +_, _, ips = socket.gethostbyname_ex(socket.gethostname()) +INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to @@ -163,24 +164,29 @@ # CACHEBACK SETTINGS -CELERY_BROKER_URL = 'redis://localhost:6379/0' -CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' +REDIS_HOST = os.environ.get('REDIS_HOST', 'localhost') +REDIS_PORT = os.environ.get('REDIS_PORT', 6379) +REDIS_DB = os.environ.get('REDIS_DB', 0) +REDIS_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}' + +CELERY_BROKER_URL = REDIS_URL +CELERY_RESULT_BACKEND = REDIS_URL CELERY_TASK_SERIALIZER = 'json' RQ_QUEUES = { 'default': { - 'HOST': 'localhost', - 'PORT': 6379, - 'DB': 0, + 'HOST': REDIS_HOST, + 'PORT': int(REDIS_PORT), + 'DB': int(REDIS_DB), }, } CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': REDIS_URL, } } -CACHEBACK_TASK_QUEUE = dict([(q, q) for q in ('celery', 'rq')]).get( - os.environ.get('QUEUE', ''), 'celery') +CACHEBACK_TASK_QUEUE = {q: q for q in ('celery', 'rq')}.get( + os.environ.get('Q', ''), 'celery') diff --git a/sandbox/urls.py b/sandbox/urls.py index 1049c36..07e4932 100644 --- a/sandbox/urls.py +++ b/sandbox/urls.py @@ -1,8 +1,9 @@ from django.urls import path +from debug_toolbar.toolbar import debug_toolbar_urls from dummyapp import views urlpatterns = [ path('', views.index, name='index'), -] +] + debug_toolbar_urls() diff --git a/tests/conftest.py b/tests/conftest.py index b8fbc91..ff3c502 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ def skip_if_no_redis(): try: redis.StrictRedis( settings.RQ_QUEUES['default'].get('HOST', 'localhost'), - settings.RQ_QUEUES['default'].get('POST', 6379), + settings.RQ_QUEUES['default'].get('PORT', 6379), ).ping() skip_if_no_redis._redis_available = True except redis.ConnectionError: @@ -36,7 +36,7 @@ def cleared_cache(request): cache.clear() -@pytest.yield_fixture +@pytest.fixture() def rq_worker(request): [queue.empty() for queue in django_rq.get_worker().queues] @@ -47,7 +47,7 @@ def rq_worker(request): [queue.empty() for queue in django_rq.get_worker().queues] -@pytest.yield_fixture +@pytest.fixture() def rq_burst(request, rq_worker): def burst(): rq_worker.work(burst=True) diff --git a/tests/test_base_job.py b/tests/test_base_job.py index 48753a1..bee37c9 100644 --- a/tests/test_base_job.py +++ b/tests/test_base_job.py @@ -12,7 +12,7 @@ class DummyJob(Job): def fetch(self, param): - return ('JOB-EXECUTED:{0}'.format(param), timezone.now()) + return (f'JOB-EXECUTED:{param}', timezone.now()) class CacheAliasDummyJob(DummyJob): diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 29e8d99..3813685 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -10,22 +10,22 @@ class OtherFunctionJob(FunctionJob): @cacheback(fetch_on_miss=False, job_class=OtherFunctionJob) def no_fetch_miss_function(param): - return 'JOB-EXECUTED:{0}'.format(param) + return f'JOB-EXECUTED:{param}' @cacheback(lifetime=30, fetch_on_miss=True) def fetch_miss_function(param): - return 'JOB-EXECUTED:{0}'.format(param) + return f'JOB-EXECUTED:{param}' @cacheback(cache_alias='secondary', fetch_on_miss=True) def fetch_cache_alias_function(param): - return 'JOB-EXECUTED:{0}'.format(param) + return f'JOB-EXECUTED:{param}' @cacheback(set_data_kwarg='my_data') def custom_payload_label_function(param): - return 'JOB-EXECUTED:{0}'.format(param) + return f'JOB-EXECUTED:{param}' @pytest.mark.usefixtures('cleared_cache', scope='function') diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 1545a0f..98a71ff 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -6,12 +6,12 @@ def dummy_function(param): - return 'JOB-EXECUTED:{0}'.format(param) + return f'JOB-EXECUTED:{param}' @cacheback() def decorated_dummy_function(param): - return 'JOB-EXECUTED:{0}'.format(param) + return f'JOB-EXECUTED:{param}' @pytest.mark.usefixtures('cleared_cache', scope='function') diff --git a/tests/test_utils.py b/tests/test_utils.py index f9a4b96..d6feec9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -72,4 +72,4 @@ def test_unkown(self, settings): with pytest.raises(ImproperlyConfigured) as exc: enqueue_task('foo') - assert 'Unkown task queue' in str(exc.value) + assert 'Unknown task queue' in str(exc.value) diff --git a/tox.ini b/tox.ini index 202c906..d59ef44 100644 --- a/tox.ini +++ b/tox.ini @@ -1,31 +1,27 @@ [tox] isolated_build = true envlist = - py{36,37,38,39}-django{22,30,31} - py{36,37,38,39,310}-django{32} - py{38,39,310}-django{40} + py{310,311,312,313,314}-django{52} + py{312,313,314}-django{60} [gh-actions] python = - 3.6: py36 - 3.7: py37 - 3.8: py38 - 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 [testenv] setenv = PYTHONPATH={toxinidir} deps = - django22: Django>=2.2,<2.3 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 - django32: Django>=3.2,<3.3 - django40: Django>=4.0,<4.1 + django52: Django>=5.2,<5.3 + django60: Django>=6.0,<6.1 allowlist_externals = poetry sh skip_install = true commands = - poetry export --dev --without-hashes -o {toxworkdir}/requirements.txt + poetry export --with=dev --without-hashes -o {toxworkdir}/requirements.txt sh -c 'grep -v "^[dD]jango==" {toxworkdir}/requirements.txt | poetry run pip install --no-deps -r /dev/stdin' pytest --isort --flake8 --black --cov