Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Support an extra dict for bundles.

The values from this dict are made available in the template tags. Can be used
for things like a CSS media target.

Closes #120.
  • Loading branch information...
commit e50c61b4e72bc939e293acb2e6aadc7bf580ab72 1 parent 203cb3b
@miracle2k authored
View
24 src/webassets/bundle.py
@@ -60,10 +60,10 @@ def __init__(self, *contents, **options):
self.debug = options.pop('debug', None)
self.depends = options.pop('depends', [])
self.version = options.pop('version', [])
+ self.extra = options.pop('extra', {})
if options:
raise TypeError("got unexpected keyword argument '%s'" %
options.keys()[0])
- self.extra_data = {}
def __repr__(self):
return "<%s output=%s, filters=%s, contents=%s>" % (
@@ -100,6 +100,23 @@ def _set_contents(self, value):
self._resolved_contents = None
contents = property(_get_contents, _set_contents)
+ def _get_extra(self):
+ if not self._extra and not has_files(self):
+ # If this bundle has no extra values of it's own, and only
+ # wraps child bundles, use the extra values of those.
+ result = {}
+ for bundle in self.contents:
+ result.update(bundle.extra)
+ return result
+ else:
+ return self._extra
+ def _set_extra(self, value):
+ self._extra = value
+ extra = property(_get_extra, _set_extra, doc="""A custom user dict of
+ extra values attached to this bundle. Those will be available in
+ template tags, and can be used to attach things like a CSS
+ 'media' value.""")
+
def resolve_contents(self, env=None, force=False):
"""Convert bundle contents into something that can be easily processed.
@@ -264,8 +281,7 @@ def is_container(self):
It must not contain any files of its own, and must have an empty
``output`` attribute.
"""
- has_files = any([c for c in self.contents if not isinstance(c, Bundle)])
- return not has_files and not self.output
+ return not has_files(self) and not self.output
def _get_env(self, env):
# Note how bool(env) can be False, due to __len__.
@@ -734,3 +750,5 @@ def _effective_debug_level(env, bundle, extra_filters=None, default=None):
return default
+has_files = lambda bundle: \
+ any([c for c in bundle.contents if not isinstance(c, Bundle)])
View
113 src/webassets/ext/jinja2.py
@@ -29,7 +29,7 @@ class AssetsExtension(Extension):
def __init__(self, environment):
super(AssetsExtension, self).__init__(environment)
- # add the defaults to the environment
+ # Add the defaults to the environment
environment.extend(
assets_environment=None,
)
@@ -42,14 +42,14 @@ def parse(self, parser):
filters = nodes.Const(None)
dbg = nodes.Const(None)
- # parse the arguments
+ # 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)
+ # Lookahead to see if this is an assignment (an option)
if parser.stream.current.test('name') and parser.stream.look().test('assign'):
name = parser.stream.next().value
parser.stream.skip()
@@ -69,23 +69,85 @@ def parse(self, parser):
dbg = 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
+ # 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, and return a block
+ # Parse the contents of this tag
body = parser.parse_statements(['name:endassets'], drop_needle=True)
- return nodes.CallBlock(
- self.call_method('_render_assets',
- args=[filters, output, dbg, nodes.List(files)]),
- [nodes.Name('ASSET_URL', 'store')], [], body).\
- set_lineno(lineno)
+ # 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(
+ '_render_assets', args=[filters, output, dbg, nodes.List(files)])
+ call_block = nodes.CallBlock(call, args, [], body)
+ call_block.set_lineno(lineno)
+ return call_block
@classmethod
- def resolve_contents(self, contents, env):
+ def resolve_contents(cls, contents, env):
"""Resolve bundle names."""
result = []
for f in contents:
@@ -101,18 +163,23 @@ def _render_assets(self, filter, output, dbg, files, caller=None):
raise RuntimeError('No assets environment configured in '+
'Jinja2 environment')
- result = u""
- kwargs = {'output': output,
- 'filters': filter,
- }
+ # Construct a bundle with the given options
+ bundle_kwargs = {
+ 'output': output,
+ 'filters': filter,
+ 'debug': dbg
+ }
+ bundle = self.BundleClass(
+ *self.resolve_contents(files, env), **bundle_kwargs)
- if dbg != None:
- kwargs['debug'] = dbg
+ # Retrieve urls (this may or may not cause a build)
+ urls = bundle.urls(env=env)
- urls = self.BundleClass(*self.resolve_contents(files, env),
- **kwargs).urls(env=env)
- for f in urls:
- result += caller(f)
+ # 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
View
12 tests/test_bundle.py
@@ -21,7 +21,7 @@
class TestBundleConfig(TempEnvironmentHelper):
- def test_init_kwargs(self):
+ def test_unknown_init_kwargs(self):
"""We used to silently ignore unsupported kwargs, which can make
mistakes harder to track down; in particular "filters" vs "filter"
is confusing. Now we raise an error.
@@ -33,6 +33,16 @@ def test_init_kwargs(self):
else:
raise Exception('Expected TypeError not raised')
+ def test_init_extra_kwarg(self):
+ """Bundles may be given an ``extra`` dictionary."""
+ assert Bundle().extra == {}
+ assert Bundle(extra={'foo': 'bar'}).extra == {'foo': 'bar'}
+
+ # Nested extra values
+ assert Bundle(Bundle(extra={'foo': 'bar'}),
+ Bundle(extra={'baz': 'qux'})).extra == {
+ 'foo': 'bar', 'baz': 'qux'}
+
def test_filter_assign(self):
"""Test the different ways we can assign filters to the bundle.
"""
View
12 tests/test_ext/test_jinja2.py
@@ -49,9 +49,10 @@ def teardown(self):
AssetsExtension.BundleClass = self._old_bundle_class
del self._old_bundle_class
- def render_template(self, args, ctx={}):
+ def render_template(self, args, ctx=None):
return self.jinja_env.from_string(
- '{% assets '+args+' %}{{ ASSET_URL }};{% endassets %}').render(ctx)
+ '{% assets '+args+' %}{{ ASSET_URL }};{% endassets %}')\
+ .render(ctx or {})
def test_reference_bundles(self):
self.render_template('"foo_bundle", "bar_bundle"')
@@ -70,7 +71,7 @@ def test_with_vars(self):
assert self.the_bundle.contents == (self.foo_bundle, 'a_file',)
def test_output_urls(self):
- """Ensure the tag correcly spits out the urls the bundle returns.
+ """Ensure the tag correctly spits out the urls the bundle returns.
"""
self.BundleClass.urls_to_fake = ['foo', 'bar']
assert self.render_template('"file1" "file2" "file3"') == 'foo;bar;'
@@ -80,3 +81,8 @@ def test_debug_on_tag(self):
'{% assets debug="True", "debug1.txt" %}{{ ASSET_URL }};{% endassets %}').render({})
assert self.the_bundle.dbg == 'True'
+ def test_extra_values(self):
+ self.foo_bundle.extra = {'moo': 42}
+ output = self.jinja_env.from_string(
+ '{% assets "foo_bundle" %}{{ EXTRA.moo }}{% endassets %}').render()
+ assert output == '42'
Please sign in to comment.
Something went wrong with that request. Please try again.