/usr/share/pyshared/webassets/ext/jinja2.py is in python-webassets 3:0.9-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | from __future__ import absolute_import
import warnings
import jinja2
from jinja2.ext import Extension
from jinja2 import nodes
from webassets import Bundle
from webassets.loaders import GlobLoader, LoaderError
from webassets.exceptions import ImminentDeprecationWarning
__all__ = ('assets', 'Jinja2Loader',)
class AssetsExtension(Extension):
"""
As opposed to the Django tag, this tag is slightly more capable due
to the expressive powers inherited from Jinja. For example:
{% assets "src1.js", "src2.js", get_src3(),
filter=("jsmin", "gzip"), output=get_output() %}
{% endassets %}
"""
tags = set(['assets'])
BundleClass = Bundle # Helpful for mocking during tests.
def __init__(self, environment):
super(AssetsExtension, self).__init__(environment)
# Add the defaults to the environment
environment.extend(
assets_environment=None,
)
def parse(self, parser):
lineno = next(parser.stream).lineno
files = []
output = nodes.Const(None)
filters = nodes.Const(None)
dbg = nodes.Const(None)
depends = nodes.Const(None)
# Parse the arguments
first = True
while parser.stream.current.type != 'block_end':
if not first:
parser.stream.expect('comma')
first = False
# Lookahead to see if this is an assignment (an option)
if parser.stream.current.test('name') and parser.stream.look().test('assign'):
name = next(parser.stream).value
parser.stream.skip()
value = parser.parse_expression()
if name == 'filters':
filters = value
elif name == 'filter':
filters = value
warnings.warn('The "filter" option of the {%% assets %%} '
'template tag has been renamed to '
'"filters" for consistency reasons '
'(line %s).' % lineno,
ImminentDeprecationWarning)
elif name == 'output':
output = value
elif name == 'debug':
dbg = value
elif name == 'depends':
depends = value
else:
parser.fail('Invalid keyword argument: %s' % name)
# Otherwise assume a source file is given, which may be any
# expression, except note that strings are handled separately above.
else:
files.append(parser.parse_expression())
# Parse the contents of this tag
body = parser.parse_statements(['name:endassets'], drop_needle=True)
# We want to make some values available to the body of our tag.
# Specifically, the file url(s) (ASSET_URL), and any extra dict set in
# the bundle (EXTRA).
#
# A short interlope: I would have preferred to make the values of the
# extra dict available directly. Unfortunately, the way Jinja2 does
# things makes this problematic. I'll explain.
#
# Jinja2 generates Python code from it's AST which it then executes.
# So the way extensions implement making custom variables available to
# a block of code is by generating a ``CallBlock``, which essentially
# wraps our child nodes in a Python function. The arguments of this
# function are the values that are available to our tag contents.
#
# But we need to generate this ``CallBlock`` now, during parsing, and
# right now we don't know the actual ``Bundle.extra`` values yet. We
# only resolve the bundle during rendering!
#
# This would easily be solved if Jinja2 where to allow extensions to
# scope it's context, which is a dict of values that templates can
# access, just like in Django (you might see on occasion
# ``context.resolve('foo')`` calls in Jinja2's generated code).
# However, it seems the context is essentially only for the initial
# set of data passed to render(). There are some statements by Armin
# that this might change at some point, but I've run into this problem
# before, and I'm not holding my breath.
#
# I **really** did try to get around this, including crazy things like
# inserting custom Python code by patching the tag child nodes::
#
# rv = object.__new__(nodes.InternalName)
# # l_EXTRA is the argument we defined for the CallBlock/Macro
# # Letting Jinja define l_kwargs is also possible
# nodes.Node.__init__(rv, '; context.vars.update(l_EXTRA)',
# lineno=lineno)
# # Scope required to ensure our code on top
# body = [rv, nodes.Scope(body)]
#
# This custom code would run at the top of the function in which the
# CallBlock node would wrap the code generated from our tag's child
# nodes. Note that it actually does works, but doesn't clear the values
# at the end of the scope).
#
# If it is possible to do this, it certainly isn't reasonable/
#
# There is of course another option altogether: Simple resolve the tag
# definition to a bundle right here and now, thus get access to the
# extra dict, make all values arguments to the CallBlock (Limited to
# 255 arguments to a Python function!). And while that would work fine
# in 99% of cases, it wouldn't be correct. The compiled template could
# be cached and used with different bundles/environments, and this
# would require the bundle to resolve at parse time, and hardcode it's
# extra values.
#
# Interlope end.
#
# Summary: We have to be satisfied with a single EXTRA variable.
args = [nodes.Name('ASSET_URL', 'store'),
nodes.Name('EXTRA', 'store')]
# Return a ``CallBlock``, which means Jinja2 will call a Python method
# of ours when the tag needs to be rendered. That method can then
# render the template body.
call = self.call_method(
# Note: Changing the args here requires updating ``Jinja2Loader``
'_render_assets', args=[filters, output, dbg, depends, nodes.List(files)])
call_block = nodes.CallBlock(call, args, [], body)
call_block.set_lineno(lineno)
return call_block
@classmethod
def resolve_contents(cls, contents, env):
"""Resolve bundle names."""
result = []
for f in contents:
try:
result.append(env[f])
except KeyError:
result.append(f)
return result
def _render_assets(self, filter, output, dbg, depends, files, caller=None):
env = self.environment.assets_environment
if env is None:
raise RuntimeError('No assets environment configured in '+
'Jinja2 environment')
# Construct a bundle with the given options
bundle_kwargs = {
'output': output,
'filters': filter,
'debug': dbg,
'depends': depends,
}
bundle = self.BundleClass(
*self.resolve_contents(files, env), **bundle_kwargs)
# Retrieve urls (this may or may not cause a build)
urls = bundle.urls(env=env)
# For each url, execute the content of this template tag (represented
# by the macro ```caller`` given to use by Jinja2).
result = u""
for url in urls:
result += caller(url, bundle.extra)
return result
assets = AssetsExtension # nicer import name
class Jinja2Loader(GlobLoader):
"""Parse all the Jinja2 templates in the given directory, try to
find bundles in active use.
Try all the given environments to parse the template, until we
succeed.
"""
def __init__(self, assets_env, directories, jinja2_envs, charset='utf8'):
self.asset_env = assets_env
self.directories = directories
self.jinja2_envs = jinja2_envs
self.charset = charset
def load_bundles(self):
bundles = []
for template_dir in self.directories:
for filename in self.glob_files((template_dir, '*.html')):
bundles.extend(self.with_file(filename, self._parse) or [])
return bundles
def _parse(self, filename, contents):
for i, env in enumerate(self.jinja2_envs):
try:
t = env.parse(contents.decode(self.charset))
except jinja2.exceptions.TemplateSyntaxError as e:
#print ('jinja parser (env %d) failed: %s'% (i, e))
pass
else:
result = []
def _recurse_node(node_to_search):
for node in node_to_search.iter_child_nodes():
if isinstance(node, jinja2.nodes.Call):
if isinstance(node.node, jinja2.nodes.ExtensionAttribute)\
and node.node.identifier == AssetsExtension.identifier:
filter, output, dbg, depends, files = node.args
bundle = Bundle(
*AssetsExtension.resolve_contents(files.as_const(), self.asset_env),
**{
'output': output.as_const(),
'depends': depends.as_const(),
'filters': filter.as_const()})
result.append(bundle)
else:
_recurse_node(node)
for node in t.iter_child_nodes():
_recurse_node(node)
return result
else:
raise LoaderError('Jinja parser failed on %s, tried %d environments' % (
filename, len(self.jinja2_envs)))
return False
|