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 2023-11-13 22:18:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-django-cacheops (Old) and /work/SRC/openSUSE:Factory/.python-django-cacheops.new.17445 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-django-cacheops" Mon Nov 13 22:18:00 2023 rev:10 rq:1124878 version:7.0.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-django-cacheops/python-django-cacheops.changes 2023-05-10 16:18:14.086871882 +0200 +++ /work/SRC/openSUSE:Factory/.python-django-cacheops.new.17445/python-django-cacheops.changes 2023-11-13 22:20:37.682966331 +0100 @@ -1,0 +2,10 @@ +Fri Nov 10 12:25:25 UTC 2023 - Dirk Müller <dmuel...@suse.com> + +- update to 7.0.2: + * fixed .aggregate() + * fixed big memory usage during migrations + * fixed INSIDEOUT in older redises + * better handle model families with abstracts in them + * allow funcy 2.0+ + +------------------------------------------------------------------- Old: ---- django-cacheops-7.0.1.tar.gz New: ---- django-cacheops-7.0.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-django-cacheops.spec ++++++ --- /var/tmp/diff_new_pack.wj7dcZ/_old 2023-11-13 22:20:38.186984888 +0100 +++ /var/tmp/diff_new_pack.wj7dcZ/_new 2023-11-13 22:20:38.186984888 +0100 @@ -19,7 +19,7 @@ %define skip_python2 1 %define skip_python36 1 Name: python-django-cacheops -Version: 7.0.1 +Version: 7.0.2 Release: 0 Summary: Django ORM cache with automatic granular event-driven invalidation License: BSD-3-Clause ++++++ django-cacheops-7.0.1.tar.gz -> django-cacheops-7.0.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/CHANGELOG new/django-cacheops-7.0.2/CHANGELOG --- old/django-cacheops-7.0.1/CHANGELOG 2023-05-09 09:41:55.000000000 +0200 +++ new/django-cacheops-7.0.2/CHANGELOG 2023-10-24 12:16:26.000000000 +0200 @@ -1,3 +1,10 @@ +7.0.2 +- fixed .aggregate() +- fixed big memory usage during migrations +- fixed INSIDEOUT in older redises +- better handle model families with abstracts in them +- allow funcy 2.0+ + 7.0.1 - made it work with Redis 6.x and older again - handle abstract models better diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/PKG-INFO new/django-cacheops-7.0.2/PKG-INFO --- old/django-cacheops-7.0.1/PKG-INFO 2023-05-09 09:49:47.681410600 +0200 +++ new/django-cacheops-7.0.2/PKG-INFO 2023-10-24 12:23:36.260995400 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: django-cacheops -Version: 7.0.1 +Version: 7.0.2 Summary: A slick ORM cache with automatic granular event-driven invalidation for Django. Home-page: http://github.com/Suor/django-cacheops Author: Alexander Schepanovski Author-email: suor....@gmail.com License: BSD -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent @@ -886,5 +885,3 @@ .. |Build Status| image:: https://github.com/Suor/django-cacheops/actions/workflows/ci.yml/badge.svg :target: https://github.com/Suor/django-cacheops/actions/workflows/ci.yml?query=branch%3Amaster - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/__init__.py new/django-cacheops-7.0.2/cacheops/__init__.py --- old/django-cacheops-7.0.1/cacheops/__init__.py 2023-05-09 09:42:08.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/__init__.py 2023-10-24 12:16:49.000000000 +0200 @@ -1,4 +1,4 @@ -__version__ = '7.0.1' +__version__ = '7.0.2' VERSION = tuple(map(int, __version__.split('.'))) from .simple import * # noqa diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/conf.py new/django-cacheops-7.0.2/cacheops/conf.py --- old/django-cacheops-7.0.1/cacheops/conf.py 2023-05-05 13:51:50.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/conf.py 2023-05-19 17:36:38.000000000 +0200 @@ -96,7 +96,7 @@ """ Returns cacheops profile for a model """ - assert not model._meta.abstract, "This should be handled by caller" + assert not model._meta.abstract, "Can't get profile for %s" % model # Django migrations create lots of fake models, just skip them if model.__module__ == '__fake__': return None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/getset.py new/django-cacheops-7.0.2/cacheops/getset.py --- old/django-cacheops-7.0.1/cacheops/getset.py 2023-02-25 06:59:13.000000000 +0100 +++ new/django-cacheops-7.0.2/cacheops/getset.py 2023-10-24 12:11:51.000000000 +0200 @@ -1,6 +1,7 @@ from contextlib import contextmanager import hashlib import json +import random from .conf import settings from .redis import redis_client, handle_connection_failure, load_script @@ -35,6 +36,8 @@ json.dumps(schemes), json.dumps(conj_keys), timeout, + # Need to pass it from here since random inside is not seeded in Redis pre 7.0 + random.random(), expected_checksum, ] ) @@ -78,7 +81,7 @@ if None in stamps: redis_client.unlink(key) - return + return None stamp_checksum, data = coded.split(b':', 1) if stamp_checksum.decode() != join_stamps(stamps): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/lua/cache_thing.lua new/django-cacheops-7.0.2/cacheops/lua/cache_thing.lua --- old/django-cacheops-7.0.1/cacheops/lua/cache_thing.lua 2023-04-03 13:32:23.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/lua/cache_thing.lua 2023-05-20 07:25:51.000000000 +0200 @@ -50,13 +50,13 @@ -- REDIS_7 redis.call('expire', conj_key, timeout, 'gt') -- /REDIS_7 - -- REDIS_6 + -- REDIS_4 local conj_ttl = redis.call('ttl', conj_key) if conj_ttl < timeout then -- We set conj_key life with a margin over key life to call expire rarer -- And add few extra seconds to be extra safe redis.call('expire', conj_key, timeout * 2 + 10) end - -- /REDIS_6 + -- /REDIS_4 end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/lua/cache_thing_insideout.lua new/django-cacheops-7.0.2/cacheops/lua/cache_thing_insideout.lua --- old/django-cacheops-7.0.1/cacheops/lua/cache_thing_insideout.lua 2023-04-03 13:31:30.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/lua/cache_thing_insideout.lua 2023-05-20 08:14:06.000000000 +0200 @@ -4,7 +4,8 @@ local schemes = cjson.decode(ARGV[2]) local conj_keys = cjson.decode(ARGV[3]) local timeout = tonumber(ARGV[4]) -local expected_checksum = ARGV[5] +local rnd = ARGV[5] -- A new value for empty stamps +local expected_checksum = ARGV[6] -- Ensure schemes are known for db_table, _schemes in pairs(schemes) do @@ -13,23 +14,31 @@ -- Fill in invalidators and collect stamps local stamps = {} -local rnd = tostring(math.random()) -- A new value for empty stamps for _, conj_key in ipairs(conj_keys) do + -- REDIS_7 local stamp = redis.call('set', conj_key, rnd, 'nx', 'get') or rnd + -- /REDIS_7 + -- REDIS_4 + local stamp = redis.call('get', conj_key) + if not stamp then + stamp = rnd + redis.call('set', conj_key, rnd) + end + -- /REDIS_4 table.insert(stamps, stamp) -- NOTE: an invalidator should live longer than any key it references. -- So we update its ttl on every key if needed. -- REDIS_7 redis.call('expire', conj_key, timeout, 'gt') -- /REDIS_7 - -- REDIS_6 + -- REDIS_4 local conj_ttl = redis.call('ttl', conj_key) if conj_ttl < timeout then -- We set conj_key life with a margin over key life to call expire rarer -- And add few extra seconds to be extra safe redis.call('expire', conj_key, timeout * 2 + 10) end - -- /REDIS_6 + -- /REDIS_4 end -- Write data to cache along with a checksum of the stamps to see if any of them changed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/query.py new/django-cacheops-7.0.2/cacheops/query.py --- old/django-cacheops-7.0.1/cacheops/query.py 2023-05-05 13:51:50.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/query.py 2023-10-18 11:15:33.000000000 +0200 @@ -274,14 +274,33 @@ def aggregate(self, *args, **kwargs): if self._should_cache('aggregate'): - # We resolve aggregates to add joins, which will affect query DNF - qs = self._clone() - for aggregate_expr in chain(args, kwargs.values()): - aggregate_expr.resolve_expression( - qs.query, allow_joins=True, reuse=None, summarize=True) + # Apply all aggregates the same way original .aggregate() does, but do not perform sql. + # This code is mostly taken from QuerySet.aggregate(). + normalized_kwargs = kwargs.copy() + for arg in args: + try: + normalized_kwargs[arg.default_alias] = arg + except (AttributeError, TypeError): + # Let Django raise a proper error + return self._no_monkey.aggregate(*args, **kwargs) + + # Simulate Query.get_aggregation() preparations, this adds proper joins to qs.query + if not normalized_kwargs: + return {} - # Use resulting qs as a ref - return cached_as(qs)(lambda: self._no_monkey.aggregate(self, *args, **kwargs))() + qs = self._clone() + aggregates = {} + for alias, aggregate_expr in normalized_kwargs.items(): + aggregate = aggregate_expr.resolve_expression( + qs.query, allow_joins=True, reuse=None, summarize=True + ) + if not aggregate.contains_aggregate: + raise TypeError("%s is not an aggregate expression" % alias) + aggregates[alias] = aggregate + + # Use resulting qs as a ref, aggregates still contain names, etc + func = lambda: self._no_monkey.aggregate(self, *args, **kwargs) + return cached_as(qs, extra=aggregates)(func)() else: return self._no_monkey.aggregate(self, *args, **kwargs) @@ -397,8 +416,9 @@ def contribute_to_class(self, cls, name): self._no_monkey.contribute_to_class(self, cls, name) # NOTE: we check it here rather then inside _install_cacheops() - # because we don't want @once_per() to hold refs to all of them. - if family_has_profile(cls): + # because we don't want @once_per() and family_has_profile() memory to hold refs. + # Otherwise, temporary classes made for migrations might hoard lots of memory. + if cls.__module__ != '__fake__' and family_has_profile(cls): self._install_cacheops(cls) @skip_on_no_invalidation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/redis.py new/django-cacheops-7.0.2/cacheops/redis.py --- old/django-cacheops-7.0.1/cacheops/redis.py 2023-04-04 12:55:06.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/redis.py 2023-05-20 08:14:38.000000000 +0200 @@ -63,7 +63,7 @@ with open(filename) as f: code = f.read() if is_redis_7(): - code = re.sub(r'REDIS_6.*?/REDIS_6', '', code, flags=re.S) + code = re.sub(r'REDIS_4.*?/REDIS_4', '', code, flags=re.S) else: code = re.sub(r'REDIS_7.*?/REDIS_7', '', code, flags=re.S) return redis_client.register_script(code) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/cacheops/utils.py new/django-cacheops-7.0.2/cacheops/utils.py --- old/django-cacheops-7.0.1/cacheops/utils.py 2023-05-05 13:51:50.000000000 +0200 +++ new/django-cacheops-7.0.2/cacheops/utils.py 2023-05-19 17:43:23.000000000 +0200 @@ -1,7 +1,7 @@ import re import json import inspect -from funcy import memoize, compose, wraps, any, any_fn, select_values, lmapcat +from funcy import memoize, compose, wraps, any, any_fn, select_values, mapcat from django.db import models from django.http import HttpRequest @@ -9,10 +9,6 @@ from .conf import model_profile -def get_table_model(model): - return next((b for b in model.__mro__ if issubclass(b, models.Model) and b is not models.Model - and not b._meta.proxy and not b._meta.abstract), None) - def model_family(model): """ The family is models sharing a database table, events on one should affect each other. @@ -20,14 +16,19 @@ We simply collect a list of all proxy models, including subclasess, superclasses and siblings. Two descendants of an abstract model are not family - they cannot affect each other. """ - def class_tree(cls): - return [cls] + lmapcat(class_tree, cls.__subclasses__()) - - # NOTE: we also list multitable submodels here, we just don't care. - # Cacheops doesn't support them anyway. - table_model = get_table_model(model) - return class_tree(table_model) if table_model else [] + if model._meta.abstract: # No table - no family + return set() + @memoize + def class_tree(cls): + # NOTE: we also list multitable submodels here, we just don't care. + # Cacheops doesn't support them anyway. + return {cls} | set(mapcat(class_tree, cls.__subclasses__())) + + table_bases = {b for b in model.__mro__ if issubclass(b, models.Model) and b is not models.Model + and not b._meta.proxy and not b._meta.abstract} + family = set(mapcat(class_tree, table_bases)) + return {cls for cls in family if not cls._meta.abstract} @memoize def family_has_profile(cls): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/django_cacheops.egg-info/PKG-INFO new/django-cacheops-7.0.2/django_cacheops.egg-info/PKG-INFO --- old/django-cacheops-7.0.1/django_cacheops.egg-info/PKG-INFO 2023-05-09 09:49:47.000000000 +0200 +++ new/django-cacheops-7.0.2/django_cacheops.egg-info/PKG-INFO 2023-10-24 12:23:36.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: django-cacheops -Version: 7.0.1 +Version: 7.0.2 Summary: A slick ORM cache with automatic granular event-driven invalidation for Django. Home-page: http://github.com/Suor/django-cacheops Author: Alexander Schepanovski Author-email: suor....@gmail.com License: BSD -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent @@ -886,5 +885,3 @@ .. |Build Status| image:: https://github.com/Suor/django-cacheops/actions/workflows/ci.yml/badge.svg :target: https://github.com/Suor/django-cacheops/actions/workflows/ci.yml?query=branch%3Amaster - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/django_cacheops.egg-info/requires.txt new/django-cacheops-7.0.2/django_cacheops.egg-info/requires.txt --- old/django-cacheops-7.0.1/django_cacheops.egg-info/requires.txt 2023-05-09 09:49:47.000000000 +0200 +++ new/django-cacheops-7.0.2/django_cacheops.egg-info/requires.txt 2023-10-24 12:23:36.000000000 +0200 @@ -1,3 +1,3 @@ django>=3.2 redis>=3.0.0 -funcy<2.0,>=1.8 +funcy<3.0,>=1.8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/requirements-test.txt new/django-cacheops-7.0.2/requirements-test.txt --- old/django-cacheops-7.0.1/requirements-test.txt 2023-03-11 10:05:33.000000000 +0100 +++ new/django-cacheops-7.0.2/requirements-test.txt 2023-05-11 08:42:49.000000000 +0200 @@ -2,7 +2,7 @@ pytest-django==4.5.2 django>=3.2 redis>=3.0.0 -funcy>=1.8,<2.0 +funcy>=1.8,<3.0 before_after==1.0.0 jinja2>=2.10 dill diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/setup.py new/django-cacheops-7.0.2/setup.py --- old/django-cacheops-7.0.1/setup.py 2023-05-09 09:42:03.000000000 +0200 +++ new/django-cacheops-7.0.2/setup.py 2023-10-24 12:16:36.000000000 +0200 @@ -7,7 +7,7 @@ setup( name='django-cacheops', - version='7.0.1', + version='7.0.2', author='Alexander Schepanovski', author_email='suor....@gmail.com', @@ -26,7 +26,7 @@ install_requires=[ 'django>=3.2', 'redis>=3.0.0', - 'funcy>=1.8,<2.0', + 'funcy>=1.8,<3.0', ], classifiers=[ 'Development Status :: 5 - Production/Stable', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/tests/models.py new/django-cacheops-7.0.2/tests/models.py --- old/django-cacheops-7.0.1/tests/models.py 2023-05-05 13:51:50.000000000 +0200 +++ new/django-cacheops-7.0.2/tests/models.py 2023-05-20 07:03:30.000000000 +0200 @@ -309,6 +309,44 @@ name = models.CharField(max_length=255) +# Abstract models class Abs(models.Model): class Meta: abstract = True + +class Concrete1(Abs): + pass + +class AbsChild(Abs): + class Meta: + abstract = True + +class Concrete2(AbsChild): + pass + +class NoProfile(models.Model): + title = models.CharField(max_length=128) + +class NoProfileProxy(NoProfile): + class Meta: + proxy = True + +class AbsNoProfile(NoProfile): + class Meta: + abstract = True + +class NoProfileChild(AbsNoProfile): + pass + + +class ParentId(models.Model): + pass + +class ParentStr(models.Model): + name = models.CharField(max_length=128, primary_key=True) + +class Mess(ParentId, ParentStr): + pass + +class MessChild(Mess): + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/tests/settings.py new/django-cacheops-7.0.2/tests/settings.py --- old/django-cacheops-7.0.1/tests/settings.py 2023-05-03 09:01:43.000000000 +0200 +++ new/django-cacheops-7.0.2/tests/settings.py 2023-05-17 13:55:39.000000000 +0200 @@ -111,6 +111,7 @@ 'tests.*': {}, 'tests.noncachedvideoproxy': None, 'tests.noncachedmedia': None, + 'tests.noprofile': None, 'auth.*': {}, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/tests/test_extras.py new/django-cacheops-7.0.2/tests/test_extras.py --- old/django-cacheops-7.0.1/tests/test_extras.py 2023-05-05 13:51:50.000000000 +0200 +++ new/django-cacheops-7.0.2/tests/test_extras.py 2023-05-20 07:03:41.000000000 +0200 @@ -6,7 +6,7 @@ from cacheops.signals import cache_read, cache_invalidated from .utils import BaseTestCase, make_inc -from .models import Post, Category, Local, DbAgnostic, DbBinded, Abs +from .models import Post, Category, Local, DbAgnostic, DbBinded class SettingsTests(TestCase): @@ -185,7 +185,23 @@ list(DbBinded.objects.cache().using('slave')) -def test_abstract_family(): +def test_model_family(): from cacheops.utils import model_family + from .models import Abs, Concrete1, AbsChild, Concrete2 + from .models import NoProfile, NoProfileProxy, AbsNoProfile, NoProfileChild + from .models import ParentId, ParentStr, Mess, MessChild + + # Abstract models do not have family, children of an abstract model are not a family + assert model_family(Abs) == set() + assert model_family(Concrete1) == {Concrete1} + assert model_family(AbsChild) == set() + assert model_family(Concrete2) == {Concrete2} + + # Everything in but an abstract model + assert model_family(NoProfile) == {NoProfile, NoProfileProxy, NoProfileChild} + assert model_family(NoProfileProxy) == {NoProfile, NoProfileProxy, NoProfileChild} + assert model_family(AbsNoProfile) == set() + assert model_family(NoProfileChild) == {NoProfile, NoProfileProxy, NoProfileChild} - assert model_family(Abs) == [] + # The worst of multiple inheritance + assert model_family(Mess) == {Mess, MessChild, ParentId, ParentStr} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/django-cacheops-7.0.1/tests/tests.py new/django-cacheops-7.0.2/tests/tests.py --- old/django-cacheops-7.0.1/tests/tests.py 2023-05-05 13:51:50.000000000 +0200 +++ new/django-cacheops-7.0.2/tests/tests.py 2023-05-20 08:14:46.000000000 +0200 @@ -10,7 +10,7 @@ from django.test import override_settings from django.test.client import RequestFactory from django.template import Context, Template -from django.db.models import F, Count, OuterRef, Sum, Subquery, Exists +from django.db.models import F, Count, Max, OuterRef, Sum, Subquery, Exists, Q from django.db.models.expressions import RawSQL from cacheops import invalidate_model, invalidate_obj, \ @@ -794,6 +794,16 @@ with self.assertNumQueries(0): qs.aggregate(posts_count=Count('posts')) + def test_new_alias(self): + qs = Post.objects.cache() + assert qs.aggregate(max=Max('category')) == {'max': 3} + assert qs.aggregate(cat=Max('category')) == {'cat': 3} + + def test_filter(self): + qs = Post.objects.cache() + assert qs.aggregate(cnt=Count('category', filter=Q(category__gt=1))) == {'cnt': 2} + assert qs.aggregate(cnt=Count('category', filter=Q(category__lt=3))) == {'cnt': 1} + class M2MTests(BaseTestCase): brand_cls = Brand