Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-django-cacheops for
openSUSE:Factory checked in at 2021-05-11 23:04:18
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-cacheops (Old)
and /work/SRC/openSUSE:Factory/.python-django-cacheops.new.2988 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-django-cacheops"
Tue May 11 23:04:18 2021 rev:3 rq:892240 version:6.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-django-cacheops/python-django-cacheops.changes
2020-10-29 14:53:02.265260023 +0100
+++
/work/SRC/openSUSE:Factory/.python-django-cacheops.new.2988/python-django-cacheops.changes
2021-05-11 23:04:26.608880202 +0200
@@ -1,0 +2,17 @@
+Sun May 9 23:34:35 UTC 2021 - Daniel Molkentin <[email protected]>
+
+- Update to v6.0
+ * support and test against Python 3.9 and Django 3.1/3.2
+ * added custom serializers support (thx to Arcady Usov)
+ * support callable extra in @cached_as() and friends
+ * made simple cache obey prefix
+ * skip JSONFields for purposes of invalidation
+ * configure skipped fields by internal types, classes still supported
+ * handle `DatabaseError` on transaction cleanup (Roman Gorbil)
+ * do not query old object if cacheops is disabled
+ * do not fetch deferred fields during invalidation, fixes #387
+ Backwards incompatible changes:
+ * callable `extra` param, including type, now behaves differently
+ * simple cache now uses prefix
+
+-------------------------------------------------------------------
Old:
----
django-cacheops-5.1.tar.gz
New:
----
django-cacheops-6.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-django-cacheops.spec ++++++
--- /var/tmp/diff_new_pack.cWXJvb/_old 2021-05-11 23:04:27.044878214 +0200
+++ /var/tmp/diff_new_pack.cWXJvb/_new 2021-05-11 23:04:27.048878195 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-django-cacheops
#
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-django-cacheops
-Version: 5.1
+Version: 6.0
Release: 0
Summary: Django ORM cache with automatic granular event-driven
invalidation
License: BSD-3-Clause
++++++ django-cacheops-5.1.tar.gz -> django-cacheops-6.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/CHANGELOG
new/django-cacheops-6.0/CHANGELOG
--- old/django-cacheops-5.1/CHANGELOG 2020-10-25 14:47:14.000000000 +0100
+++ new/django-cacheops-6.0/CHANGELOG 2021-05-03 11:08:24.000000000 +0200
@@ -1,3 +1,17 @@
+6.0
+- support and test against Python 3.9 and Django 3.1/3.2
+- added custom serializers support (thx to Arcady Usov)
+- support callable extra in @cached_as() and friends
+- made simple cache obey prefix
+- skip JSONFields for purposes of invalidation
+- configure skipped fields by internal types, classes still supported
+- handle `DatabaseError` on transaction cleanup (Roman Gorbil)
+- do not query old object if cacheops is disabled
+- do not fetch deferred fields during invalidation, fixes #387
+Backwards incompatible changes:
+- callable `extra` param, including type, now behaves differently
+- simple cache now uses prefix
+
5.1
- support subqueries in annotations (Jeremy Stretch)
- included tests into distro (John Vandenberg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/PKG-INFO
new/django-cacheops-6.0/PKG-INFO
--- old/django-cacheops-5.1/PKG-INFO 2020-10-25 15:11:24.448593400 +0100
+++ new/django-cacheops-6.0/PKG-INFO 2021-05-03 13:09:39.016655400 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.2
Name: django-cacheops
-Version: 5.1
+Version: 6.0
Summary: A slick ORM cache with automatic granular event-driven invalidation
for Django.
Home-page: http://github.com/Suor/django-cacheops
Author: Alexander Schepanovski
@@ -275,9 +275,18 @@
@cached_view_as(News)
def news_index(request):
# ...
- return HttpResponse(...)
+ return render(...)
+
+ You can pass ``timeout``, ``extra`` and several samples the same way
as to ``@cached_as()``. Note that you can pass a function as ``extra``:
- You can pass ``timeout``, ``extra`` and several samples the same way
as to ``@cached_as()``.
+ .. code:: python
+
+ @cached_view_as(News, extra=lambda req: req.user.is_staff)
+ def news_index(request):
+ # ... add extra things for staff
+ return render(...)
+
+ A function passed as ``extra`` receives the same arguments as the
cached function.
Class based views can also be cached:
@@ -286,7 +295,7 @@
class NewsIndex(ListView):
model = News
- news_index = cached_view_as(News)(NewsIndex.as_view())
+ news_index = cached_view_as(News, ...)(NewsIndex.as_view())
Invalidation
@@ -663,6 +672,27 @@
**NOTE:** prefix is not used in simple and file cache. This might
change in future cacheops.
+ Custom serialization
+ --------------------
+
+ Cacheops uses ``pickle`` by default, employing it's default protocol.
But you can specify your own
+ it might be any module or a class having `.dumps()` and `.loads()`
functions. For example you can use ``dill`` instead, which can serialize more
things like anonymous functions:
+
+ .. code:: python
+
+ CACHEOPS_SERIALIZER = 'dill'
+
+ One less obvious use is to fix pickle protocol, to use cacheops cache
across python versions:
+
+ .. code:: python
+
+ import pickle
+
+ class CACHEOPS_SERIALIZER:
+ dumps = lambda data: pickle.dumps(data, 3)
+ loads = pickle.loads
+
+
Using memory limit
------------------
@@ -804,10 +834,13 @@
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
Classifier: Framework :: Django
Classifier: Framework :: Django :: 2.1
Classifier: Framework :: Django :: 2.2
Classifier: Framework :: Django :: 3.0
+Classifier: Framework :: Django :: 3.1
+Classifier: Framework :: Django :: 3.2
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/README.rst
new/django-cacheops-6.0/README.rst
--- old/django-cacheops-5.1/README.rst 2020-07-18 08:34:04.000000000 +0200
+++ new/django-cacheops-6.0/README.rst 2021-04-30 11:03:42.000000000 +0200
@@ -265,9 +265,18 @@
@cached_view_as(News)
def news_index(request):
# ...
- return HttpResponse(...)
+ return render(...)
+
+You can pass ``timeout``, ``extra`` and several samples the same way as to
``@cached_as()``. Note that you can pass a function as ``extra``:
-You can pass ``timeout``, ``extra`` and several samples the same way as to
``@cached_as()``.
+.. code:: python
+
+ @cached_view_as(News, extra=lambda req: req.user.is_staff)
+ def news_index(request):
+ # ... add extra things for staff
+ return render(...)
+
+A function passed as ``extra`` receives the same arguments as the cached
function.
Class based views can also be cached:
@@ -276,7 +285,7 @@
class NewsIndex(ListView):
model = News
- news_index = cached_view_as(News)(NewsIndex.as_view())
+ news_index = cached_view_as(News, ...)(NewsIndex.as_view())
Invalidation
@@ -653,6 +662,27 @@
**NOTE:** prefix is not used in simple and file cache. This might change in
future cacheops.
+Custom serialization
+--------------------
+
+Cacheops uses ``pickle`` by default, employing it's default protocol. But you
can specify your own
+it might be any module or a class having `.dumps()` and `.loads()` functions.
For example you can use ``dill`` instead, which can serialize more things like
anonymous functions:
+
+.. code:: python
+
+ CACHEOPS_SERIALIZER = 'dill'
+
+One less obvious use is to fix pickle protocol, to use cacheops cache across
python versions:
+
+.. code:: python
+
+ import pickle
+
+ class CACHEOPS_SERIALIZER:
+ dumps = lambda data: pickle.dumps(data, 3)
+ loads = pickle.loads
+
+
Using memory limit
------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/conf.py
new/django-cacheops-6.0/cacheops/conf.py
--- old/django-cacheops-5.1/cacheops/conf.py 2020-06-21 08:00:30.000000000
+0200
+++ new/django-cacheops-6.0/cacheops/conf.py 2021-05-03 11:00:18.000000000
+0200
@@ -1,10 +1,9 @@
+from importlib import import_module
from funcy import memoize, merge
from django.conf import settings as base_settings
from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed
-from django.db import models
-from django.utils.module_loading import import_string
ALL_OPS = {'get', 'fetch', 'count', 'aggregate', 'exists'}
@@ -22,8 +21,9 @@
CACHEOPS_SENTINEL = {}
# NOTE: we don't use this fields in invalidator conditions since their
values could be very long
# and one should not filter by their equality anyway.
- CACHEOPS_SKIP_FIELDS = models.FileField, models.TextField,
models.BinaryField
+ CACHEOPS_SKIP_FIELDS = "FileField", "TextField", "BinaryField", "JSONField"
CACHEOPS_LONG_DISJUNCTION = 8
+ CACHEOPS_SERIALIZER = 'pickle'
FILE_CACHE_DIR = '/tmp/cacheops_file_cache'
FILE_CACHE_TIMEOUT = 60*60*24*30
@@ -32,8 +32,13 @@
class Settings(object):
def __getattr__(self, name):
res = getattr(base_settings, name, getattr(Defaults, name))
- if name == 'CACHEOPS_PREFIX':
- res = res if callable(res) else import_string(res)
+ if name in ['CACHEOPS_PREFIX', 'CACHEOPS_SERIALIZER']:
+ res = import_string(res) if isinstance(res, str) else res
+
+ # Convert old list of classes to list of strings
+ if name == 'CACHEOPS_SKIP_FIELDS':
+ res = [f if isinstance(f, str) else f.get_internal_type(res) for f
in res]
+
# Save to dict to speed up next access, __getattr__ won't be called
self.__dict__[name] = res
return res
@@ -42,6 +47,14 @@
setting_changed.connect(lambda setting, **kw: settings.__dict__.pop(setting,
None), weak=False)
+def import_string(path):
+ if "." in path:
+ module, attr = path.rsplit(".", 1)
+ return getattr(import_module(module), attr)
+ else:
+ return import_module(path)
+
+
@memoize
def prepare_profiles():
"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/invalidation.py
new/django-cacheops-6.0/cacheops/invalidation.py
--- old/django-cacheops-5.1/cacheops/invalidation.py 2020-05-15
12:03:28.000000000 +0200
+++ new/django-cacheops-6.0/cacheops/invalidation.py 2021-02-03
04:56:58.000000000 +0100
@@ -1,6 +1,6 @@
import json
import threading
-from funcy import memoize, post_processing, ContextDecorator
+from funcy import memoize, post_processing, ContextDecorator, decorator
from django.db import DEFAULT_DB_ALIAS
from django.db.models.expressions import F, Expression
@@ -14,6 +14,14 @@
__all__ = ('invalidate_obj', 'invalidate_model', 'invalidate_all',
'no_invalidation')
+@decorator
+def skip_on_no_invalidation(call):
+ if not settings.CACHEOPS_ENABLED or no_invalidation.active:
+ return
+ return call()
+
+
+@skip_on_no_invalidation
@queue_when_in_transaction
@handle_connection_failure
def invalidate_dict(model, obj_dict, using=DEFAULT_DB_ALIAS):
@@ -28,6 +36,7 @@
cache_invalidated.send(sender=model, obj_dict=obj_dict)
+@skip_on_no_invalidation
def invalidate_obj(obj, using=DEFAULT_DB_ALIAS):
"""
Invalidates caches that can possibly be influenced by object
@@ -36,6 +45,7 @@
invalidate_dict(model, get_obj_dict(model, obj), using=using)
+@skip_on_no_invalidation
@queue_when_in_transaction
@handle_connection_failure
def invalidate_model(model, using=DEFAULT_DB_ALIAS):
@@ -44,8 +54,6 @@
NOTE: This is a heavy artillery which uses redis KEYS request,
which could be relatively slow on large datasets.
"""
- if no_invalidation.active or not settings.CACHEOPS_ENABLED:
- return
model = model._meta.concrete_model
# NOTE: if we use sharding dependent on DNF then this will fail,
# which is ok, since it's hard/impossible to predict all the shards
@@ -58,10 +66,9 @@
cache_invalidated.send(sender=model, obj_dict=None)
+@skip_on_no_invalidation
@handle_connection_failure
def invalidate_all():
- if no_invalidation.active or not settings.CACHEOPS_ENABLED:
- return
redis_client.flushdb()
cache_invalidated.send(sender=None, obj_dict=None)
@@ -90,12 +97,17 @@
@memoize
def serializable_fields(model):
- return tuple(f for f in model._meta.fields
- if not isinstance(f, settings.CACHEOPS_SKIP_FIELDS))
+ return {f for f in model._meta.fields
+ if f.get_internal_type() not in settings.CACHEOPS_SKIP_FIELDS}
@post_processing(dict)
def get_obj_dict(model, obj):
for field in serializable_fields(model):
+ # Skip deferred fields, in post_delete trying to fetch them results in
error anyway.
+ # In post_save we rely on deferred values be the same as in pre_save.
+ if field.attname not in obj.__dict__:
+ continue
+
value = getattr(obj, field.attname)
if value is None:
yield field.attname, None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/query.py
new/django-cacheops-6.0/cacheops/query.py
--- old/django-cacheops-5.1/cacheops/query.py 2020-09-26 10:56:10.000000000
+0200
+++ new/django-cacheops-6.0/cacheops/query.py 2021-04-30 11:04:51.000000000
+0200
@@ -1,13 +1,12 @@
import sys
import json
import threading
-import pickle
from random import random
from funcy import select_keys, cached_property, once, once_per, monkey, wraps,
walk, chain
from funcy import lmap, map, lcat, join_with
-from django.utils.encoding import smart_str, force_text
+from django.utils.encoding import force_str
from django.core.exceptions import ImproperlyConfigured, EmptyResultSet
from django.db import DEFAULT_DB_ALIAS
from django.db import models
@@ -23,12 +22,12 @@
MAX_GET_RESULTS = None
from .conf import model_profile, settings, ALL_OPS
-from .utils import monkey_mix, stamp_fields, func_cache_key, cached_view_fab,
family_has_profile
+from .utils import monkey_mix, stamp_fields, get_cache_key, cached_view_fab,
family_has_profile
from .utils import md5
from .sharding import get_prefix
from .redis import redis_client, handle_connection_failure, load_script
from .tree import dnfs
-from .invalidation import invalidate_obj, invalidate_dict, no_invalidation
+from .invalidation import invalidate_obj, invalidate_dict,
skip_on_no_invalidation
from .transaction import transaction_states
from .signals import cache_read
@@ -52,15 +51,14 @@
load_script('cache_thing', settings.CACHEOPS_LRU)(
keys=[prefix, cache_key, precall_key],
args=[
- pickle.dumps(data, -1),
+ settings.CACHEOPS_SERIALIZER.dumps(data),
json.dumps(cond_dnfs, default=str),
timeout
]
)
-def cached_as(*samples, timeout=None, extra=None, lock=None, keep_fresh=False,
- key_func=func_cache_key):
+def cached_as(*samples, timeout=None, extra=None, lock=None, keep_fresh=False):
"""
Caches results of a function and invalidates them same way as given
queryset(s).
NOTE: Ignores queryset cached ops settings, always caches.
@@ -92,8 +90,7 @@
querysets = lmap(_get_queryset, samples)
dbs = list({qs.db for qs in querysets})
cond_dnfs = join_with(lcat, map(dnfs, querysets))
- key_extra = [qs._cache_key(prefix=False) for qs in querysets]
- key_extra.append(extra)
+ qs_keys = [qs._cache_key(prefix=False) for qs in querysets]
if timeout is None:
timeout = min(qs._cacheprofile['timeout'] for qs in querysets)
if lock is None:
@@ -106,12 +103,13 @@
return func(*args, **kwargs)
prefix = get_prefix(func=func, _cond_dnfs=cond_dnfs, dbs=dbs)
- cache_key = prefix + 'as:' + key_func(func, args, kwargs,
key_extra)
+ extra_val = extra(*args, **kwargs) if callable(extra) else extra
+ cache_key = prefix + 'as:' + get_cache_key(func, args, kwargs,
qs_keys, extra_val)
with redis_client.getting(cache_key, lock=lock) as cache_data:
cache_read.send(sender=None, func=func, hit=cache_data is not
None)
if cache_data is not None:
- return pickle.loads(cache_data)
+ return settings.CACHEOPS_SERIALIZER.loads(cache_data)
else:
if keep_fresh:
# We call this "asp" for "as precall" because this key
is
@@ -119,7 +117,7 @@
# the key to prevent falsely thinking the key was not
# invalidated when in fact it was invalidated and the
# function was called again in another process.
- suffix = key_func(func, args, kwargs, key_extra +
[random()])
+ suffix = get_cache_key(func, args, kwargs, qs_keys,
extra_val, random())
precall_key = prefix + 'asp:' + suffix
# Cache a precall_key to watch for invalidation during
# the function call. Its value does not matter. If and
@@ -176,8 +174,8 @@
try:
sql_str = sql % params
except UnicodeDecodeError:
- sql_str = sql % walk(force_text, params)
- md.update(smart_str(sql_str))
+ sql_str = sql % walk(force_str, params)
+ md.update(force_str(sql_str))
except EmptyResultSet:
pass
# If query results differ depending on database
@@ -278,7 +276,7 @@
with redis_client.getting(cache_key, lock=lock) as cache_data:
cache_read.send(sender=self.model, func=None, hit=cache_data is
not None)
if cache_data is not None:
- self._result_cache = pickle.loads(cache_data)
+ self._result_cache =
settings.CACHEOPS_SERIALIZER.loads(cache_data)
else:
self._result_cache = list(self._iterable_class(self))
self._cache_results(cache_key, self._result_cache)
@@ -428,18 +426,18 @@
if cls.__module__ != '__fake__' and family_has_profile(cls):
self._install_cacheops(cls)
+ @skip_on_no_invalidation
def _pre_save(self, sender, instance, using, **kwargs):
- if not (instance.pk is None or instance._state.adding or
no_invalidation.active):
+ if instance.pk is not None and not instance._state.adding:
try:
+ # TODO: do not fetch non-serializable fields
_old_objs.__dict__[sender, instance.pk] \
= sender.objects.using(using).get(pk=instance.pk)
except sender.DoesNotExist:
pass
+ @skip_on_no_invalidation
def _post_save(self, sender, instance, using, **kwargs):
- if not settings.CACHEOPS_ENABLED or no_invalidation.active:
- return
-
# Invoke invalidations for both old and new versions of saved object
old = _old_objs.__dict__.pop((sender, instance.pk), None)
if old:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/serializers.py
new/django-cacheops-6.0/cacheops/serializers.py
--- old/django-cacheops-5.1/cacheops/serializers.py 1970-01-01
01:00:00.000000000 +0100
+++ new/django-cacheops-6.0/cacheops/serializers.py 2021-04-30
10:44:54.000000000 +0200
@@ -0,0 +1,11 @@
+import pickle
+
+
+class PickleSerializer:
+ # properties
+ PickleError = pickle.PickleError
+ HIGHEST_PROTOCOL = pickle.HIGHEST_PROTOCOL
+
+ # methods
+ dumps = pickle.dumps
+ loads = pickle.loads
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/signals.py
new/django-cacheops-6.0/cacheops/signals.py
--- old/django-cacheops-5.1/cacheops/signals.py 2017-06-04 05:41:28.000000000
+0200
+++ new/django-cacheops-6.0/cacheops/signals.py 2021-02-19 04:50:58.000000000
+0100
@@ -1,4 +1,4 @@
import django.dispatch
-cache_read = django.dispatch.Signal(providing_args=["func", "hit"])
-cache_invalidated = django.dispatch.Signal(providing_args=["obj_dict"])
+cache_read = django.dispatch.Signal() # args: func, hit
+cache_invalidated = django.dispatch.Signal() # args: obj_dict
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/simple.py
new/django-cacheops-6.0/cacheops/simple.py
--- old/django-cacheops-5.1/cacheops/simple.py 2020-01-05 04:46:06.000000000
+0100
+++ new/django-cacheops-6.0/cacheops/simple.py 2021-05-01 11:57:11.000000000
+0200
@@ -1,12 +1,12 @@
import os
-import pickle
import time
from funcy import wraps
from .conf import settings
-from .utils import func_cache_key, cached_view_fab, md5hex
+from .utils import get_cache_key, cached_view_fab, md5hex
from .redis import redis_client, handle_connection_failure
+from .sharding import get_prefix
__all__ = ('cache', 'cached', 'cached_view', 'file_cache', 'CacheMiss',
'FileCache', 'RedisCache')
@@ -24,25 +24,29 @@
return self
def get(self):
- self.cache.get(self)
+ self.cache._get(self)
def set(self, value):
- self.cache.set(self, value, self.timeout)
+ self.cache._set(self, value, self.timeout)
def delete(self):
- self.cache.delete(self)
+ self.cache._delete(self)
class BaseCache(object):
"""
Simple cache with time-based invalidation
"""
- def cached(self, timeout=None, extra=None, key_func=func_cache_key):
+ def cached(self, timeout=None, extra=None):
"""
A decorator for caching function calls
"""
# Support @cached (without parentheses) form
if callable(timeout):
- return self.cached(key_func=key_func)(timeout)
+ return self.cached()(timeout)
+
+ def _get_key(func, args, kwargs):
+ extra_val = extra(*args, **kwargs) if callable(extra) else extra
+ return get_prefix(func=func) + 'c:' + get_cache_key(func, args,
kwargs, extra_val)
def decorator(func):
@wraps(func)
@@ -50,23 +54,21 @@
if not settings.CACHEOPS_ENABLED:
return func(*args, **kwargs)
- cache_key = 'c:' + key_func(func, args, kwargs, extra)
+ cache_key = _get_key(func, args, kwargs)
try:
- result = self.get(cache_key)
+ result = self._get(cache_key)
except CacheMiss:
result = func(*args, **kwargs)
- self.set(cache_key, result, timeout)
+ self._set(cache_key, result, timeout)
return result
def invalidate(*args, **kwargs):
- cache_key = 'c:' + key_func(func, args, kwargs, extra)
- self.delete(cache_key)
+ self._delete(_get_key(func, args, kwargs))
wrapper.invalidate = invalidate
def key(*args, **kwargs):
- cache_key = 'c:' + key_func(func, args, kwargs, extra)
- return CacheKey.make(cache_key, cache=self, timeout=timeout)
+ return CacheKey.make(_get_key(func, args, kwargs), cache=self,
timeout=timeout)
wrapper.key = key
return wrapper
@@ -77,27 +79,36 @@
return self.cached_view()(timeout)
return cached_view_fab(self.cached)(timeout=timeout, extra=extra)
+ def get(self, cache_key):
+ return self._get(get_prefix() + cache_key)
+
+ def set(self, cache_key, data, timeout=None):
+ self._set(get_prefix() + cache_key, data, timeout)
+
+ def delete(self, cache_key):
+ self._delete(get_prefix() + cache_key)
+
class RedisCache(BaseCache):
def __init__(self, conn):
self.conn = conn
- def get(self, cache_key):
+ def _get(self, cache_key):
data = self.conn.get(cache_key)
if data is None:
raise CacheMiss
- return pickle.loads(data)
+ return settings.CACHEOPS_SERIALIZER.loads(data)
@handle_connection_failure
- def set(self, cache_key, data, timeout=None):
- pickled_data = pickle.dumps(data, -1)
+ def _set(self, cache_key, data, timeout=None):
+ pickled_data = settings.CACHEOPS_SERIALIZER.dumps(data)
if timeout is not None:
self.conn.setex(cache_key, timeout, pickled_data)
else:
self.conn.set(cache_key, pickled_data)
@handle_connection_failure
- def delete(self, cache_key):
+ def _delete(self, cache_key):
self.conn.delete(cache_key)
cache = RedisCache(redis_client)
@@ -122,7 +133,7 @@
digest = md5hex(key)
return os.path.join(self._dir, digest[-2:], digest[:-2])
- def get(self, key):
+ def _get(self, key):
filename = self._key_to_filename(key)
try:
# Remove file if it's stale
@@ -131,11 +142,11 @@
raise CacheMiss
with open(filename, 'rb') as f:
- return pickle.load(f)
- except (IOError, OSError, EOFError, pickle.PickleError):
+ return settings.CACHEOPS_SERIALIZER.load(f)
+ except (IOError, OSError, EOFError):
raise CacheMiss
- def set(self, key, data, timeout=None):
+ def _set(self, key, data, timeout=None):
filename = self._key_to_filename(key)
dirname = os.path.dirname(filename)
@@ -149,7 +160,7 @@
# Use open with exclusive rights to prevent data corruption
f = os.open(filename, os.O_EXCL | os.O_WRONLY | os.O_CREAT)
try:
- os.write(f, pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
+ os.write(f, settings.CACHEOPS_SERIALIZER.dumps(data))
finally:
os.close(f)
@@ -158,7 +169,7 @@
except (IOError, OSError):
pass
- def delete(self, fname):
+ def _delete(self, fname):
try:
os.remove(fname)
# Trying to remove directory in case it's empty
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/transaction.py
new/django-cacheops-6.0/cacheops/transaction.py
--- old/django-cacheops-5.1/cacheops/transaction.py 2020-01-05
04:24:04.000000000 +0100
+++ new/django-cacheops-6.0/cacheops/transaction.py 2021-04-30
06:08:02.000000000 +0200
@@ -3,7 +3,7 @@
from funcy import once, decorator
-from django.db import DEFAULT_DB_ALIAS
+from django.db import DEFAULT_DB_ALIAS, DatabaseError
from django.db.backends.utils import CursorWrapper
from django.db.transaction import Atomic, get_connection, on_commit
@@ -73,13 +73,17 @@
def __exit__(self, exc_type, exc_value, traceback):
connection = get_connection(self.using)
- self._no_monkey.__exit__(self, exc_type, exc_value, traceback)
- if not connection.closed_in_transaction and exc_type is None and \
- not connection.needs_rollback:
- if transaction_states[self.using]:
- transaction_states[self.using].commit()
- else:
+ try:
+ self._no_monkey.__exit__(self, exc_type, exc_value, traceback)
+ except DatabaseError:
transaction_states[self.using].rollback()
+ else:
+ if not connection.closed_in_transaction and exc_type is None and \
+ not connection.needs_rollback:
+ if transaction_states[self.using]:
+ transaction_states[self.using].commit()
+ else:
+ transaction_states[self.using].rollback()
class CursorWrapperMixin(object):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/tree.py
new/django-cacheops-6.0/cacheops/tree.py
--- old/django-cacheops-5.1/cacheops/tree.py 2020-09-26 11:01:39.000000000
+0200
+++ new/django-cacheops-6.0/cacheops/tree.py 2021-02-03 04:56:09.000000000
+0100
@@ -10,6 +10,7 @@
from django.db.models.expressions import BaseExpression, Exists
from .conf import settings
+from .invalidation import serializable_fields
def dnfs(qs):
@@ -44,7 +45,7 @@
if isinstance(where.rhs, (QuerySet, Query, BaseExpression)):
return SOME_TREE
# Skip conditions on non-serialized fields
- if isinstance(where.lhs.target, settings.CACHEOPS_SKIP_FIELDS):
+ if where.lhs.target not in
serializable_fields(where.lhs.target.model):
return SOME_TREE
attname = where.lhs.target.attname
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/cacheops/utils.py
new/django-cacheops-6.0/cacheops/utils.py
--- old/django-cacheops-5.1/cacheops/utils.py 2020-06-21 07:49:11.000000000
+0200
+++ new/django-cacheops-6.0/cacheops/utils.py 2021-04-23 06:10:03.000000000
+0200
@@ -77,6 +77,8 @@
def obj_key(obj):
if isinstance(obj, models.Model):
return '%s.%s.%s' % (obj._meta.app_label, obj._meta.model_name, obj.pk)
+ elif hasattr(obj, 'build_absolute_uri'):
+ return obj.build_absolute_uri() # Only vary HttpRequest by uri
elif inspect.isfunction(obj):
factors = [obj.__module__, obj.__name__]
# Really useful to ignore this while code still in development
@@ -86,24 +88,9 @@
else:
return str(obj)
-def func_cache_key(func, args, kwargs, extra=None):
- """
- Calculate cache key based on func and arguments
- """
- factors = [func, args, kwargs, extra]
+def get_cache_key(*factors):
return md5hex(json.dumps(factors, sort_keys=True, default=obj_key))
-def view_cache_key(func, args, kwargs, extra=None):
- """
- Calculate cache key for view func.
- Use url instead of not properly serializable request argument.
- """
- if hasattr(args[0], 'build_absolute_uri'):
- uri = args[0].build_absolute_uri()
- else:
- uri = args[0]
- return 'v:' + func_cache_key(func, args[1:], kwargs, extra=(uri, extra))
-
def cached_view_fab(_cached):
def force_render(response):
if hasattr(response, 'render') and callable(response.render):
@@ -112,7 +99,6 @@
def cached_view(*dargs, **dkwargs):
def decorator(func):
- dkwargs['key_func'] = view_cache_key
cached_func = _cached(*dargs, **dkwargs)(compose(force_render,
func))
@wraps(func)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django-cacheops-5.1/django_cacheops.egg-info/PKG-INFO
new/django-cacheops-6.0/django_cacheops.egg-info/PKG-INFO
--- old/django-cacheops-5.1/django_cacheops.egg-info/PKG-INFO 2020-10-25
15:11:24.000000000 +0100
+++ new/django-cacheops-6.0/django_cacheops.egg-info/PKG-INFO 2021-05-03
13:09:38.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.2
Name: django-cacheops
-Version: 5.1
+Version: 6.0
Summary: A slick ORM cache with automatic granular event-driven invalidation
for Django.
Home-page: http://github.com/Suor/django-cacheops
Author: Alexander Schepanovski
@@ -275,9 +275,18 @@
@cached_view_as(News)
def news_index(request):
# ...
- return HttpResponse(...)
+ return render(...)
+
+ You can pass ``timeout``, ``extra`` and several samples the same way
as to ``@cached_as()``. Note that you can pass a function as ``extra``:
- You can pass ``timeout``, ``extra`` and several samples the same way
as to ``@cached_as()``.
+ .. code:: python
+
+ @cached_view_as(News, extra=lambda req: req.user.is_staff)
+ def news_index(request):
+ # ... add extra things for staff
+ return render(...)
+
+ A function passed as ``extra`` receives the same arguments as the
cached function.
Class based views can also be cached:
@@ -286,7 +295,7 @@
class NewsIndex(ListView):
model = News
- news_index = cached_view_as(News)(NewsIndex.as_view())
+ news_index = cached_view_as(News, ...)(NewsIndex.as_view())
Invalidation
@@ -663,6 +672,27 @@
**NOTE:** prefix is not used in simple and file cache. This might
change in future cacheops.
+ Custom serialization
+ --------------------
+
+ Cacheops uses ``pickle`` by default, employing it's default protocol.
But you can specify your own
+ it might be any module or a class having `.dumps()` and `.loads()`
functions. For example you can use ``dill`` instead, which can serialize more
things like anonymous functions:
+
+ .. code:: python
+
+ CACHEOPS_SERIALIZER = 'dill'
+
+ One less obvious use is to fix pickle protocol, to use cacheops cache
across python versions:
+
+ .. code:: python
+
+ import pickle
+
+ class CACHEOPS_SERIALIZER:
+ dumps = lambda data: pickle.dumps(data, 3)
+ loads = pickle.loads
+
+
Using memory limit
------------------
@@ -804,10 +834,13 @@
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
Classifier: Framework :: Django
Classifier: Framework :: Django :: 2.1
Classifier: Framework :: Django :: 2.2
Classifier: Framework :: Django :: 3.0
+Classifier: Framework :: Django :: 3.1
+Classifier: Framework :: Django :: 3.2
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/django-cacheops-5.1/django_cacheops.egg-info/SOURCES.txt
new/django-cacheops-6.0/django_cacheops.egg-info/SOURCES.txt
--- old/django-cacheops-5.1/django_cacheops.egg-info/SOURCES.txt
2020-10-25 15:11:24.000000000 +0100
+++ new/django-cacheops-6.0/django_cacheops.egg-info/SOURCES.txt
2021-05-03 13:09:39.000000000 +0200
@@ -15,6 +15,7 @@
cacheops/jinja2.py
cacheops/query.py
cacheops/redis.py
+cacheops/serializers.py
cacheops/sharding.py
cacheops/signals.py
cacheops/simple.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/requirements-test.txt
new/django-cacheops-6.0/requirements-test.txt
--- old/django-cacheops-5.1/requirements-test.txt 2020-08-06
09:58:02.000000000 +0200
+++ new/django-cacheops-6.0/requirements-test.txt 2021-04-30
12:57:36.000000000 +0200
@@ -4,3 +4,4 @@
six>=1.4.0
before_after==1.0.0
jinja2>=2.10
+dill
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/setup.py
new/django-cacheops-6.0/setup.py
--- old/django-cacheops-5.1/setup.py 2020-10-25 14:48:47.000000000 +0100
+++ new/django-cacheops-6.0/setup.py 2021-05-03 11:10:18.000000000 +0200
@@ -10,7 +10,7 @@
setup(
name='django-cacheops',
- version='5.1',
+ version='6.0',
author='Alexander Schepanovski',
author_email='[email protected]',
@@ -41,10 +41,13 @@
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
'Framework :: Django',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
+ 'Framework :: Django :: 3.1',
+ 'Framework :: Django :: 3.2',
'Environment :: Web Environment',
'Intended Audience :: Developers',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tests/bench.py
new/django-cacheops-6.0/tests/bench.py
--- old/django-cacheops-5.1/tests/bench.py 2020-01-05 04:34:45.000000000
+0100
+++ new/django-cacheops-6.0/tests/bench.py 2021-04-30 11:06:03.000000000
+0200
@@ -1,6 +1,5 @@
-import pickle
-
from cacheops import invalidate_obj, invalidate_model
+from cacheops.conf import settings
from cacheops.redis import redis_client
from cacheops.tree import dnfs
@@ -8,13 +7,13 @@
posts = list(Post.objects.cache().all())
-posts_pickle = pickle.dumps(posts, -1)
+posts_pickle = settings.CACHEOPS_SERIALIZER.dumps(posts)
def do_pickle():
- pickle.dumps(posts, -1)
+ settings.CACHEOPS_SERIALIZER.dumps(posts)
def do_unpickle():
- pickle.loads(posts_pickle)
+ settings.CACHEOPS_SERIALIZER.loads(posts_pickle)
get_key = Category.objects.filter(pk=1).order_by()._cache_key()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tests/models.py
new/django-cacheops-6.0/tests/models.py
--- old/django-cacheops-5.1/tests/models.py 2020-06-19 08:21:05.000000000
+0200
+++ new/django-cacheops-6.0/tests/models.py 2021-04-30 10:44:54.000000000
+0200
@@ -106,14 +106,17 @@
# TODO: check other new fields:
-# - PostgreSQL ones: ArrayField, HStoreField, RangeFields, unaccent
+# - PostgreSQL ones: HStoreField, RangeFields, unaccent
# - Other: DurationField
if os.environ.get('CACHEOPS_DB') in {'postgresql', 'postgis'}:
from django.contrib.postgres.fields import ArrayField
try:
- from django.contrib.postgres.fields import JSONField
+ from django.db.models import JSONField
except ImportError:
- JSONField = None
+ try:
+ from django.contrib.postgres.fields import JSONField # Used
before Django 3.1
+ except ImportError:
+ JSONField = None
class TaggedPost(models.Model):
name = models.CharField(max_length=200)
@@ -267,3 +270,19 @@
blank=True,
null=True
)
+
+
+# 385
+class Client(models.Model):
+ def __init__(self, *args, **kwargs):
+ # copied from Django 2.1.5 (not exists in Django 3.1.5 installed by
current requirements)
+ def curry(_curried_func, *args, **kwargs):
+ def _curried(*moreargs, **morekwargs):
+ return _curried_func(*args, *moreargs, **{**kwargs,
**morekwargs})
+
+ return _curried
+
+ super().__init__(*args, **kwargs)
+ setattr(self, '_get_private_data', curry(sum, [1, 2, 3, 4]))
+
+ name = models.CharField(max_length=255)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tests/settings.py
new/django-cacheops-6.0/tests/settings.py
--- old/django-cacheops-5.1/tests/settings.py 2020-07-09 11:12:51.000000000
+0200
+++ new/django-cacheops-6.0/tests/settings.py 2021-04-23 08:08:01.000000000
+0200
@@ -13,6 +13,8 @@
AUTH_PROFILE_MODULE = 'tests.UserProfile'
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
# Django replaces this, but it still wants it. *shrugs*
DATABASE_ENGINE = 'django.db.backends.sqlite3',
if os.environ.get('CACHEOPS_DB') == 'postgresql':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tests/tests.py
new/django-cacheops-6.0/tests/tests.py
--- old/django-cacheops-5.1/tests/tests.py 2020-10-25 14:15:12.000000000
+0100
+++ new/django-cacheops-6.0/tests/tests.py 2021-05-01 11:57:55.000000000
+0200
@@ -1,5 +1,6 @@
from contextlib import contextmanager
import re
+import platform
import unittest
import mock
@@ -638,6 +639,27 @@
categories =
Category.objects.cache().annotate(newest_post=Subquery(newest_post[:1]))
self.assertEqual(categories[0].newest_post, post.pk)
+ @unittest.skipIf(platform.python_implementation() == "PyPy", "dill doesn't
do that in PyPy")
+ def test_385(self):
+ Client.objects.create(name='Client Name')
+
+ with self.assertRaises(AttributeError) as e:
+ Client.objects.filter(name='Client Name').cache().first()
+ self.assertEqual(
+ str(e.exception),
+ "Can't pickle local object
'Client.__init__.<locals>.curry.<locals>._curried'")
+
+ invalidate_model(Client)
+
+ with override_settings(CACHEOPS_SERIALIZER='dill'):
+ with self.assertNumQueries(1):
+ Client.objects.filter(name='Client Name').cache().first()
+ Client.objects.filter(name='Client Name').cache().first()
+
+ def test_387(self):
+ post = Post.objects.defer("visible").last()
+ post.delete()
+
class RelatedTests(BaseTestCase):
fixtures = ['basic']
@@ -912,12 +934,7 @@
class SimpleCacheTests(BaseTestCase):
def test_cached(self):
- calls = [0]
-
- @cached(timeout=100)
- def get_calls(_):
- calls[0] += 1
- return calls[0]
+ get_calls = make_inc(cached(timeout=100))
self.assertEqual(get_calls(1), 1)
self.assertEqual(get_calls(1), 1)
@@ -932,12 +949,7 @@
self.assertEqual(get_calls(2), 42)
def test_cached_view(self):
- calls = [0]
-
- @cached_view(timeout=100)
- def get_calls(request):
- calls[0] += 1
- return calls[0]
+ get_calls = make_inc(cached_view(timeout=100))
factory = RequestFactory()
r1 = factory.get('/hi')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tests/tests_sharding.py
new/django-cacheops-6.0/tests/tests_sharding.py
--- old/django-cacheops-5.1/tests/tests_sharding.py 2020-01-05
04:48:16.000000000 +0100
+++ new/django-cacheops-6.0/tests/tests_sharding.py 2021-05-03
05:26:29.000000000 +0200
@@ -1,6 +1,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
+from cacheops import cache, CacheMiss
from .models import Category, Post, Extra
from .utils import BaseTestCase
@@ -41,3 +42,13 @@
def test_union_tables(self):
qs = Post.objects.filter(pk=1).union(Post.objects.filter(pk=2)).cache()
list(qs)
+
+
+class SimpleCacheTests(BaseTestCase):
+ def test_prefix(self):
+ with override_settings(CACHEOPS_PREFIX=lambda _: 'a'):
+ cache.set("key", "value")
+ self.assertEqual(cache.get("key"), "value")
+
+ with self.assertRaises(CacheMiss):
+ cache.get("key")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tests/tests_transactions.py
new/django-cacheops-6.0/tests/tests_transactions.py
--- old/django-cacheops-5.1/tests/tests_transactions.py 2020-01-05
04:22:19.000000000 +0100
+++ new/django-cacheops-6.0/tests/tests_transactions.py 2021-04-30
06:08:02.000000000 +0200
@@ -1,10 +1,10 @@
-from django.db import connection
+from django.db import connection, IntegrityError
from django.db.transaction import atomic
from django.test import TransactionTestCase
from cacheops.transaction import queue_when_in_transaction
-from .models import Category
+from .models import Category, Post
from .utils import run_in_thread
@@ -91,6 +91,22 @@
with self.assertNumQueries(1):
get_category()
+ def test_rollback_during_integrity_error(self):
+ # store category in cache
+ get_category()
+
+ # Make current DB be "dirty" by write
+ try:
+ with atomic():
+ Post.objects.create(category_id=-1, title='')
+ except IntegrityError:
+ # however, this write should be rolled back and current DB should
+ # not be "dirty"
+ pass
+
+ with self.assertNumQueries(0):
+ get_category()
+
def test_call_cacheops_cbs_before_on_commit_cbs(self):
calls = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-cacheops-5.1/tox.ini
new/django-cacheops-6.0/tox.ini
--- old/django-cacheops-5.1/tox.ini 2020-06-19 15:43:01.000000000 +0200
+++ new/django-cacheops-6.0/tox.ini 2021-05-01 11:48:33.000000000 +0200
@@ -2,11 +2,11 @@
minversion = 2.7
envlist =
lint,
- py35-dj{21,22},
- py36-dj{21,22,30},
- py37-dj{21,22,30,master},
- py38-dj{21,22,30,master},
- pypy3-dj{21,22,30}
+ py36-dj{30,31},
+ py37-dj{30,31,32},
+ py38-dj{30,31,32,master},
+ py39-dj{30,31,32,master},
+ pypy3-dj{30,31,32}
[travis:env]
@@ -14,6 +14,8 @@
2.1: dj21
2.2: dj22
3.0: dj30
+ 3.1: dj31
+ 3.2: dj32
master: djmaster
@@ -27,13 +29,16 @@
dj21: Django>=2.1,<2.2
dj22: Django>=2.2,<2.3
dj30: Django>=3.0a1,<3.1
+ dj31: Django>=3.1,<3.2
+ dj32: Django>=3.2,<3.3
djmaster: git+https://github.com/django/django
mysqlclient
- py{35,36,37,38}: psycopg2
+ py{35,36,37,38,39}: psycopg2
; gdal=={env:GDAL_VERSION:2.4}
pypy3: psycopg2cffi>=2.7.6
before_after==1.0.0
jinja2>=2.10
+ dill
commands =
./run_tests.py []
env CACHEOPS_PREFIX=1 ./run_tests.py []