Compare commits

...

15 commits

Author SHA1 Message Date
Benedikt Willi
6ec032e211 Merge branch 'andrew-wang/dj4.2' 2023-06-06 12:07:08 +02:00
Benedikt Willi
2db38ec463 Added more filtered tests 2023-06-06 12:06:53 +02:00
Benedikt Willi
fb5f28aaf0 Merge branch 'andrew-wang/dj4.2' 2023-06-06 11:43:00 +02:00
Benedikt Willi
fa1fbc1a5d Added a FilteredTransactionTestCase, updated tests. 2023-06-06 10:05:44 +02:00
Jack Linke
beff1e4050 Fix simple error 2023-05-25 10:16:10 +02:00
Jack Linke
27eaaa57cc
Correct problems with tests (#234) 2023-05-24 21:35:03 -04:00
Conrad
de63580ae6
Update limits.rst (#230) 2023-04-28 17:31:15 -04:00
Andrew-Chen-Wang
b2d9de2997 Fix CI supported versions 2023-04-04 15:30:23 -04:00
Andrew-Chen-Wang
1d0a06a9ab Remove Python 2 and Django 2.2 code 2023-04-04 15:28:15 -04:00
Andrew Chen Wang
0bda00bd2c
Merge branch 'master' into andrew-wang/dj4.2 2023-04-04 15:18:08 -04:00
Andrew-Chen-Wang
d192bae22c Add Django 4.1-4.2 and Python 3.11 support (Fixes #228)
* Increment version to 4.5.0
2023-04-04 15:14:29 -04:00
Andrew Chen Wang
c955d1bbee
Update CHANGELOG.rst 2023-03-13 05:01:47 -04:00
Andrew Chen Wang
d60834910c
Update version 2023-03-13 05:00:39 -04:00
Petr Dlouhý
03f675c96f
add test that will cause error #226 (#227)
* Update utils.py

verify get_meta isn't none before requesting db_table

* Add passenv to tox.ini

* Fix test_explain in sqlite

* add test that will cause error #226

* try to get around problem with PASSWORD in GitHub actions testing

* fix tests broken not counting with other applications permissions

---------

Co-authored-by: hho6643 <63743025+hho6643@users.noreply.github.com>
Co-authored-by: Andrew Chen Wang <60190294+Andrew-Chen-Wang@users.noreply.github.com>
2023-03-12 16:39:50 -04:00
David Haas
866b662273
Update utils.py (#225)
* Update utils.py

verify get_meta isn't none before requesting db_table

* Add passenv to tox.ini

* Fix test_explain in sqlite

---------

Co-authored-by: Andrew Chen Wang <60190294+Andrew-Chen-Wang@users.noreply.github.com>
2023-03-12 16:39:04 -04:00
32 changed files with 299 additions and 186 deletions

View file

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

1
.gitignore vendored
View file

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

View file

@ -1,8 +1,23 @@
Whats new in django-cachalot? 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 2.5.2
----- -----
- Added Django 4.1 support (#217) - Added Django 4.1 support (#217)
2.5.1 2.5.1

View file

@ -39,7 +39,7 @@ Table of Contents:
Quickstart 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. 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, 2) VERSION = (2, 6, 0)
__version__ = ".".join(map(str, VERSION)) __version__ = ".".join(map(str, VERSION))
try: default_app_config = "cachalot.apps.CachalotConfig"
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"

View file

View file

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import TestModel
@admin.register(TestModel)
class TestModelAdmin(admin.ModelAdmin):
list_display = ('name', 'owner')

View file

@ -0,0 +1,52 @@
# Generated by Django 4.1.7 on 2023-03-10 19:33
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.db.models.functions.text
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="TestModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=20)),
(
"owner",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ("name",),
},
),
migrations.AddConstraint(
model_name="testmodel",
constraint=models.UniqueConstraint(
fields=["name"],
condition=models.Q(owner=None),
name="unique_name",
),
),
]

View file

@ -0,0 +1,18 @@
from django.conf import settings
from django.db.models import Q, UniqueConstraint, Model, CharField, ForeignKey, SET_NULL
class TestModel(Model):
name = CharField(max_length=20)
owner = ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
on_delete=SET_NULL)
class Meta:
ordering = ('name',)
constraints = [
UniqueConstraint(
fields=["name"],
condition=Q(owner=None),
name="unique_name",
)
]

View file

@ -0,0 +1,19 @@
from django.test import TestCase
from django.contrib.auth.models import User
from .models import TestModel
from django.test import Client
class AdminTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create(username='admin', is_staff=True, is_superuser=True)
def test_save_test_model(self):
"""
Model 'TestModel' has UniqueConstraint which caused problems when saving TestModelAdmin in Django >= 4.1
"""
self.client.force_login(self.user)
response = self.client.post('/admin/admin_tests/testmodel/add/', {'name': 'test', 'public': True})
self.assertEqual(response.status_code, 302)
self.assertEqual(TestModel.objects.count(), 1)

View file

@ -1,4 +1,5 @@
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Optional, Tuple, Union
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
@ -45,7 +46,11 @@ def _get_tables(tables_or_models):
else table_or_model._meta.db_table) 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 Clears what was cached by django-cachalot implying one or more SQL tables
or models from ``tables_or_models``. or models from ``tables_or_models``.
@ -62,19 +67,9 @@ def invalidate(*tables_or_models, **kwargs):
(or a combination) (or a combination)
:type tables_or_models: tuple of strings or models :type tables_or_models: tuple of strings or models
:arg cache_alias: Alias from the Django ``CACHES`` setting :arg cache_alias: Alias from the Django ``CACHES`` setting
:type cache_alias: string or NoneType
:arg db_alias: Alias from the Django ``DATABASES`` setting :arg db_alias: Alias from the Django ``DATABASES`` setting
:type db_alias: string or NoneType
:returns: Nothing :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 send_signal = False
invalidated = set() invalidated = set()
for cache_alias, db_alias, tables in _cache_db_tables_iterator( 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) 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 Returns the timestamp of the most recent invalidation of the given
``tables_or_models``. If ``tables_or_models`` is not specified, ``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) (or a combination)
:type tables_or_models: tuple of strings or models :type tables_or_models: tuple of strings or models
:arg cache_alias: Alias from the Django ``CACHES`` setting :arg cache_alias: Alias from the Django ``CACHES`` setting
:type cache_alias: string or NoneType
:arg db_alias: Alias from the Django ``DATABASES`` setting :arg db_alias: Alias from the Django ``DATABASES`` setting
:type db_alias: string or NoneType
:returns: The timestamp of the most recent invalidation :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 last_invalidation = 0.0
for cache_alias, db_alias, tables in _cache_db_tables_iterator( for cache_alias, db_alias, tables in _cache_db_tables_iterator(
list(_get_tables(tables_or_models)), cache_alias, db_alias): list(_get_tables(tables_or_models)), cache_alias, db_alias):
@ -134,7 +123,7 @@ def get_last_invalidation(*tables_or_models, **kwargs):
@contextmanager @contextmanager
def cachalot_disabled(all_queries=False): def cachalot_disabled(all_queries: bool = False):
""" """
Context manager for temporarily disabling cachalot. Context manager for temporarily disabling cachalot.
If you evaluate the same queryset a second time, If you evaluate the same queryset a second time,
@ -158,7 +147,6 @@ def cachalot_disabled(all_queries=False):
the original and duplicate query. the original and duplicate query.
:arg all_queries: Any query, including already evaluated queries, are re-evaluated. :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) was_enabled = getattr(LOCAL_STORAGE, "cachalot_enabled", cachalot_settings.CACHALOT_ENABLED)
LOCAL_STORAGE.cachalot_enabled = False LOCAL_STORAGE.cachalot_enabled = False

View file

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

View file

@ -2,7 +2,7 @@ from django import VERSION as DJANGO_VERSION
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields import ( from django.contrib.postgres.fields import (
ArrayField, HStoreField, IntegerRangeField, ArrayField, HStoreField, IntegerRangeField,
DateRangeField, DateTimeRangeField) DateRangeField, DateTimeRangeField, DecimalRangeField)
from django.contrib.postgres.operations import ( from django.contrib.postgres.operations import (
HStoreExtension, UnaccentExtension) HStoreExtension, UnaccentExtension)
from django.db import models, migrations from django.db import models, migrations
@ -21,19 +21,6 @@ def extra_regular_available_fields():
def extra_postgres_available_fields(): def extra_postgres_available_fields():
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 # Future proofing with Django 40 deprecation
if DJANGO_VERSION[0] < 4: if DJANGO_VERSION[0] < 4:
@ -103,6 +90,7 @@ class Migration(migrations.Migration):
('int_range', IntegerRangeField(null=True, blank=True)), ('int_range', IntegerRangeField(null=True, blank=True)),
('date_range', DateRangeField(null=True, blank=True)), ('date_range', DateRangeField(null=True, blank=True)),
('datetime_range', DateTimeRangeField(null=True, blank=True)), ('datetime_range', DateTimeRangeField(null=True, blank=True)),
('decimal_range', DecimalRangeField(null=True, blank=True))
] + extra_postgres_available_fields(), ] + extra_postgres_available_fields(),
), ),
migrations.RunSQL('CREATE TABLE cachalot_unmanagedmodel ' migrations.RunSQL('CREATE TABLE cachalot_unmanagedmodel '

View file

@ -58,7 +58,7 @@ class PostgresModel(Model):
null=True, blank=True) null=True, blank=True)
hstore = HStoreField(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 from django.contrib.postgres.fields import JSONField
json = JSONField(null=True, blank=True) json = JSONField(null=True, blank=True)

View file

@ -1,6 +1,5 @@
from unittest import skipIf from unittest import skipIf
from django import VERSION as DJANGO_VERSION
from django.conf import settings from django.conf import settings
from django.db import DEFAULT_DB_ALIAS, connections, transaction from django.db import DEFAULT_DB_ALIAS, connections, transaction
from django.test import TransactionTestCase from django.test import TransactionTestCase
@ -27,24 +26,6 @@ class MultiDatabaseTestCase(TransactionTestCase):
# will execute an extra SQL request below. # will execute an extra SQL request below.
connection2.cursor() 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): def test_read(self):
with self.assertNumQueries(1): with self.assertNumQueries(1):
data1 = list(Test.objects.all()) data1 = list(Test.objects.all())
@ -66,8 +47,7 @@ class MultiDatabaseTestCase(TransactionTestCase):
data1 = list(Test.objects.using(self.db_alias2)) data1 = list(Test.objects.using(self.db_alias2))
self.assertListEqual(data1, []) self.assertListEqual(data1, [])
with self.assertNumQueries(2 if self.is_django_21_below_and_sqlite2() else 1, with self.assertNumQueries(1, using=self.db_alias2):
using=self.db_alias2):
t3 = Test.objects.using(self.db_alias2).create(name='test3') t3 = Test.objects.using(self.db_alias2).create(name='test3')
with self.assertNumQueries(1, using=self.db_alias2): with self.assertNumQueries(1, using=self.db_alias2):
@ -82,8 +62,7 @@ class MultiDatabaseTestCase(TransactionTestCase):
data1 = list(Test.objects.all()) data1 = list(Test.objects.all())
self.assertListEqual(data1, [self.t1, self.t2]) self.assertListEqual(data1, [self.t1, self.t2])
with self.assertNumQueries(2 if self.is_django_21_below_and_sqlite2() else 1, with self.assertNumQueries(1, using=self.db_alias2):
using=self.db_alias2):
Test.objects.using(self.db_alias2).create(name='test3') Test.objects.using(self.db_alias2).create(name='test3')
with self.assertNumQueries(0): with self.assertNumQueries(0):

View file

@ -3,7 +3,7 @@ from unittest import skipIf
from uuid import UUID from uuid import UUID
from decimal import Decimal 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.conf import settings
from django.contrib.auth.models import Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -21,7 +21,7 @@ from cachalot.cache import cachalot_caches
from ..settings import cachalot_settings from ..settings import cachalot_settings
from ..utils import UncachableQuery from ..utils import UncachableQuery
from .models import Test, TestChild, TestParent, UnmanagedModel 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 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 return name in fields
class ReadTestCase(TestUtilsMixin, TransactionTestCase): class ReadTestCase(TestUtilsMixin, FilteredTransactionTestCase):
""" """
Tests if every SQL request that only reads data is cached. Tests if every SQL request that only reads data is cached.
@ -52,12 +52,13 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.group__permissions = list(Permission.objects.all()[:3]) self.group__permissions = list(Permission.objects.all()[:3])
self.group.permissions.add(*self.group__permissions) self.group.permissions.add(*self.group__permissions)
self.user = User.objects.create_user('user') self.user = User.objects.create_user('user')
self.user__permissions = list(Permission.objects.all()[3:6]) self.user__permissions = list(Permission.objects.filter(content_type__app_label='auth')[3:6])
self.user.groups.add(self.group) self.user.groups.add(self.group)
self.user.user_permissions.add(*self.user__permissions) self.user.user_permissions.add(*self.user__permissions)
self.admin = User.objects.create_superuser('admin', 'admin@test.me', self.admin = User.objects.create_superuser('admin', 'admin@test.me',
'password') 'password')
self.t1__permission = (Permission.objects.order_by('?') self.t1__permission = (Permission.objects
.order_by('?')
.select_related('content_type')[0]) .select_related('content_type')[0])
self.t1 = Test.objects.create( self.t1 = Test.objects.create(
name='test1', owner=self.user, name='test1', owner=self.user,
@ -352,7 +353,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
@all_final_sql_checks @all_final_sql_checks
def test_subquery(self): def test_subquery(self):
additional_tables = [] 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. # with Django 4.0 comes some query optimalizations that do selects little differently.
additional_tables.append('django_content_type') additional_tables.append('django_content_type')
qs = Test.objects.filter(owner__in=User.objects.all()) qs = Test.objects.filter(owner__in=User.objects.all())
@ -693,7 +694,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.assert_query_cached(qs) self.assert_query_cached(qs)
with self.assertRaisesMessage( 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.' 'Cannot combine queries on two different base models.'
): ):
Test.objects.all() | Permission.objects.all() Test.objects.all() | Permission.objects.all()
@ -738,7 +739,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.assert_query_cached(qs) self.assert_query_cached(qs)
with self.assertRaisesMessage( 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.'): 'Cannot combine queries on two different base models.'):
Test.objects.all() & Permission.objects.all() Test.objects.all() & Permission.objects.all()
@ -895,7 +896,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
self.assert_query_cached(qs, [self.t2, self.t1]) self.assert_query_cached(qs, [self.t2, self.t1])
def test_table_inheritance(self): 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') t_child = TestChild.objects.create(name='test_child')
with self.assertNumQueries(1): with self.assertNumQueries(1):
@ -907,18 +908,13 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
def test_explain(self): def test_explain(self):
explain_kwargs = {} explain_kwargs = {}
if self.is_sqlite: if self.is_sqlite:
expected = (r'\d+ 0 0 SCAN TABLE cachalot_test\n' expected = (r'\d+ 0 0 SCAN cachalot_test\n'
r'\d+ 0 0 USE TEMP B-TREE FOR ORDER BY') r'\d+ 0 0 USE TEMP B-TREE FOR ORDER BY')
elif self.is_mysql: elif self.is_mysql:
if self.django_version < (3, 1): expected = (
expected = ( r'-> Sort row IDs: cachalot_test.`name` \(cost=[\d\.]+ rows=\d\)\n '
r'1 SIMPLE cachalot_test ' r'-> Table scan on cachalot_test \(cost=[\d\.]+ rows=\d\)\n'
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'
)
else: else:
explain_kwargs.update( explain_kwargs.update(
analyze=True, analyze=True,
@ -934,9 +930,7 @@ class ReadTestCase(TestUtilsMixin, TransactionTestCase):
r'Planning Time: [\d\.]+ ms\n' r'Planning Time: [\d\.]+ ms\n'
r'Execution Time: [\d\.]+ ms$') % (operation_detail, r'Execution Time: [\d\.]+ ms$') % (operation_detail,
operation_detail) operation_detail)
with self.assertNumQueries( with self.assertNumQueries(1):
2 if self.is_mysql and django_version[0] < 3
else 1):
explanation1 = Test.objects.explain(**explain_kwargs) explanation1 = Test.objects.explain(**explain_kwargs)
self.assertRegex(explanation1, expected) self.assertRegex(explanation1, expected)
with self.assertNumQueries(0): 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.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 ..utils import _get_tables
from .models import PostgresModel from .models import PostgresModel
@ -11,7 +13,6 @@ class TestUtilsMixin:
self.is_sqlite = connection.vendor == 'sqlite' self.is_sqlite = connection.vendor == 'sqlite'
self.is_mysql = connection.vendor == 'mysql' self.is_mysql = connection.vendor == 'mysql'
self.is_postgresql = connection.vendor == 'postgresql' self.is_postgresql = connection.vendor == 'postgresql'
self.django_version = DJANGO_VERSION
self.force_reopen_connection() self.force_reopen_connection()
# TODO: Remove this workaround when this issue is fixed: # TODO: Remove this workaround when this issue is fixed:
@ -19,8 +20,6 @@ class TestUtilsMixin:
def tearDown(self): def tearDown(self):
if connection.vendor == 'postgresql': if connection.vendor == 'postgresql':
flush_args = [no_style(), (PostgresModel._meta.db_table,),] 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) flush_sql_list = connection.ops.sql_flush(*flush_args)
with transaction.atomic(): with transaction.atomic():
for sql in flush_sql_list: for sql in flush_sql_list:
@ -61,3 +60,61 @@ class TestUtilsMixin:
assert_function(data2, data1) assert_function(data2, data1)
if result is not None: if result is not None:
assert_function(data2, result) 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,10 +1,10 @@
from threading import Thread from threading import Thread
from django.db import connection, transaction from django.db import connection, transaction
from django.test import TransactionTestCase, skipUnlessDBFeature from django.test import skipUnlessDBFeature
from .models import Test from .models import Test
from .test_utils import TestUtilsMixin from .test_utils import TestUtilsMixin, FilteredTransactionTestCase
class TestThread(Thread): class TestThread(Thread):
@ -19,7 +19,7 @@ class TestThread(Thread):
@skipUnlessDBFeature('test_db_allows_multiple_connections') @skipUnlessDBFeature('test_db_allows_multiple_connections')
class ThreadSafetyTestCase(TestUtilsMixin, TransactionTestCase): class ThreadSafetyTestCase(TestUtilsMixin, FilteredTransactionTestCase):
def test_concurrent_caching(self): def test_concurrent_caching(self):
t1 = TestThread().start_and_join() t1 = TestThread().start_and_join()
t = Test.objects.create(name='test') t = Test.objects.create(name='test')

View file

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

View file

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

View file

@ -35,13 +35,6 @@ CACHABLE_PARAM_TYPES = {
} }
UNCACHABLE_FUNCS = {Now, TransactionNow} 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: try:
from psycopg2 import Binary from psycopg2 import Binary
from psycopg2.extras import ( from psycopg2.extras import (
@ -131,13 +124,7 @@ def _find_rhs_lhs_subquery(side):
elif h_class is QuerySet: elif h_class is QuerySet:
return side.query return side.query
elif h_class in (Subquery, Exists): # Subquery allows QuerySet & 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
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
elif h_class in UNCACHABLE_FUNCS: elif h_class in UNCACHABLE_FUNCS:
raise UncachableQuery raise UncachableQuery
@ -217,7 +204,8 @@ def _get_tables(db_alias, query, compiler=False):
# Gets all tables already found by the ORM. # Gets all tables already found by the ORM.
tables = set(query.table_map) tables = set(query.table_map)
tables.add(query.get_meta().db_table) if query.get_meta():
tables.add(query.get_meta().db_table)
# Gets tables in subquery annotations. # Gets tables in subquery annotations.
for annotation in query.annotations.values(): for annotation in query.annotations.values():

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 .. image:: http://img.shields.io/pypi/v/django-cachalot.svg?style=flat-square&maxAge=3600
:target: https://pypi.python.org/pypi/django-cachalot :target: https://pypi.python.org/pypi/django-cachalot
.. image:: http://img.shields.io/travis/noripyt/django-cachalot/master.svg?style=flat-square&maxAge=3600 .. image:: https://github.com/noripyt/django-cachalot/actions/workflows/ci.yml/badge.svg
:target: https://travis-ci.org/noripyt/django-cachalot :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 .. 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 :target: https://coveralls.io/r/noripyt/django-cachalot?branch=master

View file

@ -65,11 +65,9 @@ in a multi-processes project, if you use RQ or Celery for instance.
Filebased Filebased
......... .........
Filebased, a simple persistent cache implemented in Django, has a small bug Filebased, a simple persistent cache implemented in Django, had a small bug
(`#25501 <https://code.djangoproject.com/ticket/25501>`_): (`#25501 <https://code.djangoproject.com/ticket/25501>`_):
it cannot cache some objects, like psycopg2 ranges. it cannot cache some objects, like psycopg2 ranges. This bug was fixed in 2015, if you sill use an affected Django version and you use range fields from `django.contrib.postgres`, you need to add the tables using range fields
If you use range fields from `django.contrib.postgres` and your Django
version is affected by this bug, you need to add the tables using range fields
to :ref:`CACHALOT_UNCACHABLE_TABLES`. to :ref:`CACHALOT_UNCACHABLE_TABLES`.
.. _MySQL: .. _MySQL:

View file

@ -4,8 +4,8 @@ Quick start
Requirements Requirements
............ ............
- Django 2.2, 3.2, 4.0-4.1 - Django 3.2, 4.1, 4.2
- Python 3.7-3.10 - Python 3.7-3.11
- a cache configured as ``'default'`` with one of these backends: - a cache configured as ``'default'`` with one of these backends:
- `django-redis <https://github.com/niwinz/django-redis>`_ - `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 django>=2
psycopg2-binary psycopg2
psycopg
mysqlclient mysqlclient
django-redis django-redis
python-memcached python-memcached

View file

@ -9,6 +9,6 @@ if __name__ == '__main__':
django.setup() django.setup()
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner(verbosity=2, interactive=False) test_runner = DiscoverRunner(verbosity=2, interactive=False)
failures = test_runner.run_tests(['cachalot.tests']) failures = test_runner.run_tests(['cachalot.tests', 'cachalot.admin_tests'])
if failures: if failures:
sys.exit(failures) sys.exit(failures)

View file

@ -1,6 +1,7 @@
import debug_toolbar import debug_toolbar
from django.urls import re_path, include from django.urls import path, re_path, include
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib import admin
def empty_page(request): def empty_page(request):
@ -10,4 +11,5 @@ def empty_page(request):
urlpatterns = [ urlpatterns = [
re_path(r'^$', empty_page), re_path(r'^$', empty_page),
re_path(r'^__debug__/', include(debug_toolbar.urls)), re_path(r'^__debug__/', include(debug_toolbar.urls)),
path('admin/', admin.site.urls),
] ]

View file

@ -12,6 +12,7 @@ DATABASES = {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'cachalot', 'NAME': 'cachalot',
'USER': 'cachalot', 'USER': 'cachalot',
'PASSWORD': 'password',
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
}, },
'mysql': { 'mysql': {
@ -90,9 +91,13 @@ elif DEFAULT_CACHE_ALIAS == 'pylibmc':
INSTALLED_APPS = [ INSTALLED_APPS = [
'cachalot', 'cachalot',
'cachalot.admin_tests',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.postgres', # Enables the unaccent lookup. 'django.contrib.postgres', # Enables the unaccent lookup.
'django.contrib.sessions',
'django.contrib.admin',
'django.contrib.messages',
] ]
MIGRATION_MODULES = { MIGRATION_MODULES = {
@ -104,6 +109,12 @@ TEMPLATES = [
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
}
}, },
{ {
'BACKEND': 'django.template.backends.jinja2.Jinja2', 'BACKEND': 'django.template.backends.jinja2.Jinja2',
@ -116,7 +127,11 @@ TEMPLATES = [
} }
] ]
MIDDLEWARE = [] MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
SECRET_KEY = 'its not important in tests but we have to set it' SECRET_KEY = 'its not important in tests but we have to set it'

View file

@ -25,15 +25,15 @@ setup(
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.2', 'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1', 'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',
], ],
license='BSD', license='BSD',

17
tox.ini
View file

@ -1,24 +1,25 @@
[tox] [tox]
envlist = 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{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}-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] [testenv]
passenv = *
basepython = basepython =
py37: python3.7 py37: python3.7
py38: python3.8 py38: python3.8
py39: python3.9 py39: python3.9
py310: python3.10 py310: python3.10
py311: python3.11
deps = deps =
django2.2: Django>=2.2,<2.3
django3.2: Django>=3.2,<4.0 django3.2: Django>=3.2,<4.0
django4.0: Django>=4.0,<4.1
django4.1: Django>=4.1,<4.2 django4.1: Django>=4.1,<4.2
django4.2: Django>=4.2,<4.3
djangomain: https://github.com/django/django/archive/main.tar.gz djangomain: https://github.com/django/django/archive/main.tar.gz
psycopg2-binary>=2.8,<2.9 psycopg2>=2.9.5,<3.0
psycopg
mysqlclient mysqlclient
django-redis django-redis
python-memcached python-memcached
@ -47,11 +48,11 @@ python =
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310 3.10: py310
3.11: py311
[gh-actions:env] [gh-actions:env]
DJANGO = DJANGO =
2.2: django2.2
3.2: django3.2 3.2: django3.2
4.0: django4.0
4.1: django4.1 4.1: django4.1
4.2: django4.2
main: djangomain main: djangomain