Merge branch 'andrew-wang/dj4.2'

This commit is contained in:
Benedikt Willi 2023-06-06 11:43:00 +02:00
commit fb5f28aaf0
22 changed files with 178 additions and 181 deletions

View file

@ -6,21 +6,27 @@ on:
pull_request:
branches: [ master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
django-version: ['2.2', '3.2', '4.0', '4.1']
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
django-version: ['3.2', '4.1', '4.2']
exclude:
- python-version: '3.10'
django-version: '2.2'
- python-version: '3.7'
django-version: '4.0'
- python-version: '3.11'
django-version: '3.2'
- python-version: '3.11'
django-version: '4.1'
- python-version: '3.7'
django-version: '4.1'
- python-version: '3.7'
django-version: '4.2'
services:
redis:
image: redis:6
@ -71,7 +77,7 @@ jobs:
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
sudo apt-get install -y libmemcached-dev zlib1g-dev
sudo apt-get install -y libmemcached-dev zlib1g-dev libpq-dev
python -m pip install --upgrade pip wheel
python -m pip install tox tox-gh-actions coveralls
- name: Tox Test

1
.gitignore vendored
View file

@ -57,6 +57,7 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
*.sqlite3
db.sqlite3
db.sqlite3-journal

View file

@ -1,12 +1,23 @@
Whats new in django-cachalot?
==============================
2.6.0
-----
- Dropped Django 2.2 and 4.0 support
- Added Django 4.2 and Python 3.11 support
- Added psycopg support (#229)
- Updated tests to account for the `BEGIN` and `COMMIT` query changes in Django 4.2
- Standardized django version comparisons in tests
2.5.3
-----
- Verify get_meta isn't none before requesting db_table (#225 #226)
2.5.2
-----
- Added Django 4.1 support (#217)
2.5.1

View file

@ -39,7 +39,7 @@ Table of Contents:
Quickstart
----------
Cachalot officially supports Python 3.7-3.10 and Django 2.2, 3.2, and 4.0-4.1 with the databases PostgreSQL, SQLite, and MySQL.
Cachalot officially supports Python 3.7-3.11 and Django 3.2, 4.1, 4.2 with the databases PostgreSQL, SQLite, and MySQL.
Note: an upper limit on Django version is set for your safety. Please do not ignore it.

View file

@ -1,10 +1,4 @@
VERSION = (2, 5, 3)
VERSION = (2, 6, 0)
__version__ = ".".join(map(str, VERSION))
try:
from django import VERSION as DJANGO_VERSION
if DJANGO_VERSION < (3, 2):
default_app_config = "cachalot.apps.CachalotConfig"
except ImportError: # pragma: no cover
default_app_config = "cachalot.apps.CachalotConfig"
default_app_config = "cachalot.apps.CachalotConfig"

View file

@ -1,4 +1,5 @@
from contextlib import contextmanager
from typing import Any, Optional, Tuple, Union
from django.apps import apps
from django.conf import settings
@ -45,7 +46,11 @@ def _get_tables(tables_or_models):
else table_or_model._meta.db_table)
def invalidate(*tables_or_models, **kwargs):
def invalidate(
*tables_or_models: Tuple[Union[str, Any], ...],
cache_alias: Optional[str] = None,
db_alias: Optional[str] = None,
) -> None:
"""
Clears what was cached by django-cachalot implying one or more SQL tables
or models from ``tables_or_models``.
@ -62,19 +67,9 @@ def invalidate(*tables_or_models, **kwargs):
(or a combination)
:type tables_or_models: tuple of strings or models
:arg cache_alias: Alias from the Django ``CACHES`` setting
:type cache_alias: string or NoneType
:arg db_alias: Alias from the Django ``DATABASES`` setting
:type db_alias: string or NoneType
:returns: Nothing
:rtype: NoneType
"""
# TODO: Replace with positional arguments when we drop Python 2 support.
cache_alias = kwargs.pop('cache_alias', None)
db_alias = kwargs.pop('db_alias', None)
for k in kwargs:
raise TypeError(
"invalidate() got an unexpected keyword argument '%s'" % k)
send_signal = False
invalidated = set()
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
@ -90,7 +85,11 @@ def invalidate(*tables_or_models, **kwargs):
post_invalidation.send(table, db_alias=db_alias)
def get_last_invalidation(*tables_or_models, **kwargs):
def get_last_invalidation(
*tables_or_models: Tuple[Union[str, Any], ...],
cache_alias: Optional[str] = None,
db_alias: Optional[str] = None,
) -> float:
"""
Returns the timestamp of the most recent invalidation of the given
``tables_or_models``. If ``tables_or_models`` is not specified,
@ -106,19 +105,9 @@ def get_last_invalidation(*tables_or_models, **kwargs):
(or a combination)
:type tables_or_models: tuple of strings or models
:arg cache_alias: Alias from the Django ``CACHES`` setting
:type cache_alias: string or NoneType
:arg db_alias: Alias from the Django ``DATABASES`` setting
:type db_alias: string or NoneType
:returns: The timestamp of the most recent invalidation
:rtype: float
"""
# TODO: Replace with positional arguments when we drop Python 2 support.
cache_alias = kwargs.pop('cache_alias', None)
db_alias = kwargs.pop('db_alias', None)
for k in kwargs:
raise TypeError("get_last_invalidation() got an unexpected "
"keyword argument '%s'" % k)
last_invalidation = 0.0
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
list(_get_tables(tables_or_models)), cache_alias, db_alias):
@ -134,7 +123,7 @@ def get_last_invalidation(*tables_or_models, **kwargs):
@contextmanager
def cachalot_disabled(all_queries=False):
def cachalot_disabled(all_queries: bool = False):
"""
Context manager for temporarily disabling cachalot.
If you evaluate the same queryset a second time,
@ -158,7 +147,6 @@ def cachalot_disabled(all_queries=False):
the original and duplicate query.
:arg all_queries: Any query, including already evaluated queries, are re-evaluated.
:type all_queries: bool
"""
was_enabled = getattr(LOCAL_STORAGE, "cachalot_enabled", cachalot_settings.CACHALOT_ENABLED)
LOCAL_STORAGE.cachalot_enabled = False

View file

@ -9,8 +9,6 @@ SUPPORTED_DATABASE_ENGINES = {
'django.db.backends.sqlite3',
'django.db.backends.postgresql',
'django.db.backends.mysql',
# TODO: Remove when we drop Django 2.x support.
'django.db.backends.postgresql_psycopg2',
# GeoDjango
'django.contrib.gis.db.backends.spatialite',
@ -20,8 +18,6 @@ SUPPORTED_DATABASE_ENGINES = {
# django-transaction-hooks
'transaction_hooks.backends.sqlite3',
'transaction_hooks.backends.postgis',
# TODO: Remove when we drop Django 2.x support.
'transaction_hooks.backends.postgresql_psycopg2',
'transaction_hooks.backends.mysql',
# django-prometheus wrapped engines

View file

@ -2,7 +2,7 @@ from django import VERSION as DJANGO_VERSION
from django.conf import settings
from django.contrib.postgres.fields import (
ArrayField, HStoreField, IntegerRangeField,
DateRangeField, DateTimeRangeField)
DateRangeField, DateTimeRangeField, DecimalRangeField)
from django.contrib.postgres.operations import (
HStoreExtension, UnaccentExtension)
from django.db import models, migrations
@ -21,19 +21,6 @@ def extra_regular_available_fields():
def extra_postgres_available_fields():
fields = []
try:
# TODO Remove when Dj31 support is dropped
from django.contrib.postgres.fields import FloatRangeField
fields.append(('float_range', FloatRangeField(null=True, blank=True)))
except ImportError:
pass
try:
# TODO Add to module import when Dj31 is dropped
from django.contrib.postgres.fields import DecimalRangeField
fields.append(('decimal_range', DecimalRangeField(null=True, blank=True)))
except ImportError:
pass
# Future proofing with Django 40 deprecation
if DJANGO_VERSION[0] < 4:
@ -103,6 +90,7 @@ class Migration(migrations.Migration):
('int_range', IntegerRangeField(null=True, blank=True)),
('date_range', DateRangeField(null=True, blank=True)),
('datetime_range', DateTimeRangeField(null=True, blank=True)),
('decimal_range', DecimalRangeField(null=True, blank=True))
] + extra_postgres_available_fields(),
),
migrations.RunSQL('CREATE TABLE cachalot_unmanagedmodel '

View file

@ -58,7 +58,7 @@ class PostgresModel(Model):
null=True, blank=True)
hstore = HStoreField(null=True, blank=True)
if DJANGO_VERSION[0] < 4:
if DJANGO_VERSION < (4, 0):
from django.contrib.postgres.fields import JSONField
json = JSONField(null=True, blank=True)

View file

@ -1,6 +1,5 @@
from unittest import skipIf
from django import VERSION as DJANGO_VERSION
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS, connections, transaction
from django.test import TransactionTestCase
@ -27,24 +26,6 @@ class MultiDatabaseTestCase(TransactionTestCase):
# will execute an extra SQL request below.
connection2.cursor()
def is_django_21_below_and_sqlite2(self):
"""
Note: See test_utils.py with this function name
Checks if Django 2.1 or below and SQLite2
"""
django_version = DJANGO_VERSION
if not self.is_sqlite2:
# Immediately know if SQLite
return False
if django_version[0] < 2:
# Takes Django 0 and 1 out of the picture
return True
else:
if django_version[0] == 2 and django_version[1] < 2:
# Takes Django 2.0-2.1 out
return True
return False
def test_read(self):
with self.assertNumQueries(1):
data1 = list(Test.objects.all())
@ -66,8 +47,7 @@ class MultiDatabaseTestCase(TransactionTestCase):
data1 = list(Test.objects.using(self.db_alias2))
self.assertListEqual(data1, [])
with self.assertNumQueries(2 if self.is_django_21_below_and_sqlite2() else 1,
using=self.db_alias2):
with self.assertNumQueries(1, using=self.db_alias2):
t3 = Test.objects.using(self.db_alias2).create(name='test3')
with self.assertNumQueries(1, using=self.db_alias2):
@ -82,8 +62,7 @@ class MultiDatabaseTestCase(TransactionTestCase):
data1 = list(Test.objects.all())
self.assertListEqual(data1, [self.t1, self.t2])
with self.assertNumQueries(2 if self.is_django_21_below_and_sqlite2() else 1,
using=self.db_alias2):
with self.assertNumQueries(1, using=self.db_alias2):
Test.objects.using(self.db_alias2).create(name='test3')
with self.assertNumQueries(0):

View file

@ -3,7 +3,7 @@ from unittest import skipIf
from uuid import UUID
from decimal import Decimal
from django import VERSION as django_version
from django import VERSION as DJANGO_VERSION
from django.conf import settings
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
@ -21,7 +21,7 @@ from cachalot.cache import cachalot_caches
from ..settings import cachalot_settings
from ..utils import UncachableQuery
from .models import Test, TestChild, TestParent, UnmanagedModel
from .test_utils import TestUtilsMixin
from .test_utils import TestUtilsMixin, FilteredTransactionTestCase
from .tests_decorators import all_final_sql_checks, with_final_sql_check, no_final_sql_check
@ -36,7 +36,7 @@ def is_field_available(name):
return name in fields
class ReadTestCase(TestUtilsMixin, TransactionTestCase):
class ReadTestCase(TestUtilsMixin, FilteredTransactionTestCase):
"""
Tests if every SQL request that only reads data is cached.
@ -353,7 +353,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
@all_final_sql_checks
def test_subquery(self):
additional_tables = []
if django_version[0] == 4 and django_version[1] < 1 and settings.CACHALOT_FINAL_SQL_CHECK:
if DJANGO_VERSION >= (4, 0) and DJANGO_VERSION < (4, 1) and settings.CACHALOT_FINAL_SQL_CHECK:
# with Django 4.0 comes some query optimalizations that do selects little differently.
additional_tables.append('django_content_type')
qs = Test.objects.filter(owner__in=User.objects.all())
@ -694,7 +694,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.assert_query_cached(qs)
with self.assertRaisesMessage(
AssertionError if django_version[0] < 4 else TypeError,
AssertionError if DJANGO_VERSION < (4, 0) else TypeError,
'Cannot combine queries on two different base models.'
):
Test.objects.all() | Permission.objects.all()
@ -739,7 +739,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.assert_query_cached(qs)
with self.assertRaisesMessage(
AssertionError if django_version[0] < 4 else TypeError,
AssertionError if DJANGO_VERSION < (4, 0) else TypeError,
'Cannot combine queries on two different base models.'):
Test.objects.all() & Permission.objects.all()
@ -816,21 +816,21 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
with self.assertRaises(TransactionManagementError):
list(Test.objects.select_for_update())
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
data1 = list(Test.objects.select_for_update())
self.assertListEqual(data1, [self.t1, self.t2])
self.assertListEqual([t.name for t in data1],
['test1', 'test2'])
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
data2 = list(Test.objects.select_for_update())
self.assertListEqual(data2, [self.t1, self.t2])
self.assertListEqual([t.name for t in data2],
['test1', 'test2'])
with self.assertNumQueries(2):
with self.assertNumQueries(4 if DJANGO_VERSION >= (4, 2) else 2):
with transaction.atomic():
data3 = list(Test.objects.select_for_update())
data4 = list(Test.objects.select_for_update())
@ -896,7 +896,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.assert_query_cached(qs, [self.t2, self.t1])
def test_table_inheritance(self):
with self.assertNumQueries(3 if self.is_sqlite else 2):
with self.assertNumQueries(2):
t_child = TestChild.objects.create(name='test_child')
with self.assertNumQueries(1):
@ -911,15 +911,10 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
expected = (r'\d+ 0 0 SCAN cachalot_test\n'
r'\d+ 0 0 USE TEMP B-TREE FOR ORDER BY')
elif self.is_mysql:
if self.django_version < (3, 1):
expected = (
r'1 SIMPLE cachalot_test '
r'(?:None )?ALL None None None None 2 100\.0 Using filesort')
else:
expected = (
r'-> Sort row IDs: cachalot_test.`name` \(cost=[\d\.]+ rows=\d\)\n '
r'-> Table scan on cachalot_test \(cost=[\d\.]+ rows=\d\)\n'
)
expected = (
r'-> Sort row IDs: cachalot_test.`name` \(cost=[\d\.]+ rows=\d\)\n '
r'-> Table scan on cachalot_test \(cost=[\d\.]+ rows=\d\)\n'
)
else:
explain_kwargs.update(
analyze=True,
@ -935,9 +930,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
r'Planning Time: [\d\.]+ ms\n'
r'Execution Time: [\d\.]+ ms$') % (operation_detail,
operation_detail)
with self.assertNumQueries(
2 if self.is_mysql and django_version[0] < 3
else 1):
with self.assertNumQueries(1):
explanation1 = Test.objects.explain(**explain_kwargs)
self.assertRegex(explanation1, expected)
with self.assertNumQueries(0):

View file

@ -1,6 +1,8 @@
from django import VERSION as DJANGO_VERSION
from django.core.management.color import no_style
from django.db import connection, transaction
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
from django.test import TransactionTestCase
from django.test.utils import CaptureQueriesContext
from ..utils import _get_tables
from .models import PostgresModel
@ -11,7 +13,6 @@ class TestUtilsMixin:
self.is_sqlite = connection.vendor == 'sqlite'
self.is_mysql = connection.vendor == 'mysql'
self.is_postgresql = connection.vendor == 'postgresql'
self.django_version = DJANGO_VERSION
self.force_reopen_connection()
# TODO: Remove this workaround when this issue is fixed:
@ -19,8 +20,6 @@ class TestUtilsMixin:
def tearDown(self):
if connection.vendor == 'postgresql':
flush_args = [no_style(), (PostgresModel._meta.db_table,),]
if float(".".join(map(str, DJANGO_VERSION[:2]))) < 3.1:
flush_args.append(())
flush_sql_list = connection.ops.sql_flush(*flush_args)
with transaction.atomic():
for sql in flush_sql_list:
@ -61,3 +60,61 @@ class TestUtilsMixin:
assert_function(data2, data1)
if result is not None:
assert_function(data2, result)
class FilteredTransactionTestCase(TransactionTestCase):
"""
TransactionTestCase with assertNumQueries that ignores BEGIN, COMMIT and ROLLBACK
queries.
"""
def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwargs):
conn = connections[using]
context = FilteredAssertNumQueriesContext(self, num, conn)
if func is None:
return context
with context:
func(*args, **kwargs)
class FilteredAssertNumQueriesContext(CaptureQueriesContext):
"""
Capture queries and assert their number ignoring BEGIN, COMMIT and ROLLBACK queries.
"""
EXCLUDE = ('BEGIN', 'COMMIT', 'ROLLBACK')
def __init__(self, test_case, num, connection):
self.test_case = test_case
self.num = num
super().__init__(connection)
def __exit__(self, exc_type, exc_value, traceback):
super().__exit__(exc_type, exc_value, traceback)
if exc_type is not None:
return
filtered_queries = []
excluded_queries = []
for q in self.captured_queries:
if q['sql'].upper() not in self.EXCLUDE:
filtered_queries.append(q)
else:
excluded_queries.append(q)
executed = len(filtered_queries)
self.test_case.assertEqual(
executed,
self.num,
f"\n{executed} queries executed, {self.num} expected\n" +
"\nCaptured queries were:\n" +
"".join(
f"{i}. {query['sql']}\n"
for i, query in enumerate(filtered_queries, start=1)
) +
"\nCaptured queries, that were excluded:\n" +
"".join(
f"{i}. {query['sql']}\n"
for i, query in enumerate(excluded_queries, start=1)
)
)

View file

@ -1,5 +1,6 @@
from threading import Thread
from django import VERSION as DJANGO_VERSION
from django.db import connection, transaction
from django.test import TransactionTestCase, skipUnlessDBFeature
@ -29,7 +30,7 @@ class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase):
self.assertEqual(t2, t)
def test_concurrent_caching_during_atomic(self):
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
t1 = TestThread().start_and_join()
t = Test.objects.create(name='test')
@ -45,7 +46,7 @@ class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase):
def test_concurrent_caching_before_and_during_atomic_1(self):
t1 = TestThread().start_and_join()
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
t2 = TestThread().start_and_join()
t = Test.objects.create(name='test')
@ -60,7 +61,7 @@ class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase):
def test_concurrent_caching_before_and_during_atomic_2(self):
t1 = TestThread().start_and_join()
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
t = Test.objects.create(name='test')
t2 = TestThread().start_and_join()
@ -73,7 +74,7 @@ class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase):
self.assertEqual(data, t)
def test_concurrent_caching_during_and_after_atomic_1(self):
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
t1 = TestThread().start_and_join()
t = Test.objects.create(name='test')
@ -88,7 +89,7 @@ class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase):
self.assertEqual(data, t)
def test_concurrent_caching_during_and_after_atomic_2(self):
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
t = Test.objects.create(name='test')
t1 = TestThread().start_and_join()
@ -103,7 +104,7 @@ class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase):
self.assertEqual(data, t)
def test_concurrent_caching_during_and_after_atomic_3(self):
with self.assertNumQueries(1):
with self.assertNumQueries(3 if DJANGO_VERSION >= (4, 2) else 1):
with transaction.atomic():
t1 = TestThread().start_and_join()
t = Test.objects.create(name='test')

View file

@ -1,16 +1,17 @@
from cachalot.transaction import AtomicCache
from django.contrib.auth.models import User
from django.core.cache import cache
from django.db import transaction, connection, IntegrityError
from django.test import SimpleTestCase, TransactionTestCase, skipUnlessDBFeature
from django.test import SimpleTestCase, skipUnlessDBFeature
from .models import Test
from .test_utils import TestUtilsMixin
from .test_utils import TestUtilsMixin, FilteredTransactionTestCase
class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
class AtomicTestCase(TestUtilsMixin, FilteredTransactionTestCase):
def test_successful_read_atomic(self):
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
with transaction.atomic():
data1 = list(Test.objects.all())
self.assertListEqual(data1, [])
@ -20,7 +21,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
self.assertListEqual(data2, [])
def test_unsuccessful_read_atomic(self):
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
try:
with transaction.atomic():
data1 = list(Test.objects.all())
@ -38,21 +39,21 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
data1 = list(Test.objects.all())
self.assertListEqual(data1, [])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
with transaction.atomic():
t1 = Test.objects.create(name='test1')
with self.assertNumQueries(1):
data2 = list(Test.objects.all())
self.assertListEqual(data2, [t1])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
with transaction.atomic():
t2 = Test.objects.create(name='test2')
with self.assertNumQueries(1):
data3 = list(Test.objects.all())
self.assertListEqual(data3, [t1, t2])
with self.assertNumQueries(4 if self.is_sqlite else 3):
with self.assertNumQueries(3):
with transaction.atomic():
data4 = list(Test.objects.all())
t3 = Test.objects.create(name='test3')
@ -67,7 +68,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
data1 = list(Test.objects.all())
self.assertListEqual(data1, [])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
try:
with transaction.atomic():
Test.objects.create(name='test')
@ -82,7 +83,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
Test.objects.get(name='test')
def test_cache_inside_atomic(self):
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
with transaction.atomic():
data1 = list(Test.objects.all())
data2 = list(Test.objects.all())
@ -90,7 +91,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
self.assertListEqual(data2, [])
def test_invalidation_inside_atomic(self):
with self.assertNumQueries(4 if self.is_sqlite else 3):
with self.assertNumQueries(3):
with transaction.atomic():
data1 = list(Test.objects.all())
t = Test.objects.create(name='test')
@ -99,7 +100,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
self.assertListEqual(data2, [t])
def test_successful_nested_read_atomic(self):
with self.assertNumQueries(7 if self.is_sqlite else 6):
with self.assertNumQueries(6):
with transaction.atomic():
list(Test.objects.all())
with transaction.atomic():
@ -114,7 +115,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
list(User.objects.all())
def test_unsuccessful_nested_read_atomic(self):
with self.assertNumQueries(6 if self.is_sqlite else 5):
with self.assertNumQueries(5):
with transaction.atomic():
try:
with transaction.atomic():
@ -127,7 +128,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
list(Test.objects.all())
def test_successful_nested_write_atomic(self):
with self.assertNumQueries(13 if self.is_sqlite else 12):
with self.assertNumQueries(12):
with transaction.atomic():
t1 = Test.objects.create(name='test1')
with transaction.atomic():
@ -144,7 +145,7 @@ class AtomicTestCase(TestUtilsMixin, TransactionTestCase):
self.assertListEqual(data3, [t1, t2, t3, t4])
def test_unsuccessful_nested_write_atomic(self):
with self.assertNumQueries(16 if self.is_sqlite else 15):
with self.assertNumQueries(15):
with transaction.atomic():
t1 = Test.objects.create(name='test1')
try:

View file

@ -1,6 +1,5 @@
from unittest import skipIf, skipUnless
from django import VERSION as DJANGO_VERSION
from django.contrib.auth.models import User, Permission, Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned
@ -12,10 +11,10 @@ from django.db.models.expressions import RawSQL
from django.test import TransactionTestCase, skipUnlessDBFeature
from .models import Test, TestParent, TestChild
from .test_utils import TestUtilsMixin
from .test_utils import TestUtilsMixin, FilteredTransactionTestCase
class WriteTestCase(TestUtilsMixin, TransactionTestCase):
class WriteTestCase(TestUtilsMixin, FilteredTransactionTestCase):
"""
Tests if every SQL request writing data is not cached and invalidates the
implied data.
@ -56,7 +55,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
data1 = list(Test.objects.all())
self.assertListEqual(data1, [])
with self.assertNumQueries(3 if self.is_sqlite else 2):
with self.assertNumQueries(2):
t, created = Test.objects.get_or_create(name='test')
self.assertTrue(created)
@ -78,14 +77,14 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
with self.assertNumQueries(1):
self.assertListEqual(list(Test.objects.all()), [])
with self.assertNumQueries(5 if self.is_sqlite else 4):
with self.assertNumQueries(4):
t, created = Test.objects.update_or_create(
name='test', defaults={'public': True})
self.assertTrue(created)
self.assertEqual(t.name, 'test')
self.assertEqual(t.public, True)
with self.assertNumQueries(3 if self.is_sqlite else 2):
with self.assertNumQueries(2):
t, created = Test.objects.update_or_create(
name='test', defaults={'public': False})
self.assertFalse(created)
@ -94,7 +93,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
# The number of SQL queries doesnt decrease because update_or_create
# always calls an UPDATE, even when data wasnt changed.
with self.assertNumQueries(3 if self.is_sqlite else 2):
with self.assertNumQueries(2):
t, created = Test.objects.update_or_create(
name='test', defaults={'public': False})
self.assertFalse(created)
@ -109,12 +108,12 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
data1 = list(Test.objects.all())
self.assertListEqual(data1, [])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
unsaved_tests = [Test(name='test%02d' % i) for i in range(1, 11)]
Test.objects.bulk_create(unsaved_tests)
self.assertEqual(Test.objects.count(), 10)
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
unsaved_tests = [Test(name='test%02d' % i) for i in range(1, 11)]
Test.objects.bulk_create(unsaved_tests)
self.assertEqual(Test.objects.count(), 20)
@ -160,12 +159,12 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
self.assertListEqual(data1, [t1.name, t2.name])
self.assertListEqual(data2, [t1.name])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
Test.objects.bulk_create([Test(name='test%s' % i)
for i in range(2, 11)])
with self.assertNumQueries(1):
self.assertEqual(Test.objects.count(), 10)
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
Test.objects.all().delete()
with self.assertNumQueries(1):
self.assertEqual(Test.objects.count(), 0)
@ -360,7 +359,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
self.assertListEqual(data4, [user1, user2])
self.assertListEqual([u.n for u in data4], [1, 0])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
Test.objects.bulk_create([
Test(name='test3', owner=user1),
Test(name='test4', owner=user2),
@ -588,7 +587,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
data2 = list(Test.objects.select_related('owner'))
self.assertListEqual(data2, [])
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
Test.objects.bulk_create([
Test(name='test1', owner=u1),
Test(name='test2', owner=u2),
@ -602,7 +601,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
self.assertEqual(data3[2].owner, u2)
self.assertEqual(data3[3].owner, u1)
with self.assertNumQueries(2 if self.is_sqlite else 1):
with self.assertNumQueries(1):
Test.objects.filter(name__in=['test1', 'test2']).delete()
with self.assertNumQueries(1):
data4 = list(Test.objects.select_related('owner'))
@ -634,12 +633,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
self.assertEqual(data3[0].owner, u)
self.assertListEqual(list(data3[0].owner.groups.all()), [])
with self.assertNumQueries(
8 if self.is_sqlite and DJANGO_VERSION[0] == 2 and DJANGO_VERSION[1] == 2
else 4 if self.is_postgresql and DJANGO_VERSION[0] > 2
else 4 if self.is_mysql and DJANGO_VERSION[0] > 2
else 6
):
with self.assertNumQueries(4):
group = Group.objects.create(name='test_group')
permissions = list(Permission.objects.all()[:5])
group.permissions.add(*permissions)
@ -852,7 +846,7 @@ class WriteTestCase(TestUtilsMixin, TransactionTestCase):
with self.assertRaises(TestChild.DoesNotExist):
TestChild.objects.get()
with self.assertNumQueries(3 if self.is_sqlite else 2):
with self.assertNumQueries(2):
t_child = TestChild.objects.create(name='test_child')
with self.assertNumQueries(1):

View file

@ -35,13 +35,6 @@ CACHABLE_PARAM_TYPES = {
}
UNCACHABLE_FUNCS = {Now, TransactionNow}
try:
# TODO Drop after Dj30 drop
from django.contrib.postgres.fields.jsonb import JsonAdapter
CACHABLE_PARAM_TYPES.update((JsonAdapter,))
except ImportError:
pass
try:
from psycopg2 import Binary
from psycopg2.extras import (
@ -131,13 +124,7 @@ def _find_rhs_lhs_subquery(side):
elif h_class is QuerySet:
return side.query
elif h_class in (Subquery, Exists): # Subquery allows QuerySet & Query
try:
return side.query.query if side.query.__class__ is QuerySet else side.query
except AttributeError: # TODO Remove try/except closure after drop Django 2.2
try:
return side.queryset.query
except AttributeError:
return None
return side.query.query if side.query.__class__ is QuerySet else side.query
elif h_class in UNCACHABLE_FUNCS:
raise UncachableQuery

View file

@ -11,8 +11,8 @@ Caches your Django ORM queries and automatically invalidates them.
.. image:: http://img.shields.io/pypi/v/django-cachalot.svg?style=flat-square&maxAge=3600
:target: https://pypi.python.org/pypi/django-cachalot
.. image:: http://img.shields.io/travis/noripyt/django-cachalot/master.svg?style=flat-square&maxAge=3600
:target: https://travis-ci.org/noripyt/django-cachalot
.. image:: https://github.com/noripyt/django-cachalot/actions/workflows/ci.yml/badge.svg
:target: https://github.com/noripyt/django-cachalot/actions/workflows/ci.yml
.. image:: http://img.shields.io/coveralls/noripyt/django-cachalot/master.svg?style=flat-square&maxAge=3600
:target: https://coveralls.io/r/noripyt/django-cachalot?branch=master

View file

@ -4,8 +4,8 @@ Quick start
Requirements
............
- Django 2.2, 3.2, 4.0-4.1
- Python 3.7-3.10
- Django 3.2, 4.1, 4.2
- Python 3.7-3.11
- a cache configured as ``'default'`` with one of these backends:
- `django-redis <https://github.com/niwinz/django-redis>`_

View file

@ -1 +1 @@
Django>=2.2,<4.2
Django>=3.2,<4.3

View file

@ -2,7 +2,8 @@
django>=2
psycopg2-binary
psycopg2
psycopg
mysqlclient
django-redis
python-memcached

View file

@ -25,15 +25,15 @@ setup(
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Programming Language :: Python :: 3',
'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',
'Topic :: Internet :: WWW/HTTP',
],
license='BSD',

16
tox.ini
View file

@ -1,10 +1,9 @@
[tox]
envlist =
py{37,38,39}-django2.2-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
py{37,38,39,310}-django3.2-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
py{38,39,310}-django4.0-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
py{38,39,310}-django4.1-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
py{38,39,310}-djangomain-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
py{38,39,310,311}-django4.2-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
py{38,39,310,311}-djangomain-{sqlite3,postgresql,mysql}-{redis,memcached,pylibmc,locmem,filebased},
[testenv]
passenv = *
@ -13,13 +12,14 @@ basepython =
py38: python3.8
py39: python3.9
py310: python3.10
py311: python3.11
deps =
django2.2: Django>=2.2,<2.3
django3.2: Django>=3.2,<4.0
django4.0: Django>=4.0,<4.1
django4.1: Django>=4.1,<4.2
django4.2: Django>=4.2,<4.3
djangomain: https://github.com/django/django/archive/main.tar.gz
psycopg2-binary>=2.8,<2.9
psycopg2>=2.9.5,<3.0
psycopg
mysqlclient
django-redis
python-memcached
@ -48,11 +48,11 @@ python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311
[gh-actions:env]
DJANGO =
2.2: django2.2
3.2: django3.2
4.0: django4.0
4.1: django4.1
4.2: django4.2
main: djangomain