diff --git a/compressor/offline/django.py b/compressor/offline/django.py index 944a551..a62407e 100644 --- a/compressor/offline/django.py +++ b/compressor/offline/django.py @@ -1,143 +1,143 @@ -from __future__ import absolute_import # noqa -import io -from types import MethodType - -from django import template -from django.template import Template -from django.template.loader_tags import (ExtendsNode, BlockNode, - BLOCK_CONTEXT_KEY) - - -from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist -from compressor.templatetags.compress import CompressorNode - - -def patched_render(self, context): - # 'Fake' _render method that just returns the context instead of - # rendering. It also checks whether the first node is an extend node or - # not, to be able to handle complex inheritance chain. - self._render_firstnode = MethodType(patched_render_firstnode, self) - self._render_firstnode(context) - - # Cleanup, uninstall our _render monkeypatch now that it has been called - self._render = self._old_render - return context - - -def patched_render_firstnode(self, context): - # If this template has a ExtendsNode, we want to find out what - # should be put in render_context to make the {% block ... %} - # tags work. - # - # We can't fully render the base template(s) (we don't have the - # full context vars - only what's necessary to render the compress - # nodes!), therefore we hack the ExtendsNode we found, patching - # its get_parent method so that rendering the ExtendsNode only - # gives us the blocks content without doing any actual rendering. - extra_context = {} - try: - firstnode = self.nodelist[0] - except IndexError: - firstnode = None - if isinstance(firstnode, ExtendsNode): - firstnode._log = self._log - firstnode._log_verbosity = self._log_verbosity - firstnode._old_get_parent = firstnode.get_parent - firstnode.get_parent = MethodType(patched_get_parent, firstnode) - try: - extra_context = firstnode.render(context) - context.render_context = extra_context.render_context - # We aren't rendering {% block %} tags, but we want - # {{ block.super }} inside {% compress %} inside {% block %}s to - # work. Therefore, we need to pop() the last block context for - # each block name, to emulate what would have been done if the - # {% block %} had been fully rendered. - for blockname in firstnode.blocks.keys(): - context.render_context[BLOCK_CONTEXT_KEY].pop(blockname) - except (IOError, template.TemplateSyntaxError, - template.TemplateDoesNotExist): - # That first node we are trying to render might cause more errors - # that we didn't catch when simply creating a Template instance - # above, so we need to catch that (and ignore it, just like above) - # as well. - if self._log_verbosity > 0: - self._log.write("Caught error when rendering extend node from " - "template %s\n" % getattr(self, 'name', self)) - return None - finally: - # Cleanup, uninstall our get_parent monkeypatch now that it has been called - firstnode.get_parent = firstnode._old_get_parent - return extra_context - - -def patched_get_parent(self, context): - # Patch template returned by extendsnode's get_parent to make sure their - # _render method is just returning the context instead of actually - # rendering stuff. - # In addition, this follows the inheritance chain by looking if the first - # node of the template is an extend node itself. - compiled_template = self._old_get_parent(context) - compiled_template._log = self._log - compiled_template._log_verbosity = self._log_verbosity - compiled_template._old_render = compiled_template._render - compiled_template._render = MethodType(patched_render, compiled_template) - return compiled_template - - -class DjangoParser(object): - def __init__(self, charset): - self.charset = charset - - def parse(self, template_name): - with io.open(template_name, mode='rb') as file: - try: - return Template(file.read().decode(self.charset)) - except template.TemplateSyntaxError as e: - raise TemplateSyntaxError(str(e)) - except template.TemplateDoesNotExist as e: - raise TemplateDoesNotExist(str(e)) - - def process_template(self, template, context): - template._render_firstnode = MethodType(patched_render_firstnode, template) - template._extra_context = template._render_firstnode(context) - - if template._extra_context is None: - # Something is wrong - ignore this template - return False - - return True - - def get_init_context(self, offline_context): - return offline_context - - def process_node(self, template, context, node): - if template._extra_context and node._block_name: - # Give a block context to the node if it was found inside - # a {% block %}. - context['block'] = context.render_context[BLOCK_CONTEXT_KEY].get_block(node._block_name) - - if context['block']: - context['block'].context = context - - def render_nodelist(self, template, context, node): - return node.nodelist.render(context) - - def render_node(self, template, context, node): - return node.render(context, forced=True) - - def get_nodelist(self, node): - # Check if node is an ```if``` switch with true and false branches - if hasattr(node, 'nodelist_true') and hasattr(node, 'nodelist_false'): - return node.nodelist_true + node.nodelist_false - return getattr(node, "nodelist", []) - - def walk_nodes(self, node, block_name=None): - for node in self.get_nodelist(node): - if isinstance(node, BlockNode): - block_name = node.name - if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True): - node._block_name = block_name - yield node - else: - for node in self.walk_nodes(node, block_name=block_name): - yield node +from __future__ import absolute_import +import io +from types import MethodType + +from django import template +from django.template import Template +from django.template.loader_tags import (ExtendsNode, BlockNode, + BLOCK_CONTEXT_KEY) + + +from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist +from compressor.templatetags.compress import CompressorNode + + +def patched_render(self, context): + # 'Fake' _render method that just returns the context instead of + # rendering. It also checks whether the first node is an extend node or + # not, to be able to handle complex inheritance chain. + self._render_firstnode = MethodType(patched_render_firstnode, self) + self._render_firstnode(context) + + # Cleanup, uninstall our _render monkeypatch now that it has been called + self._render = self._old_render + return context + + +def patched_render_firstnode(self, context): + # If this template has a ExtendsNode, we want to find out what + # should be put in render_context to make the {% block ... %} + # tags work. + # + # We can't fully render the base template(s) (we don't have the + # full context vars - only what's necessary to render the compress + # nodes!), therefore we hack the ExtendsNode we found, patching + # its get_parent method so that rendering the ExtendsNode only + # gives us the blocks content without doing any actual rendering. + extra_context = {} + try: + firstnode = self.nodelist[0] + except IndexError: + firstnode = None + if isinstance(firstnode, ExtendsNode): + firstnode._log = self._log + firstnode._log_verbosity = self._log_verbosity + firstnode._old_get_parent = firstnode.get_parent + firstnode.get_parent = MethodType(patched_get_parent, firstnode) + try: + extra_context = firstnode.render(context) + context.render_context = extra_context.render_context + # We aren't rendering {% block %} tags, but we want + # {{ block.super }} inside {% compress %} inside {% block %}s to + # work. Therefore, we need to pop() the last block context for + # each block name, to emulate what would have been done if the + # {% block %} had been fully rendered. + for blockname in firstnode.blocks.keys(): + context.render_context[BLOCK_CONTEXT_KEY].pop(blockname) + except (IOError, template.TemplateSyntaxError, + template.TemplateDoesNotExist): + # That first node we are trying to render might cause more errors + # that we didn't catch when simply creating a Template instance + # above, so we need to catch that (and ignore it, just like above) + # as well. + if self._log_verbosity > 0: + self._log.write("Caught error when rendering extend node from " + "template %s\n" % getattr(self, 'name', self)) + return None + finally: + # Cleanup, uninstall our get_parent monkeypatch now that it has been called + firstnode.get_parent = firstnode._old_get_parent + return extra_context + + +def patched_get_parent(self, context): + # Patch template returned by extendsnode's get_parent to make sure their + # _render method is just returning the context instead of actually + # rendering stuff. + # In addition, this follows the inheritance chain by looking if the first + # node of the template is an extend node itself. + compiled_template = self._old_get_parent(context) + compiled_template._log = self._log + compiled_template._log_verbosity = self._log_verbosity + compiled_template._old_render = compiled_template._render + compiled_template._render = MethodType(patched_render, compiled_template) + return compiled_template + + +class DjangoParser(object): + def __init__(self, charset): + self.charset = charset + + def parse(self, template_name): + with io.open(template_name, mode='rb') as file: + try: + return Template(file.read().decode(self.charset)) + except template.TemplateSyntaxError as e: + raise TemplateSyntaxError(str(e)) + except template.TemplateDoesNotExist as e: + raise TemplateDoesNotExist(str(e)) + + def process_template(self, template, context): + template._render_firstnode = MethodType(patched_render_firstnode, template) + template._extra_context = template._render_firstnode(context) + + if template._extra_context is None: + # Something is wrong - ignore this template + return False + + return True + + def get_init_context(self, offline_context): + return offline_context + + def process_node(self, template, context, node): + if template._extra_context and node._block_name: + # Give a block context to the node if it was found inside + # a {% block %}. + context['block'] = context.render_context[BLOCK_CONTEXT_KEY].get_block(node._block_name) + + if context['block']: + context['block'].context = context + + def render_nodelist(self, template, context, node): + return node.nodelist.render(context) + + def render_node(self, template, context, node): + return node.render(context, forced=True) + + def get_nodelist(self, node): + # Check if node is an ```if``` switch with true and false branches + if hasattr(node, 'nodelist_true') and hasattr(node, 'nodelist_false'): + return node.nodelist_true + node.nodelist_false + return getattr(node, "nodelist", []) + + def walk_nodes(self, node, block_name=None): + for node in self.get_nodelist(node): + if isinstance(node, BlockNode): + block_name = node.name + if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True): + node._block_name = block_name + yield node + else: + for node in self.walk_nodes(node, block_name=block_name): + yield node diff --git a/compressor/offline/jinja2.py b/compressor/offline/jinja2.py index 8ac8989..feee818 100644 --- a/compressor/offline/jinja2.py +++ b/compressor/offline/jinja2.py @@ -1,125 +1,125 @@ -from __future__ import absolute_import # noqa -import io - -import jinja2 -import jinja2.ext -from jinja2 import nodes -from jinja2.ext import Extension -from jinja2.nodes import CallBlock, Call, ExtensionAttribute - -from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist - - -def flatten_context(context): - if hasattr(context, 'dicts'): - context_dict = {} - - for d in context.dicts: - context_dict.update(d) - - return context_dict - - return context - - -class SpacelessExtension(Extension): - """ - Functional "spaceless" extension equivalent to Django's. - - See: https://github.com/django/django/blob/master/django/template/defaulttags.py - """ - - tags = set(['spaceless']) - - def parse(self, parser): - lineno = next(parser.stream).lineno - body = parser.parse_statements(['name:endspaceless'], drop_needle=True) - - return nodes.CallBlock(self.call_method('_spaceless', []), - [], [], body).set_lineno(lineno) - - def _spaceless(self, caller): - from django.utils.html import strip_spaces_between_tags - - return strip_spaces_between_tags(caller().strip()) - - -def url_for(mod, filename): - """ - Incomplete emulation of Flask's url_for. - """ - from django.contrib.staticfiles.templatetags import staticfiles - - if mod == "static": - return staticfiles.static(filename) - - return "" - - -class Jinja2Parser(object): - COMPRESSOR_ID = 'compressor.contrib.jinja2ext.CompressorExtension' - - def __init__(self, charset, env): - self.charset = charset - self.env = env - - def parse(self, template_name): - with io.open(template_name, mode='rb') as file: - try: - template = self.env.parse(file.read().decode(self.charset)) - except jinja2.TemplateSyntaxError as e: - raise TemplateSyntaxError(str(e)) - except jinja2.TemplateNotFound as e: - raise TemplateDoesNotExist(str(e)) - - return template - - def process_template(self, template, context): - return True - - def get_init_context(self, offline_context): - # Don't need to add filters and tests to the context, as Jinja2 will - # automatically look for them in self.env.filters and self.env.tests. - # This is tested by test_complex and test_templatetag. - - # Allow offline context to override the globals. - context = self.env.globals.copy() - context.update(offline_context) - - return context - - def process_node(self, template, context, node): - pass - - def _render_nodes(self, template, context, nodes): - compiled_node = self.env.compile(jinja2.nodes.Template(nodes)) - template = jinja2.Template.from_code(self.env, compiled_node, {}) - flat_context = flatten_context(context) - - return template.render(flat_context) - - def render_nodelist(self, template, context, node): - return self._render_nodes(template, context, node.body) - - def render_node(self, template, context, node): - return self._render_nodes(template, context, [node]) - - def get_nodelist(self, node): - body = getattr(node, "body", getattr(node, "nodes", [])) - - if isinstance(node, jinja2.nodes.If): - return body + node.else_ - - return body - - def walk_nodes(self, node, block_name=None): - for node in self.get_nodelist(node): - if (isinstance(node, CallBlock) and - isinstance(node.call, Call) and - isinstance(node.call.node, ExtensionAttribute) and - node.call.node.identifier == self.COMPRESSOR_ID): - node.call.node.name = '_compress_forced' - yield node - else: - for node in self.walk_nodes(node, block_name=block_name): - yield node +from __future__ import absolute_import +import io + +import jinja2 +import jinja2.ext +from jinja2 import nodes +from jinja2.ext import Extension +from jinja2.nodes import CallBlock, Call, ExtensionAttribute + +from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist + + +def flatten_context(context): + if hasattr(context, 'dicts'): + context_dict = {} + + for d in context.dicts: + context_dict.update(d) + + return context_dict + + return context + + +class SpacelessExtension(Extension): + """ + Functional "spaceless" extension equivalent to Django's. + + See: https://github.com/django/django/blob/master/django/template/defaulttags.py + """ + + tags = set(['spaceless']) + + def parse(self, parser): + lineno = next(parser.stream).lineno + body = parser.parse_statements(['name:endspaceless'], drop_needle=True) + + return nodes.CallBlock(self.call_method('_spaceless', []), + [], [], body).set_lineno(lineno) + + def _spaceless(self, caller): + from django.utils.html import strip_spaces_between_tags + + return strip_spaces_between_tags(caller().strip()) + + +def url_for(mod, filename): + """ + Incomplete emulation of Flask's url_for. + """ + from django.contrib.staticfiles.templatetags import staticfiles + + if mod == "static": + return staticfiles.static(filename) + + return "" + + +class Jinja2Parser(object): + COMPRESSOR_ID = 'compressor.contrib.jinja2ext.CompressorExtension' + + def __init__(self, charset, env): + self.charset = charset + self.env = env + + def parse(self, template_name): + with io.open(template_name, mode='rb') as file: + try: + template = self.env.parse(file.read().decode(self.charset)) + except jinja2.TemplateSyntaxError as e: + raise TemplateSyntaxError(str(e)) + except jinja2.TemplateNotFound as e: + raise TemplateDoesNotExist(str(e)) + + return template + + def process_template(self, template, context): + return True + + def get_init_context(self, offline_context): + # Don't need to add filters and tests to the context, as Jinja2 will + # automatically look for them in self.env.filters and self.env.tests. + # This is tested by test_complex and test_templatetag. + + # Allow offline context to override the globals. + context = self.env.globals.copy() + context.update(offline_context) + + return context + + def process_node(self, template, context, node): + pass + + def _render_nodes(self, template, context, nodes): + compiled_node = self.env.compile(jinja2.nodes.Template(nodes)) + template = jinja2.Template.from_code(self.env, compiled_node, {}) + flat_context = flatten_context(context) + + return template.render(flat_context) + + def render_nodelist(self, template, context, node): + return self._render_nodes(template, context, node.body) + + def render_node(self, template, context, node): + return self._render_nodes(template, context, [node]) + + def get_nodelist(self, node): + body = getattr(node, "body", getattr(node, "nodes", [])) + + if isinstance(node, jinja2.nodes.If): + return body + node.else_ + + return body + + def walk_nodes(self, node, block_name=None): + for node in self.get_nodelist(node): + if (isinstance(node, CallBlock) and + isinstance(node.call, Call) and + isinstance(node.call.node, ExtensionAttribute) and + node.call.node.identifier == self.COMPRESSOR_ID): + node.call.node.name = '_compress_forced' + yield node + else: + for node in self.walk_nodes(node, block_name=block_name): + yield node diff --git a/tox.ini b/tox.ini index 08ce2aa..1aa5e81 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,16 @@ three = BeautifulSoup4 jingo coffin +three_two = + flake8 + coverage + html5lib + mock + jinja2==2.6 + lxml + BeautifulSoup4 + jingo + coffin [tox] envlist = @@ -54,7 +64,7 @@ deps = basepython = python3.2 deps = Django>=1.6,<1.7 - {[deps]three} + {[deps]three_two} [testenv:py27-1.6.X] basepython = python2.7 @@ -80,7 +90,7 @@ basepython = python3.2 deps = Django>=1.5,<1.6 django-discover-runner - {[deps]three} + {[deps]three_two} [testenv:py27-1.5.X] basepython = python2.7