Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,17 +24,18 @@ 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"
run: |
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You cann add the extra install to the line above where we install poetry.


- name: "Run tox targets for ${{ matrix.python-version }}"
run: "python -m tox"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.vagrant
sandbox/db.sqlite3
docs/_build
dist/*
Expand Down
8 changes: 5 additions & 3 deletions .python-version
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ build:
image: latest

python:
version: 3.8
version: 3.10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to upgrade to 3.14 right away?

pip_install: true
extra_requirements:
- docs
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~

Expand Down
8 changes: 0 additions & 8 deletions Vagrantfile

This file was deleted.

7 changes: 2 additions & 5 deletions cacheback/__init__.py
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
21 changes: 12 additions & 9 deletions cacheback/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -29,15 +28,15 @@ 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):
return value
return bytes(str(value), 'utf8')


class Job(object):
class Job:
"""
A cached read job.

Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -283,6 +282,10 @@ def set(self, *raw_args, **raw_kwargs):
# HELPER METHODS
# --------------

@staticmethod
def is_iterable(value):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we only use it once, I'd not have it as a separate method, especially not as a staticmethod :)

return isinstance(value, collections.abc.Iterable)

def prepare_args(self, *args):
return args

Expand Down Expand Up @@ -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)]),
Expand All @@ -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()

Expand All @@ -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
Expand Down
13 changes: 5 additions & 8 deletions cacheback/jobs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
try:
import importlib
except ImportError:
import django.utils.importlib as importlib
import importlib

from .base import Job

Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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):
Expand Down
8 changes: 2 additions & 6 deletions cacheback/utils.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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}')
41 changes: 41 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
image: redis:7-alpine
image: redis:8-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
17 changes: 8 additions & 9 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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'),
]

Expand Down
22 changes: 7 additions & 15 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$ Q=rq docker compose up
$ QUEUE=rq docker compose up

Lets make it more verbose.


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
Expand Down
6 changes: 3 additions & 3 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
}
Expand Down
Loading