Kaydet (Commit) 655f5249 authored tarafından Preston Timmons's avatar Preston Timmons

Fixed #17085, #24783 -- Refactored template library registration.

* Converted the ``libraries`` and ``builtins`` globals of
  ``django.template.base`` into properties of the Engine class.
* Added a public API for explicit registration of libraries and builtins.
üst 7b8008a0
......@@ -12,12 +12,7 @@ from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.db import models
from django.http import Http404
from django.template.base import (
InvalidTemplateLibrary, builtins, get_library, get_templatetags_modules,
libraries,
)
from django.template.engine import Engine
from django.utils._os import upath
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
......@@ -60,31 +55,32 @@ class TemplateTagIndexView(BaseAdminDocsView):
template_name = 'admin_doc/template_tag_index.html'
def get_context_data(self, **kwargs):
load_all_installed_template_libraries()
tags = []
app_libs = list(libraries.items())
builtin_libs = [(None, lib) for lib in builtins]
for module_name, library in builtin_libs + app_libs:
for tag_name, tag_func in library.tags.items():
title, body, metadata = utils.parse_docstring(tag_func.__doc__)
if title:
title = utils.parse_rst(title, 'tag', _('tag:') + tag_name)
if body:
body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
if library in builtins:
tag_library = ''
else:
try:
engine = Engine.get_default()
except ImproperlyConfigured:
# Non-trivial TEMPLATES settings aren't supported (#24125).
pass
else:
app_libs = sorted(engine.template_libraries.items())
builtin_libs = [('', lib) for lib in engine.template_builtins]
for module_name, library in builtin_libs + app_libs:
for tag_name, tag_func in library.tags.items():
title, body, metadata = utils.parse_docstring(tag_func.__doc__)
if title:
title = utils.parse_rst(title, 'tag', _('tag:') + tag_name)
if body:
body = utils.parse_rst(body, 'tag', _('tag:') + tag_name)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name)
tag_library = module_name.split('.')[-1]
tags.append({
'name': tag_name,
'title': title,
'body': body,
'meta': metadata,
'library': tag_library,
})
tags.append({
'name': tag_name,
'title': title,
'body': body,
'meta': metadata,
'library': tag_library,
})
kwargs.update({'tags': tags})
return super(TemplateTagIndexView, self).get_context_data(**kwargs)
......@@ -93,31 +89,32 @@ class TemplateFilterIndexView(BaseAdminDocsView):
template_name = 'admin_doc/template_filter_index.html'
def get_context_data(self, **kwargs):
load_all_installed_template_libraries()
filters = []
app_libs = list(libraries.items())
builtin_libs = [(None, lib) for lib in builtins]
for module_name, library in builtin_libs + app_libs:
for filter_name, filter_func in library.filters.items():
title, body, metadata = utils.parse_docstring(filter_func.__doc__)
if title:
title = utils.parse_rst(title, 'filter', _('filter:') + filter_name)
if body:
body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
if library in builtins:
tag_library = ''
else:
try:
engine = Engine.get_default()
except ImproperlyConfigured:
# Non-trivial TEMPLATES settings aren't supported (#24125).
pass
else:
app_libs = sorted(engine.template_libraries.items())
builtin_libs = [('', lib) for lib in engine.template_builtins]
for module_name, library in builtin_libs + app_libs:
for filter_name, filter_func in library.filters.items():
title, body, metadata = utils.parse_docstring(filter_func.__doc__)
if title:
title = utils.parse_rst(title, 'filter', _('filter:') + filter_name)
if body:
body = utils.parse_rst(body, 'filter', _('filter:') + filter_name)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name)
tag_library = module_name.split('.')[-1]
filters.append({
'name': filter_name,
'title': title,
'body': body,
'meta': metadata,
'library': tag_library,
})
filters.append({
'name': filter_name,
'title': title,
'body': body,
'meta': metadata,
'library': tag_library,
})
kwargs.update({'filters': filters})
return super(TemplateFilterIndexView, self).get_context_data(**kwargs)
......@@ -320,29 +317,6 @@ class TemplateDetailView(BaseAdminDocsView):
# Helper functions #
####################
def load_all_installed_template_libraries():
# Load/register all template tag libraries from installed apps.
for module_name in get_templatetags_modules():
mod = import_module(module_name)
if not hasattr(mod, '__file__'):
# e.g. packages installed as eggs
continue
try:
libraries = [
os.path.splitext(p)[0]
for p in os.listdir(os.path.dirname(upath(mod.__file__)))
if p.endswith('.py') and p[0].isalpha()
]
except OSError:
continue
else:
for library_name in libraries:
try:
get_library(library_name)
except InvalidTemplateLibrary:
pass
def get_return_data_type(func_name):
"""Return a somewhat-helpful data type given a function name"""
......
......@@ -66,7 +66,7 @@ from .base import (Context, Node, NodeList, Origin, RequestContext, # NOQA
from .base import resolve_variable # NOQA
# Library management
from .base import Library # NOQA
from .library import Library # NOQA
__all__ += ('Template', 'Context', 'RequestContext')
......@@ -3,11 +3,15 @@ from __future__ import absolute_import
import sys
import warnings
from importlib import import_module
from pkgutil import walk_packages
from django.apps import apps
from django.conf import settings
from django.template import TemplateDoesNotExist
from django.template.context import Context, RequestContext, make_context
from django.template.engine import Engine, _dirs_undefined
from django.template.library import InvalidTemplateLibrary
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
......@@ -23,6 +27,8 @@ class DjangoTemplates(BaseEngine):
options = params.pop('OPTIONS').copy()
options.setdefault('debug', settings.DEBUG)
options.setdefault('file_charset', settings.FILE_CHARSET)
libraries = options.get('libraries', {})
options['libraries'] = self.get_templatetag_libraries(libraries)
super(DjangoTemplates, self).__init__(params)
self.engine = Engine(self.dirs, self.app_dirs, **options)
......@@ -35,6 +41,15 @@ class DjangoTemplates(BaseEngine):
except TemplateDoesNotExist as exc:
reraise(exc, self)
def get_templatetag_libraries(self, custom_libraries):
"""
Return a collation of template tag libraries from installed
applications and the supplied custom_libraries argument.
"""
libraries = get_installed_libraries()
libraries.update(custom_libraries)
return libraries
class Template(object):
......@@ -90,3 +105,48 @@ def reraise(exc, backend):
if hasattr(exc, 'template_debug'):
new.template_debug = exc.template_debug
six.reraise(exc.__class__, new, sys.exc_info()[2])
def get_installed_libraries():
"""
Return the built-in template tag libraries and those from installed
applications. Libraries are stored in a dictionary where keys are the
individual module names, not the full module paths. Example:
django.templatetags.i18n is stored as i18n.
"""
libraries = {}
candidates = ['django.templatetags']
candidates.extend(
'%s.templatetags' % app_config.name
for app_config in apps.get_app_configs())
for candidate in candidates:
try:
pkg = import_module(candidate)
except ImportError:
# No templatetags package defined. This is safe to ignore.
continue
if hasattr(pkg, '__path__'):
for name in get_package_libraries(pkg):
libraries[name[len(candidate) + 1:]] = name
return libraries
def get_package_libraries(pkg):
"""
Recursively yield template tag libraries defined in submodules of a
package.
"""
for entry in walk_packages(pkg.__path__, pkg.__name__ + '.'):
try:
module = import_module(entry[1])
except ImportError as e:
raise InvalidTemplateLibrary(
"Invalid template library specified. ImportError raised when "
"trying to load '%s': %s" % (entry[1], e)
)
if hasattr(module, 'register'):
yield entry[1]
This diff is collapsed.
......@@ -25,7 +25,8 @@ from django.utils.text import (
from django.utils.timesince import timesince, timeuntil
from django.utils.translation import ugettext, ungettext
from .base import Library, Variable, VariableDoesNotExist
from .base import Variable, VariableDoesNotExist
from .library import Library
register = Library()
......
......@@ -19,12 +19,12 @@ from django.utils.safestring import mark_safe
from .base import (
BLOCK_TAG_END, BLOCK_TAG_START, COMMENT_TAG_END, COMMENT_TAG_START,
SINGLE_BRACE_END, SINGLE_BRACE_START, VARIABLE_ATTRIBUTE_SEPARATOR,
VARIABLE_TAG_END, VARIABLE_TAG_START, Context, InvalidTemplateLibrary,
Library, Node, NodeList, Template, TemplateSyntaxError,
VariableDoesNotExist, get_library, kwarg_re, render_value_in_context,
token_kwargs,
VARIABLE_TAG_END, VARIABLE_TAG_START, Context, Node, NodeList, Template,
TemplateSyntaxError, VariableDoesNotExist, kwarg_re,
render_value_in_context, token_kwargs,
)
from .defaultfilters import date
from .library import Library
from .smartif import IfParser, Literal
register = Library()
......@@ -1121,10 +1121,43 @@ def ssi(parser, token):
return SsiNode(filepath, parsed)
def find_library(parser, name):
try:
return parser.libraries[name]
except KeyError:
raise TemplateSyntaxError(
"'%s' is not a registered tag library. Must be one of:\n%s" % (
name, "\n".join(sorted(parser.libraries.keys())),
),
)
def load_from_library(library, label, names):
"""
Return a subset of tags and filters from a library.
"""
subset = Library()
for name in names:
found = False
if name in library.tags:
found = True
subset.tags[name] = library.tags[name]
if name in library.filters:
found = True
subset.filters[name] = library.filters[name]
if found is False:
raise TemplateSyntaxError(
"'%s' is not a valid tag or filter in tag library '%s'" % (
name, label,
),
)
return subset
@register.tag
def load(parser, token):
"""
Loads a custom template tag set.
Loads a custom template tag library into the parser.
For example, to load the template tags in
``django/templatetags/news/photos.py``::
......@@ -1140,35 +1173,16 @@ def load(parser, token):
# token.split_contents() isn't useful here because this tag doesn't accept variable as arguments
bits = token.contents.split()
if len(bits) >= 4 and bits[-2] == "from":
try:
taglib = bits[-1]
lib = get_library(taglib)
except InvalidTemplateLibrary as e:
raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
(taglib, e))
else:
temp_lib = Library()
for name in bits[1:-2]:
if name in lib.tags:
temp_lib.tags[name] = lib.tags[name]
# a name could be a tag *and* a filter, so check for both
if name in lib.filters:
temp_lib.filters[name] = lib.filters[name]
elif name in lib.filters:
temp_lib.filters[name] = lib.filters[name]
else:
raise TemplateSyntaxError("'%s' is not a valid tag or filter in tag library '%s'" %
(name, taglib))
parser.add_library(temp_lib)
# from syntax is used; load individual tags from the library
name = bits[-1]
lib = find_library(parser, name)
subset = load_from_library(lib, name, bits[1:-2])
parser.add_library(subset)
else:
for taglib in bits[1:]:
# add the library to the parser
try:
lib = get_library(taglib)
parser.add_library(lib)
except InvalidTemplateLibrary as e:
raise TemplateSyntaxError("'%s' is not a valid tag library: %s" %
(taglib, e))
# one or more libraries are specified; load and add them to the parser
for name in bits[1:]:
lib = find_library(parser, name)
parser.add_library(lib)
return LoadNode()
......
......@@ -9,6 +9,7 @@ from django.utils.module_loading import import_string
from .base import Context, Template
from .context import _builtin_context_processors
from .exceptions import TemplateDoesNotExist
from .library import import_library
_context_instance_undefined = object()
_dictionary_undefined = object()
......@@ -16,11 +17,16 @@ _dirs_undefined = object()
class Engine(object):
default_builtins = [
'django.template.defaulttags',
'django.template.defaultfilters',
'django.template.loader_tags',
]
def __init__(self, dirs=None, app_dirs=False,
allowed_include_roots=None, context_processors=None,
debug=False, loaders=None, string_if_invalid='',
file_charset='utf-8'):
file_charset='utf-8', libraries=None, builtins=None):
if dirs is None:
dirs = []
if allowed_include_roots is None:
......@@ -35,6 +41,10 @@ class Engine(object):
if app_dirs:
raise ImproperlyConfigured(
"app_dirs must not be set when loaders is defined.")
if libraries is None:
libraries = {}
if builtins is None:
builtins = []
if isinstance(allowed_include_roots, six.string_types):
raise ImproperlyConfigured(
......@@ -48,6 +58,10 @@ class Engine(object):
self.loaders = loaders
self.string_if_invalid = string_if_invalid
self.file_charset = file_charset
self.libraries = libraries
self.template_libraries = self.get_template_libraries(libraries)
self.builtins = self.default_builtins + builtins
self.template_builtins = self.get_template_builtins(self.builtins)
@staticmethod
@lru_cache.lru_cache()
......@@ -90,6 +104,15 @@ class Engine(object):
context_processors += tuple(self.context_processors)
return tuple(import_string(path) for path in context_processors)
def get_template_builtins(self, builtins):
return [import_library(x) for x in builtins]
def get_template_libraries(self, libraries):
loaded = {}
for name, path in libraries.items():
loaded[name] = import_library(path)
return loaded
@cached_property
def template_loaders(self):
return self.get_template_loaders(self.loaders)
......
This diff is collapsed.
......@@ -4,9 +4,9 @@ from django.utils import six
from django.utils.safestring import mark_safe
from .base import (
Library, Node, Template, TemplateSyntaxError, TextNode, Variable,
token_kwargs,
Node, Template, TemplateSyntaxError, TextNode, Variable, token_kwargs,
)
from .library import Library
register = Library()
......
......@@ -35,9 +35,6 @@ def update_installed_apps(**kwargs):
# Rebuild management commands cache
from django.core.management import get_commands
get_commands.cache_clear()
# Rebuild templatetags module cache.
from django.template.base import get_templatetags_modules
get_templatetags_modules.cache_clear()
# Rebuild get_app_template_dirs cache.
from django.template.utils import get_app_template_dirs
get_app_template_dirs.cache_clear()
......
......@@ -13,9 +13,11 @@ available to your templates using the :ttag:`{% load %}<load>` tag.
Code layout
-----------
Custom template tags and filters must live inside a Django app. If they relate
to an existing app it makes sense to bundle them there; otherwise, you should
create a new app to hold them.
The most common place to specify custom template tags and filters is inside
a Django app. If they relate to an existing app, it makes sense to bundle them
there; otherwise, they can be added to a new app. When a Django app is added
to :setting:`INSTALLED_APPS`, any tags it defines in the conventional location
described below are automatically made available to load within templates.
The app should contain a ``templatetags`` directory, at the same level as
``models.py``, ``views.py``, etc. If this doesn't already exist, create it -
......@@ -63,6 +65,15 @@ following::
register = template.Library()
.. versionadded:: 1.9
Alternatively, template tag modules can be registered through the
``'libraries'`` argument to
:class:`~django.template.backends.django.DjangoTemplates`. This is useful if
you want to use a different label from the template tag module name when
loading template tags. It also enables you to register tags without installing
an application.
.. admonition:: Behind the scenes
For a ton of examples, read the source code for Django's default filters
......
......@@ -41,7 +41,7 @@ lower level APIs:
Configuring an engine
=====================
.. class:: Engine([dirs][, app_dirs][, allowed_include_roots][, context_processors][, debug][, loaders][, string_if_invalid][, file_charset])
.. class:: Engine([dirs][, app_dirs][, allowed_include_roots][, context_processors][, debug][, loaders][, string_if_invalid][, file_charset][, libraries][, builtins])
.. versionadded:: 1.8
......@@ -114,6 +114,34 @@ Configuring an engine
It defaults to ``'utf-8'``.
* ``'libraries'``: A dictionary of labels and dotted Python paths of template
tag modules to register with the template engine. This is used to add new
libraries or provide alternate labels for existing ones. For example::
Engine(
libraries={
'myapp_tags': 'path.to.myapp.tags',
'admin.urls': 'django.contrib.admin.templatetags.admin_urls',
},
)
Libraries can be loaded by passing the corresponding dictionary key to
the :ttag:`{% load %}<load>` tag.
* ``'builtins'``: A list of dotted Python paths of template tag modules to
add to :doc:`built-ins </ref/templates/builtins>`. For example::
Engine(
builtins=['myapp.builtins'],
)
Tags and filters from built-in libraries can be used without first calling
the :ttag:`{% load %}<load>` tag.
.. versionadded:: 1.9
The ``libraries`` and ``builtins`` arguments were added.
.. staticmethod:: Engine.get_default()
When a Django project configures one and only one
......
......@@ -263,6 +263,10 @@ Templates
* :ref:`Debug page integration <template-debug-integration>` for custom
template engines was added.
* The :class:`~django.template.backends.django.DjangoTemplates` backend gained
the ability to register libraries and builtins explicitly through the
template :setting:`OPTIONS <TEMPLATES-OPTIONS>`.
Requests and Responses
^^^^^^^^^^^^^^^^^^^^^^
......@@ -467,6 +471,28 @@ You don't need any of this if you're querying the database through the ORM,
even if you're using :meth:`raw() <django.db.models.query.QuerySet.raw>`
queries. The ORM takes care of managing time zone information.
Template tag modules are imported when templates are configured
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The :class:`~django.template.backends.django.DjangoTemplates` backend now
performs discovery on installed template tag modules when instantiated. This
update enables libraries to be provided explicitly via the ``'libraries'``
key of :setting:`OPTIONS <TEMPLATES-OPTIONS>` when defining a
:class:`~django.template.backends.django.DjangoTemplates` backend. Import
or syntax errors in template tag modules now fail early at instantiation time
rather than when a template with a :ttag:`{% load %}<load>` tag is first
compiled.
``django.template.base.add_to_builtins()`` is removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Although it was a private API, projects commonly used ``add_to_builtins()`` to
make template tags and filters available without using the
:ttag:`{% load %}<load>` tag. This API has been formalized. Projects should now
define built-in libraries via the ``'builtins'`` key of :setting:`OPTIONS
<TEMPLATES-OPTIONS>` when defining a
:class:`~django.template.backends.django.DjangoTemplates` backend.
Miscellaneous
~~~~~~~~~~~~~
......
......@@ -401,6 +401,34 @@ applications. This generic name was kept for backwards-compatibility.
It defaults to the value of :setting:`FILE_CHARSET`.
* ``'libraries'``: A dictionary of labels and dotted Python paths of template
tag modules to register with the template engine. This can be used to add
new libraries or provide alternate labels for existing ones. For example::
OPTIONS={
'libraries': {
'myapp_tags': 'path.to.myapp.tags',
'admin.urls': 'django.contrib.admin.templatetags.admin_urls',
},
}
Libraries can be loaded by passing the corresponding dictionary key to
the :ttag:`{% load %}<load>` tag.
* ``'builtins'``: A list of dotted Python paths of template tag modules to
add to :doc:`built-ins </ref/templates/builtins>`. For example::
OPTIONS={
'builtins': ['myapp.builtins'],
}
Tags and filters from built-in libraries can be used without first calling
the :ttag:`{% load %} <load>` tag.
.. versionadded:: 1.9
The ``libraries`` and ``builtins`` arguments were added.
.. module:: django.template.backends.jinja2
.. class:: Jinja2
......
from django.template import Library
register = Library()
from django.template import Library
register = Library()
from django.template import Library
register = Library()
......@@ -2,7 +2,8 @@ from template_tests.test_response import test_processor_name
from django.template import RequestContext
from django.template.backends.django import DjangoTemplates
from django.test import RequestFactory, ignore_warnings
from django.template.library import InvalidTemplateLibrary
from django.test import RequestFactory, ignore_warnings, override_settings
from django.utils.deprecation import RemovedInDjango20Warning
from .test_dummy import TemplateStringsTests
......@@ -51,3 +52,78 @@ class DjangoTemplatesTests(TemplateStringsTests):
"the two arguments refer to the same request.")
with self.assertRaisesMessage(ValueError, msg):
template.render(request_context, other_request)
@override_settings(INSTALLED_APPS=['template_backends.apps.good'])
def test_templatetag_discovery(self):
engine = DjangoTemplates({
'DIRS': [],
'APP_DIRS': False,
'NAME': 'django',
'OPTIONS': {
'libraries': {
'alternate': 'template_backends.apps.good.templatetags.good_tags',
'override': 'template_backends.apps.good.templatetags.good_tags',
},
},
})
# libraries are discovered from installed applications
self.assertEqual(
engine.engine.libraries['good_tags'],
'template_backends.apps.good.templatetags.good_tags',
)
self.assertEqual(
engine.engine.libraries['subpackage.tags'],
'template_backends.apps.good.templatetags.subpackage.tags',
)
# libraries are discovered from django.templatetags
self.assertEqual(
engine.engine.libraries['static'],
'django.templatetags.static',
)
# libraries passed in OPTIONS are registered
self.assertEqual(
engine.engine.libraries['alternate'],
'template_backends.apps.good.templatetags.good_tags',
)
# libraries passed in OPTIONS take precedence over discovered ones
self.assertEqual(
engine.engine.libraries['override'],
'template_backends.apps.good.templatetags.good_tags',
)
@override_settings(INSTALLED_APPS=['template_backends.apps.importerror'])
def test_templatetag_discovery_import_error(self):
"""
Import errors in tag modules should be reraised with a helpful message.
"""
with self.assertRaisesMessage(
InvalidTemplateLibrary,
"ImportError raised when trying to load "
"'template_backends.apps.importerror.templatetags.broken_tags'"
):
DjangoTemplates({
'DIRS': [],
'APP_DIRS': False,
'NAME': 'django',
'OPTIONS': {},
})
def test_builtins_discovery(self):
engine = DjangoTemplates({
'DIRS': [],
'APP_DIRS': False,
'NAME': 'django',
'OPTIONS': {
'builtins': ['template_backends.apps.good.templatetags.good_tags'],
},
})
self.assertEqual(
engine.engine.builtins, [
'django.template.defaulttags',
'django.template.defaultfilters',
'django.template.loader_tags',
'template_backends.apps.good.templatetags.good_tags',
]
)
......@@ -6,6 +6,10 @@ from ..utils import setup
class CacheTagTests(SimpleTestCase):
libraries = {
'cache': 'django.templatetags.cache',
'custom': 'template_tests.templatetags.custom',
}
def tearDown(self):
cache.clear()
......@@ -121,7 +125,7 @@ class CacheTests(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.engine = Engine()
cls.engine = Engine(libraries={'cache': 'django.templatetags.cache'})
super(CacheTests, cls).setUpClass()
def test_cache_regression_20130(self):
......
......@@ -6,6 +6,7 @@ from ..utils import setup
class CycleTagTests(SimpleTestCase):
libraries = {'future': 'django.templatetags.future'}
@setup({'cycle01': '{% cycle a %}'})
def test_cycle01(self):
......
......@@ -56,6 +56,7 @@ inheritance_templates = {
class InheritanceTests(SimpleTestCase):
libraries = {'testtags': 'template_tests.templatetags.testtags'}
@setup(inheritance_templates)
def test_inheritance01(self):
......
......@@ -6,6 +6,7 @@ from ..utils import setup
class FirstOfTagTests(SimpleTestCase):
libraries = {'future': 'django.templatetags.future'}
@setup({'firstof01': '{% firstof a b c %}'})
def test_firstof01(self):
......
......@@ -6,6 +6,7 @@ from ..utils import setup
class ForTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'for-tag01': '{% for val in values %}{{ val }}{% endfor %}'})
def test_for_tag01(self):
......
......@@ -10,6 +10,10 @@ from ..utils import setup
class I18nTagTests(SimpleTestCase):
libraries = {
'custom': 'template_tests.templatetags.custom',
'i18n': 'django.templatetags.i18n',
}
@setup({'i18n01': '{% load i18n %}{% trans \'xxxyyyxxx\' %}'})
def test_i18n01(self):
......
......@@ -5,6 +5,7 @@ from ..utils import setup
class IfChangedTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'ifchanged01': '{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}'})
def test_ifchanged01(self):
......
......@@ -13,6 +13,7 @@ include_fail_templates = {
class IncludeTagTests(SimpleTestCase):
libraries = {'bad_tag': 'template_tests.templatetags.bad_tag'}
@setup({'include01': '{% include "basic-syntax01" %}'}, basic_templates)
def test_include01(self):
......
......@@ -4,6 +4,7 @@ from ..utils import setup
class InvalidStringTests(SimpleTestCase):
libraries = {'i18n': 'django.templatetags.i18n'}
@setup({'invalidstr01': '{{ var|default:"Foo" }}'})
def test_invalidstr01(self):
......
......@@ -5,6 +5,10 @@ from ..utils import setup
class LoadTagTests(SimpleTestCase):
libraries = {
'subpackage.echo': 'template_tests.templatetags.subpackage.echo',
'testtags': 'template_tests.templatetags.testtags',
}
@setup({'load01': '{% load testtags subpackage.echo %}{% echo test %} {% echo2 "test" %}'})
def test_load01(self):
......@@ -42,30 +46,30 @@ class LoadTagTests(SimpleTestCase):
# {% load %} tag errors
@setup({'load07': '{% load echo other_echo bad_tag from testtags %}'})
def test_load07(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'bad_tag' is not a valid tag or filter in tag library 'testtags'"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load07')
@setup({'load08': '{% load echo other_echo bad_tag from %}'})
def test_load08(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'echo' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load08')
@setup({'load09': '{% load from testtags %}'})
def test_load09(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'from' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load09')
@setup({'load10': '{% load echo from bad_library %}'})
def test_load10(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'bad_library' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load10')
@setup({'load11': '{% load subpackage.echo_invalid %}'})
def test_load11(self):
with self.assertRaises(TemplateSyntaxError):
self.engine.get_template('load11')
@setup({'load12': '{% load subpackage.missing %}'})
def test_load12(self):
with self.assertRaises(TemplateSyntaxError):
msg = "'subpackage.missing' is not a registered tag library. Must be one of:\nsubpackage.echo\ntesttags"
with self.assertRaisesMessage(TemplateSyntaxError, msg):
self.engine.get_template('load12')
......@@ -5,6 +5,7 @@ from ..utils import setup
class SimpleTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'simpletag-renamed01': '{% load custom %}{% minusone 7 %}'})
def test_simpletag_renamed01(self):
......
......@@ -7,6 +7,7 @@ from ..utils import setup
@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/")
class StaticTagTests(SimpleTestCase):
libraries = {'static': 'django.templatetags.static'}
@setup({'static-prefixtag01': '{% load static %}{% get_static_prefix %}'})
def test_static_prefixtag01(self):
......
......@@ -6,6 +6,7 @@ from ..utils import setup
class WidthRatioTagTests(SimpleTestCase):
libraries = {'custom': 'template_tests.templatetags.custom'}
@setup({'widthratio01': '{% widthratio a b 0 %}'})
def test_widthratio01(self):
......
from django.template import Library, Node
register = Library()
class EchoNode(Node):
def __init__(self, contents):
self.contents = contents
def render(self, context):
return ' '.join(self.contents)
@register.tag
def echo(parser, token):
return EchoNode(token.contents.split()[1:])
register.tag('other_echo', echo)
@register.filter
def upper(value):
return value.upper()
......@@ -4,18 +4,26 @@ import os
from django.template import Context, Engine, TemplateSyntaxError
from django.template.base import Node
from django.template.library import InvalidTemplateLibrary
from django.test import SimpleTestCase, ignore_warnings
from django.test.utils import extend_sys_path
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from .templatetags import custom, inclusion
from .utils import ROOT
LIBRARIES = {
'custom': 'template_tests.templatetags.custom',
'inclusion': 'template_tests.templatetags.inclusion',
}
class CustomFilterTests(SimpleTestCase):
def test_filter(self):
t = Engine().from_string("{% load custom %}{{ string|trim:5 }}")
engine = Engine(libraries=LIBRARIES)
t = engine.from_string("{% load custom %}{{ string|trim:5 }}")
self.assertEqual(
t.render(Context({"string": "abcdefghijklmnopqrstuvwxyz"})),
"abcde"
......@@ -26,7 +34,7 @@ class TagTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.engine = Engine(app_dirs=True)
cls.engine = Engine(app_dirs=True, libraries=LIBRARIES)
super(TagTestCase, cls).setUpClass()
def verify_tag(self, tag, name):
......@@ -269,7 +277,7 @@ class InclusionTagTests(TagTestCase):
"""
#23441 -- InclusionNode shouldn't modify its nodelist at render time.
"""
engine = Engine(app_dirs=True)
engine = Engine(app_dirs=True, libraries=LIBRARIES)
template = engine.from_string('{% load inclusion %}{% inclusion_no_params %}')
count = template.nodelist.get_nodes_by_type(Node)
template.render(Context({}))
......@@ -281,7 +289,7 @@ class InclusionTagTests(TagTestCase):
when rendering. Otherwise, leftover values such as blocks from
extending can interfere with subsequent rendering.
"""
engine = Engine(app_dirs=True)
engine = Engine(app_dirs=True, libraries=LIBRARIES)
template = engine.from_string('{% load inclusion %}{% inclusion_extends1 %}{% inclusion_extends2 %}')
self.assertEqual(template.render(Context({})).strip(), 'one\ntwo')
......@@ -313,34 +321,37 @@ class TemplateTagLoadingTests(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.egg_dir = os.path.join(ROOT, 'eggs')
cls.engine = Engine()
super(TemplateTagLoadingTests, cls).setUpClass()
def test_load_error(self):
ttext = "{% load broken_tag %}"
with self.assertRaises(TemplateSyntaxError) as e:
self.engine.from_string(ttext)
self.assertIn('ImportError', e.exception.args[0])
self.assertIn('Xtemplate', e.exception.args[0])
msg = (
"Invalid template library specified. ImportError raised when "
"trying to load 'template_tests.broken_tag': cannot import name "
"'?Xtemplate'?"
)
with six.assertRaisesRegex(self, InvalidTemplateLibrary, msg):
Engine(libraries={
'broken_tag': 'template_tests.broken_tag',
})
def test_load_error_egg(self):
ttext = "{% load broken_egg %}"
egg_name = '%s/tagsegg.egg' % self.egg_dir
msg = (
"Invalid template library specified. ImportError raised when "
"trying to load 'tagsegg.templatetags.broken_egg': cannot "
"import name '?Xtemplate'?"
)
with extend_sys_path(egg_name):
with self.assertRaises(TemplateSyntaxError):
with self.settings(INSTALLED_APPS=['tagsegg']):
self.engine.from_string(ttext)
try:
with self.settings(INSTALLED_APPS=['tagsegg']):
self.engine.from_string(ttext)
except TemplateSyntaxError as e:
self.assertIn('ImportError', e.args[0])
self.assertIn('Xtemplate', e.args[0])
with six.assertRaisesRegex(self, InvalidTemplateLibrary, msg):
Engine(libraries={
'broken_egg': 'tagsegg.templatetags.broken_egg',
})
def test_load_working_egg(self):
ttext = "{% load working_egg %}"
egg_name = '%s/tagsegg.egg' % self.egg_dir
with extend_sys_path(egg_name):
with self.settings(INSTALLED_APPS=['tagsegg']):
self.engine.from_string(ttext)
engine = Engine(libraries={
'working_egg': 'tagsegg.templatetags.working_egg',
})
engine.from_string(ttext)
......@@ -14,7 +14,10 @@ OTHER_DIR = os.path.join(ROOT, 'other_templates')
class DeprecatedRenderToStringTest(SimpleTestCase):
def setUp(self):
self.engine = Engine(dirs=[TEMPLATE_DIR])
self.engine = Engine(
dirs=[TEMPLATE_DIR],
libraries={'custom': 'template_tests.templatetags.custom'},
)
def test_basic_context(self):
self.assertEqual(
......
from django.template import Library
from django.template.base import Node
from django.test import TestCase
class FilterRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_filter(self):
@self.library.filter
def func():
return ''
self.assertEqual(self.library.filters['func'], func)
def test_filter_parens(self):
@self.library.filter()
def func():
return ''
self.assertEqual(self.library.filters['func'], func)
def test_filter_name_arg(self):
@self.library.filter('name')
def func():
return ''
self.assertEqual(self.library.filters['name'], func)
def test_filter_name_kwarg(self):
@self.library.filter(name='name')
def func():
return ''
self.assertEqual(self.library.filters['name'], func)
def test_filter_call(self):
def func():
return ''
self.library.filter('name', func)
self.assertEqual(self.library.filters['name'], func)
def test_filter_invalid(self):
msg = "Unsupported arguments to Library.filter: (None, '')"
with self.assertRaisesMessage(ValueError, msg):
self.library.filter(None, '')
class InclusionTagRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_inclusion_tag(self):
@self.library.inclusion_tag('template.html')
def func():
return ''
self.assertIn('func', self.library.tags)
def test_inclusion_tag_name(self):
@self.library.inclusion_tag('template.html', name='name')
def func():
return ''
self.assertIn('name', self.library.tags)
class SimpleTagRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_simple_tag(self):
@self.library.simple_tag
def func():
return ''
self.assertIn('func', self.library.tags)
def test_simple_tag_parens(self):
@self.library.simple_tag()
def func():
return ''
self.assertIn('func', self.library.tags)
def test_simple_tag_name_kwarg(self):
@self.library.simple_tag(name='name')
def func():
return ''
self.assertIn('name', self.library.tags)
def test_simple_tag_invalid(self):
msg = "Invalid arguments provided to simple_tag"
with self.assertRaisesMessage(ValueError, msg):
self.library.simple_tag('invalid')
class TagRegistrationTests(TestCase):
def setUp(self):
self.library = Library()
def test_tag(self):
@self.library.tag
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['func'], func)
def test_tag_parens(self):
@self.library.tag()
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['func'], func)
def test_tag_name_arg(self):
@self.library.tag('name')
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['name'], func)
def test_tag_name_kwarg(self):
@self.library.tag(name='name')
def func(parser, token):
return Node()
self.assertEqual(self.library.tags['name'], func)
def test_tag_call(self):
def func(parser, token):
return Node()
self.library.tag('name', func)
self.assertEqual(self.library.tags['name'], func)
def test_tag_invalid(self):
msg = "Unsupported arguments to Library.tag: (None, '')"
with self.assertRaisesMessage(ValueError, msg):
self.library.tag(None, '')
......@@ -49,7 +49,7 @@ class ErrorIndexTest(TestCase):
'range': range(5),
'five': 5,
})
engine = Engine(debug=True)
engine = Engine(debug=True, libraries={'bad_tag': 'template_tests.templatetags.bad_tag'})
for source, expected_error_source_index in tests:
template = engine.from_string(source)
try:
......
......@@ -9,6 +9,7 @@ from django.template import Library, TemplateSyntaxError
from django.template.base import (
TOKEN_BLOCK, FilterExpression, Parser, Token, Variable,
)
from django.template.defaultfilters import register as filter_library
from django.utils import six
......@@ -24,7 +25,7 @@ class ParserTests(TestCase):
def test_filter_parsing(self):
c = {"article": {"section": "News"}}
p = Parser("")
p = Parser("", builtins=[filter_library])
def fe_test(s, val):
self.assertEqual(FilterExpression(s, p).resolve(c), val)
......
......@@ -97,7 +97,10 @@ class TemplateTests(SimpleTestCase):
Errors raised while compiling nodes should include the token
information.
"""
engine = Engine(debug=True)
engine = Engine(
debug=True,
libraries={'bad_tag': 'template_tests.templatetags.bad_tag'},
)
with self.assertRaises(RuntimeError) as e:
engine.from_string("{% load bad_tag %}{% badtag %}")
self.assertEqual(e.exception.template_debug['during'], '{% badtag %}')
......
......@@ -5,9 +5,6 @@ from __future__ import unicode_literals
import functools
import os
from django import template
from django.template import Library
from django.template.base import libraries
from django.template.engine import Engine
from django.test.utils import override_settings
from django.utils._os import upath
......@@ -49,14 +46,17 @@ def setup(templates, *args, **kwargs):
]
def decorator(func):
@register_test_tags
# Make Engine.get_default() raise an exception to ensure that tests
# are properly isolated from Django's global settings.
@override_settings(TEMPLATES=None)
@functools.wraps(func)
def inner(self):
# Set up custom template tag libraries if specified
libraries = getattr(self, 'libraries', {})
self.engine = Engine(
allowed_include_roots=[ROOT],
libraries=libraries,
loaders=loaders,
)
func(self)
......@@ -66,6 +66,7 @@ def setup(templates, *args, **kwargs):
self.engine = Engine(
allowed_include_roots=[ROOT],
libraries=libraries,
loaders=loaders,
string_if_invalid='INVALID',
)
......@@ -75,6 +76,7 @@ def setup(templates, *args, **kwargs):
self.engine = Engine(
allowed_include_roots=[ROOT],
debug=True,
libraries=libraries,
loaders=loaders,
)
func(self)
......@@ -85,43 +87,9 @@ def setup(templates, *args, **kwargs):
return decorator
# Custom template tag for tests
register = Library()
class EchoNode(template.Node):
def __init__(self, contents):
self.contents = contents
def render(self, context):
return ' '.join(self.contents)
@register.tag
def echo(parser, token):
return EchoNode(token.contents.split()[1:])
register.tag('other_echo', echo)
@register.filter
def upper(value):
return value.upper()
def register_test_tags(func):
@functools.wraps(func)
def inner(self):
libraries['testtags'] = register
try:
func(self)
finally:
del libraries['testtags']
return inner
# Helper objects
class SomeException(Exception):
silent_variable_failure = True
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment