Adds a Jinja2 extension.
This commit is contained in:
parent
62cb9c5c78
commit
04afa3f439
|
@ -0,0 +1,75 @@
|
|||
from django.core.cache import caches, DEFAULT_CACHE_ALIAS
|
||||
from django.core.cache.utils import make_template_fragment_key
|
||||
from jinja2.nodes import Keyword, Const, CallBlock
|
||||
from jinja2.ext import Extension
|
||||
|
||||
from .api import get_last_invalidation
|
||||
|
||||
|
||||
class CachalotExtension(Extension):
|
||||
tags = {'cache'}
|
||||
allowed_kwargs = ('cache_key', 'timeout', 'cache_alias')
|
||||
|
||||
def __init__(self, environment):
|
||||
super(CachalotExtension, self).__init__(environment)
|
||||
|
||||
self.environment.globals.update(
|
||||
get_last_invalidation=get_last_invalidation)
|
||||
|
||||
def parse_args(self, parser):
|
||||
args = []
|
||||
kwargs = []
|
||||
|
||||
stream = parser.stream
|
||||
|
||||
while stream.current.type != 'block_end':
|
||||
if stream.current.type == 'name' \
|
||||
and stream.look().type == 'assign':
|
||||
key = stream.current.value
|
||||
if key not in self.allowed_kwargs:
|
||||
parser.fail(
|
||||
"'%s' is not a valid keyword argument "
|
||||
"for {%% cache %%}" % key,
|
||||
stream.current.lineno)
|
||||
stream.skip(2)
|
||||
value = parser.parse_expression()
|
||||
kwargs.append(Keyword(key, value, lineno=value.lineno))
|
||||
else:
|
||||
args.append(parser.parse_expression())
|
||||
|
||||
if stream.current.type == 'block_end':
|
||||
break
|
||||
|
||||
parser.stream.expect('comma')
|
||||
|
||||
return args, kwargs
|
||||
|
||||
def parse(self, parser):
|
||||
tag = parser.stream.current.value
|
||||
lineno = next(parser.stream).lineno
|
||||
args, kwargs = self.parse_args(parser)
|
||||
default_cache_key = (None if parser.filename is None
|
||||
else '%s:%d' % (parser.filename, lineno))
|
||||
kwargs.append(Keyword('default_cache_key', Const(default_cache_key),
|
||||
lineno=lineno))
|
||||
body = parser.parse_statements(['name:end' + tag], drop_needle=True)
|
||||
|
||||
return CallBlock(self.call_method('cache', args, kwargs),
|
||||
[], [], body).set_lineno(lineno)
|
||||
|
||||
def cache(self, *args, **kwargs):
|
||||
cache_alias = kwargs.get('cache_alias', DEFAULT_CACHE_ALIAS)
|
||||
cache_key = kwargs.get('cache_key', kwargs['default_cache_key'])
|
||||
if cache_key is None:
|
||||
raise ValueError(
|
||||
'You must set `cache_key` when the template is not a file.')
|
||||
cache_key = make_template_fragment_key(cache_key, args)
|
||||
|
||||
out = caches[cache_alias].get(cache_key)
|
||||
if out is None:
|
||||
out = kwargs['caller']()
|
||||
caches[cache_alias].set(cache_key, out, kwargs.get('timeout'))
|
||||
return out
|
||||
|
||||
|
||||
ext = CachalotExtension
|
|
@ -6,11 +6,12 @@ from unittest import skipIf
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.cache import DEFAULT_CACHE_ALIAS
|
||||
from django.core.cache import DEFAULT_CACHE_ALIAS, caches
|
||||
from django.core.management import call_command
|
||||
from django.db import connection, transaction, DEFAULT_DB_ALIAS
|
||||
from django.template import Template, Context
|
||||
from django.template import engines, Context
|
||||
from django.test import TransactionTestCase
|
||||
from jinja2.exceptions import TemplateSyntaxError
|
||||
|
||||
from ..api import *
|
||||
from .models import Test
|
||||
|
@ -20,6 +21,8 @@ class APITestCase(TransactionTestCase):
|
|||
def setUp(self):
|
||||
self.t1 = Test.objects.create(name='test1')
|
||||
self.is_sqlite = connection.vendor == 'sqlite'
|
||||
self.cache_alias2 = next(alias for alias in settings.CACHES
|
||||
if alias != DEFAULT_CACHE_ALIAS)
|
||||
|
||||
def test_invalidate_tables(self):
|
||||
with self.assertNumQueries(1):
|
||||
|
@ -109,11 +112,33 @@ class APITestCase(TransactionTestCase):
|
|||
self.assertAlmostEqual(timestamp, time(), delta=0.1)
|
||||
|
||||
def test_get_last_invalidation_template_tag(self):
|
||||
original_timestamp = Template("{{ timestamp }}").render(Context({
|
||||
'timestamp': get_last_invalidation('auth.Group', 'cachalot_test')
|
||||
# Without arguments
|
||||
original_timestamp = engines['django'].from_string(
|
||||
"{{ timestamp }}"
|
||||
).render(Context({
|
||||
'timestamp': get_last_invalidation(),
|
||||
}))
|
||||
|
||||
template = Template("""
|
||||
template = engines['django'].from_string("""
|
||||
{% load cachalot %}
|
||||
{% get_last_invalidation as timestamp %}
|
||||
{{ timestamp }}
|
||||
""")
|
||||
timestamp = template.render(Context()).strip()
|
||||
|
||||
self.assertNotEqual(timestamp, '')
|
||||
self.assertNotEqual(timestamp, '0.0')
|
||||
self.assertAlmostEqual(float(timestamp), float(original_timestamp),
|
||||
delta=0.1)
|
||||
|
||||
# With arguments
|
||||
original_timestamp = engines['django'].from_string(
|
||||
"{{ timestamp }}"
|
||||
).render(Context({
|
||||
'timestamp': get_last_invalidation('auth.Group', 'cachalot_test'),
|
||||
}))
|
||||
|
||||
template = engines['django'].from_string("""
|
||||
{% load cachalot %}
|
||||
{% get_last_invalidation 'auth.Group' 'cachalot_test' as timestamp %}
|
||||
{{ timestamp }}
|
||||
|
@ -125,7 +150,8 @@ class APITestCase(TransactionTestCase):
|
|||
self.assertAlmostEqual(float(timestamp), float(original_timestamp),
|
||||
delta=0.1)
|
||||
|
||||
template = Template("""
|
||||
# While using the `cache` template tag, with invalidation
|
||||
template = engines['django'].from_string("""
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'auth.Group' 'cachalot_test' as timestamp %}
|
||||
{% cache 10 cache_key_name timestamp %}
|
||||
|
@ -140,6 +166,76 @@ class APITestCase(TransactionTestCase):
|
|||
content = template.render(Context({'content': 'yet another'})).strip()
|
||||
self.assertEqual(content, 'yet another')
|
||||
|
||||
def test_get_last_invalidation_jinja2(self):
|
||||
original_timestamp = engines['jinja2'].from_string(
|
||||
"{{ timestamp }}"
|
||||
).render({
|
||||
'timestamp': get_last_invalidation('auth.Group', 'cachalot_test'),
|
||||
})
|
||||
template = engines['jinja2'].from_string(
|
||||
"{{ get_last_invalidation('auth.Group', 'cachalot_test') }}")
|
||||
timestamp = template.render({})
|
||||
|
||||
self.assertNotEqual(timestamp, '')
|
||||
self.assertNotEqual(timestamp, '0.0')
|
||||
self.assertAlmostEqual(float(timestamp), float(original_timestamp),
|
||||
delta=0.1)
|
||||
|
||||
def test_cache_jinja2(self):
|
||||
# Invalid arguments
|
||||
with self.assertRaises(TemplateSyntaxError,
|
||||
msg="'invalid' is not a valid keyword argument "
|
||||
"for {% cache %}"):
|
||||
engines['jinja2'].from_string("""
|
||||
{% cache cache_key='anything', invalid='what?' %}{% endcache %}
|
||||
""")
|
||||
with self.assertRaises(ValueError, msg='You must set `cache_key` when '
|
||||
'the template is not a file.'):
|
||||
engines['jinja2'].from_string(
|
||||
'{% cache %} broken {% endcache %}').render()
|
||||
|
||||
# With the minimum number of arguments
|
||||
template = engines['jinja2'].from_string("""
|
||||
{%- cache cache_key='first' -%}
|
||||
{{ content1 }}
|
||||
{%- endcache -%}
|
||||
{%- cache cache_key='second' -%}
|
||||
{{ content2 }}
|
||||
{%- endcache -%}
|
||||
""")
|
||||
content = template.render({'content1': 'abc', 'content2': 'def'})
|
||||
self.assertEqual(content, 'abcdef')
|
||||
invalidate()
|
||||
content = template.render({'content1': 'ghi', 'content2': 'jkl'})
|
||||
self.assertEqual(content, 'abcdef')
|
||||
|
||||
# With the maximum number of arguments
|
||||
template = engines['jinja2'].from_string("""
|
||||
{%- cache get_last_invalidation('auth.Group', 'cachalot_test',
|
||||
cache_alias=cache),
|
||||
timeout=10, cache_key='cache_key_name', cache_alias=cache -%}
|
||||
{{ content }}
|
||||
{%- endcache -%}
|
||||
""")
|
||||
content = template.render({'content': 'something',
|
||||
'cache': self.cache_alias2})
|
||||
self.assertEqual(content, 'something')
|
||||
content = template.render({'content': 'anything',
|
||||
'cache': self.cache_alias2})
|
||||
self.assertEqual(content, 'something')
|
||||
invalidate('cachalot_test', cache_alias=DEFAULT_CACHE_ALIAS)
|
||||
content = template.render({'content': 'yet another',
|
||||
'cache': self.cache_alias2})
|
||||
self.assertEqual(content, 'something')
|
||||
invalidate('cachalot_test')
|
||||
content = template.render({'content': 'will you change?',
|
||||
'cache': self.cache_alias2})
|
||||
self.assertEqual(content, 'will you change?')
|
||||
caches[self.cache_alias2].clear()
|
||||
content = template.render({'content': 'better!',
|
||||
'cache': self.cache_alias2})
|
||||
self.assertEqual(content, 'better!')
|
||||
|
||||
|
||||
class CommandTestCase(TransactionTestCase):
|
||||
multi_db = True
|
||||
|
|
|
@ -53,7 +53,7 @@ Features
|
|||
- A few bonus features like
|
||||
:ref:`a signal triggered at each database change <Signal>`
|
||||
(including bulk changes) and
|
||||
:ref:`a template tag for a better template fragment caching <Template tag>`.
|
||||
:ref:`a template tag for a better template fragment caching <Template utils>`.
|
||||
|
||||
Comparison with similar tools
|
||||
.............................
|
||||
|
|
|
@ -166,10 +166,10 @@ For example:
|
|||
settings.CACHALOT_ENABLED = False
|
||||
|
||||
|
||||
.. _Template tag:
|
||||
.. _Template utils:
|
||||
|
||||
Template tag
|
||||
............
|
||||
Template utils
|
||||
..............
|
||||
|
||||
`Caching template fragments <https://docs.djangoproject.com/en/1.8/topics/cache/#template-fragment-caching>`_
|
||||
can be extremely powerful to speedup a Django application. However, it often
|
||||
|
@ -190,6 +190,9 @@ or tables. The API function
|
|||
and we provided a ``get_last_invalidation`` template tag to directly
|
||||
use it in templates. It works exactly the same as the API function.
|
||||
|
||||
Django template tag
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Example of a quite heavy nested loop with a lot of SQL queries
|
||||
(considering no prefetch has been done)::
|
||||
|
||||
|
@ -215,6 +218,45 @@ are also available (see
|
|||
:meth:`cachalot.api.get_last_invalidation`).
|
||||
|
||||
|
||||
Jinja2 statement and function
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A Jinja2 extension for django-cachalot can be used, simply add
|
||||
``''cachalot.jinja2.ext','`` to the ``'extensions'`` list of the ``OPTIONS``
|
||||
dict in the Django ``TEMPLATES`` settings.
|
||||
|
||||
It provides:
|
||||
|
||||
- The API function
|
||||
:meth:`get_last_invalidation <cachalot.api.get_last_invalidation>` directly
|
||||
available as a function anywhere in Jinja2.
|
||||
- An Jinja2 statement equivalent to the ``cache`` template tag of Django.
|
||||
|
||||
The ``cache`` does the same thing as its Django template equivalent,
|
||||
except that ``cache_key`` and ``timeout`` are optional keyword arguments, and
|
||||
you need to add commas between arguments. When unspecified, ``cache_key`` is
|
||||
generated from the template filename plus the statement line number, and
|
||||
``timeout`` defaults to infinite. To specify which cache should store the
|
||||
saved content, use the ``cache_alias`` keyword argument.
|
||||
|
||||
Same example than above, but for Jinja2::
|
||||
|
||||
{% cache get_last_invalidation('auth.User', 'library.Book', 'library.Author'),
|
||||
cache_key='short_user_profile', timeout=3600 %}
|
||||
{{ user }} has borrowed these books:
|
||||
{% for book in user.borrowed_books.all() %}
|
||||
<div class="book">
|
||||
{{ book }} ({{ book.pages.count() }} pages)
|
||||
<span class="authors">
|
||||
{% for author in book.authors.all() %}
|
||||
{{ author }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endcache %}
|
||||
|
||||
|
||||
.. _Signal:
|
||||
|
||||
Signal
|
||||
|
|
|
@ -6,3 +6,4 @@ django-redis
|
|||
python-memcached
|
||||
pylibmc
|
||||
pytz
|
||||
Jinja2
|
||||
|
|
16
settings.py
16
settings.py
|
@ -24,8 +24,9 @@ DATABASES = {
|
|||
if django_version[:2] == (1, 8):
|
||||
DATABASES['postgresql']['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
|
||||
for alias in DATABASES:
|
||||
test_db_name = 'test_' + DATABASES[alias]['NAME']
|
||||
DATABASES[alias]['TEST'] = {'NAME': test_db_name}
|
||||
if 'TEST' not in DATABASES[alias]:
|
||||
test_db_name = 'test_' + DATABASES[alias]['NAME']
|
||||
DATABASES[alias]['TEST'] = {'NAME': test_db_name}
|
||||
|
||||
DATABASES['default'] = DATABASES.pop(os.environ.get('DB_ENGINE', 'sqlite3'))
|
||||
|
||||
|
@ -105,12 +106,21 @@ TEMPLATES = [
|
|||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
},
|
||||
{
|
||||
'BACKEND': 'django.template.backends.jinja2.Jinja2',
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'extensions': [
|
||||
'cachalot.jinja2.ext',
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
MIDDLEWARE_CLASSES = ()
|
||||
PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher',)
|
||||
SECRET_KEY = 'it’s not important but we have to set it'
|
||||
SECRET_KEY = 'it’s not important in tests but we have to set it'
|
||||
|
||||
|
||||
USE_TZ = False # Time zones are not supported by MySQL,
|
||||
|
|
5
tox.ini
5
tox.ini
|
@ -14,13 +14,14 @@ deps =
|
|||
django1.9: Django>=1.9,<1.10
|
||||
django1.10: Django>=1.10,<1.11
|
||||
psycopg2
|
||||
mysqlclient
|
||||
django-redis
|
||||
python-memcached
|
||||
# FIXME: Remove the version when this is fixed: https://github.com/lericson/pylibmc/issues/216
|
||||
pylibmc==1.5.0
|
||||
pytz
|
||||
py{2.7,3.3,3.4,3.5}: coverage
|
||||
py{2.7,3.3,3.4,3.5}: mysqlclient
|
||||
jinja2
|
||||
coverage
|
||||
setenv =
|
||||
sqlite3: DB_ENGINE=sqlite3
|
||||
postgresql: DB_ENGINE=postgresql
|
||||
|
|
Loading…
Reference in New Issue