From ee686ad7a2d4e8a4d5c53df2fcb14a82e3cbdf4d Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 15:00:26 -0500 Subject: [PATCH 01/19] added initial implementation of bundle rendering --- src/webassets/bundle.py | 20 ++++++ src/webassets/env.py | 12 ++++ src/webassets/renderer.py | 131 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/webassets/renderer.py diff --git a/src/webassets/bundle.py b/src/webassets/bundle.py index 2cf66e10..8daed971 100644 --- a/src/webassets/bundle.py +++ b/src/webassets/bundle.py @@ -10,6 +10,7 @@ from .updater import SKIP_CACHE from .exceptions import BundleError, BuildError from .utils import cmp_debug_levels, urlparse +from .renderer import BundleRenderer __all__ = ('Bundle', 'get_all_bundle_files',) @@ -56,6 +57,7 @@ def __init__(self, *contents, **options): self.depends = options.pop('depends', []) self.version = options.pop('version', []) self.extra = options.pop('extra', {}) + self.renderer = options.pop('renderer', None) if options: raise TypeError("got unexpected keyword argument '%s'" % list(options.keys())[0]) @@ -685,6 +687,24 @@ def urls(self, env=None, *args, **kwargs): urls.extend(bundle._urls(env, extra_filters, *args, **kwargs)) return urls + def renderers(self, env=None, inline=None, *args, **kwargs): + ''' + Returns a generator of renderers for this bundle. + + This operates almost identically to :meth:`.urls()`, so this + may return one BundleRenderer (usually in production) or + multiple BundleRenderers (usually in debug mode). + + If `inline` is specified (i.e. not ``None``), then this will + be specified as the default inlining mode of each + renderer. Note, however, that this can be overriden then on a + per-renderer basis. + ''' + env = self._get_env(env) + for bundle, extra_filters in self.iterbuild(env): + for url in bundle._urls(env, extra_filters, *args, **kwargs): + yield BundleRenderer(env, bundle, url, inline) + def pull_external(env, filename): """Helper which will pull ``filename`` into diff --git a/src/webassets/env.py b/src/webassets/env.py index 7872283a..c39f915f 100644 --- a/src/webassets/env.py +++ b/src/webassets/env.py @@ -15,6 +15,7 @@ from .version import get_versioner, get_manifest from .updater import get_updater from .utils import urlparse +from .renderer import prepare_renderer __all__ = ('Environment', 'RegisterError') @@ -402,6 +403,7 @@ def __init__(self, **config): BundleRegistry.__init__(self) self._config = self.config_storage_class(self) self.resolver = self.resolver_class(self) + self.renderers = dict() # directory, url currently do not have default values # @@ -695,6 +697,16 @@ def _get_url_mapping(self): modifying this setting directly. """) + def register_renderer(self, name, renderer, inline_renderer=None): + ''' + Registers renderers to be used only for renderings done + within the context of this environment. + + For details, and how to register renderers globally, see + :func:`webassets.renderer.register_global_renderer()`. + ''' + self.renderers[name] = prepare_renderer( + name, renderer, inline_renderer) class DictConfigStorage(ConfigStorage): """Using a lower-case dict for configuration values. diff --git a/src/webassets/renderer.py b/src/webassets/renderer.py new file mode 100644 index 00000000..fd4514bf --- /dev/null +++ b/src/webassets/renderer.py @@ -0,0 +1,131 @@ +from webassets import six + +__all__ = 'register_global_renderer', 'BundleRenderer' + +global_renderers = dict() + + +def prepare_renderer(name, renderer, inline_renderer=None): + ''' + For internal use only -- prepares the renderers to be + stored in the internal lookup tables. + ''' + if isinstance(renderer, six.string_types): + renderer = make_template_renderer(renderer) + if isinstance(inline_renderer, six.string_types): + inline_renderer = make_template_renderer(inline_renderer) + if inline_renderer is None: + inline_renderer = renderer + return (renderer, inline_renderer) + + +def register_global_renderer(name, renderer, inline_renderer=None): + ''' + Register the `renderer` under the name `name` globally. If + `inline_renderer` is ``None``, it will default to the using the + non-inline `renderer`. + + Note that, as always, using globals is usually not a good. It is + usually a much better idea to register the renderers within the + specific :class:`webassets.env.Environment` context that they will + be used (the Environment class has a `register_renderer()` method + that is perfect for that). + + Renderers can be either a callable or a template string. If they + are a string, they will be converted to callables by + :func:`.make_template_renderer`. + + Renderers are called with the following keyword parameters: + + * `bundle`: the Bundle object being rendered. + * `type`: the renderer type, i.e. the `name`. + * `url`: the currently being rendered asset URL. + * `content`: the asset content (for inline renderings only). + ''' + global_renderers[name] = prepare_renderer( + name, renderer, inline_renderer) + + +def make_template_renderer(template): + ''' + Returns a callable renderer from the provided string `template`. + ''' + return str(template).format + + +# register a default renderer that simply outputs the data as-is. not +# particularly useful, but at least that way it is visible, and +# developers will hopefully realize that it is a mis-configuration... + +register_global_renderer(None, '{url}', '{content}') + +# register some globally useful renderers for css and javascript. + +# todo: technically, the `content` should be CDATA-escaped here, but +# that is perhaps a little too much? afterall, the "]]>" sequence in +# css and javascript is pretty rare, i think (i've never seen it) + +register_global_renderer( + 'css', + '', + '') + +register_global_renderer( + 'js', + '', + '') + +# todo: register renderers for less. the complication with that, is +# that it is sensitive to whether or not the less is compiled +# client-side or server-side... + +# # in server-side compiled mode +# register_global_renderer( +# 'less', +# '', +# '') +# # in client-side compiled mode +# register_global_renderer( +# 'less', +# '', +# '') + + +class BundleRenderer(object): + + def __init__(self, env, bundle, url, inline): + self.env = env + self.bundle = bundle + self.url = url + self.inline = inline + + def _get_renderer(self, name): + rend = None + if hasattr(self.env, 'renderers'): + rend = self.env.renderers.get(name, None) + if rend is None: + rend = global_renderers.get(name, None) + if rend is None: + raise ValueError('Cannot find renderer "%s"' % (str(name),)) + return rend + + def render(self, inline=None): + if inline or ( inline is None and self.inline ): + return self._render_inline() + return self._render_ref() + + def _render_ref(self): + typ = self.bundle.renderer + rend = self._get_renderer(typ)[0] + return rend( + type=typ, bundle=self.bundle, url=self.url, env=self.env) + + def _render_inline(self): + typ = self.bundle.renderer + rend = self._get_renderer(typ)[1] + buf = six.StringIO() + self.bundle.build(force=True, output=buf, env=self.env) + buf = buf.getvalue() + return rend( + type=typ, bundle=self.bundle, url=self.url, content=buf, env=self.env) + From 511a1e989d14fe298507cdb36b6ff78b85688d6d Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 15:59:11 -0500 Subject: [PATCH 02/19] added more pydocs --- src/webassets/renderer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/webassets/renderer.py b/src/webassets/renderer.py index fd4514bf..25298a30 100644 --- a/src/webassets/renderer.py +++ b/src/webassets/renderer.py @@ -49,6 +49,11 @@ def register_global_renderer(name, renderer, inline_renderer=None): def make_template_renderer(template): ''' Returns a callable renderer from the provided string `template`. + The template is assumed to be in `str.format syntax + `_, + which has access to all parameters specified in + :func:`.register_global_renderer` (of which `url` and `content` + are most interesting). ''' return str(template).format From 6ba68edeadf208ea8bcc1315219c65fbdfb648f6 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 15:59:31 -0500 Subject: [PATCH 03/19] added renderer unit tests --- tests/test_renderer.py | 75 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/test_renderer.py diff --git a/tests/test_renderer.py b/tests/test_renderer.py new file mode 100644 index 00000000..774e2e6d --- /dev/null +++ b/tests/test_renderer.py @@ -0,0 +1,75 @@ +''' +Tests for the "rendering" aspect of :class:`Bundle` via +:class:`BundleRenderer`. +''' + + +import unittest +import sys + +from tests.test_bundle_urls import BaseUrlsTester + +class TestRenderer(BaseUrlsTester, unittest.TestCase): + + def setUp(self): + self.setup() + class MockBundleWithContent(self.MockBundle): + def _build(self, *a, **kw): + super(MockBundleWithContent, self)._build(*a, **kw) + output = kw.get('output') + if output: + output.write(self.extra.get('data', 'MockData')) + self.MockBundle = MockBundleWithContent + + def test_missing_renderer(self): + b = self.MockBundle('a', output='out', renderer='no-such-renderer') + with self.assertRaises(ValueError) as cm: + [r.render() for r in b.renderers()] + self.assertEqual( + str(cm.exception), 'Cannot find renderer "no-such-renderer"') + + def test_render_reference_css(self): + b = self.MockBundle('a.css', output='out.css', renderer='css') + self.assertEqual( + [r.render() for r in b.renderers()], + ['']) + + def test_render_reference_js(self): + b = self.MockBundle('a.js', output='out.js', renderer='js') + self.assertEqual( + [r.render() for r in b.renderers()], + ['']) + + def test_render_reference_custom(self): + self.env.register_renderer('custom', '[REF: {url}]', '[RAW: {url}]') + b = self.MockBundle('a.cstm', output='out.cstm', renderer='custom') + self.assertEqual( + [r.render() for r in b.renderers()], + ['[REF: /out.cstm]']) + + def test_render_inline_css(self): + b = self.MockBundle('a.css', output='out.css', renderer='css', + extra=dict(data='some-css')) + self.assertEqual( + [r.render(inline=True) for r in b.renderers()], + ['''\ +''']) + + def test_render_inline_js(self): + b = self.MockBundle('a.js', output='out.js', renderer='js', + extra=dict(data='some-js')) + self.assertEqual( + [r.render(inline=True) for r in b.renderers()], + ['''\ +''']) + + def test_render_defaultinline_custom(self): + self.env.register_renderer('custom', '[REF: {url}]', '[RAW: {url}]') + b = self.MockBundle('a.cstm', output='out.cstm', renderer='custom') + self.assertEqual( + [r.render() for r in b.renderers(inline=True)], + ['[RAW: /out.cstm]']) From ef3505e405cf235267963e3436bacbb549af69a9 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 16:49:33 -0500 Subject: [PATCH 04/19] added documentation about bundle rendering --- AUTHORS | 1 + docs/bundles.rst | 21 ++++++++ docs/renderer.rst | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 docs/renderer.rst diff --git a/AUTHORS b/AUTHORS index 6b670b04..fb3d2de2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ Matt Haggard Matt Hanger Mattias Wong <0x1998@gmail.com> Maxim Bodyansky +metagriffin Michael Elsdörfer Michael Mior Michael Su diff --git a/docs/bundles.rst b/docs/bundles.rst index 7200585e..72c95dc1 100644 --- a/docs/bundles.rst +++ b/docs/bundles.rst @@ -43,6 +43,11 @@ arguments: .. warning:: Currently, using ``depends`` disables caching for a bundle. +* ``renderer`` - Name of the renderer used to render this bundle's + assets in context. Note that the renderer must be either one of the + webassets-provided renderers (currently ``"css"`` and ``"js"``) or a + renderer registered via ``Environment.register_renderer()`` or + ``register_global_renderer()`` (see :doc:`/renderer` for more info). Nested bundles -------------- @@ -155,6 +160,22 @@ which allows you do something like this: {% assets filters="cssmin,datauri", output="gen/packed.css", "common/jquery.css", "site/base.css", "site/widgets.css" %} ... +You can also delegate contextual rendering of assets to webassets +(using Mako and pyramid-webassets environment): + +.. code-block:: mako + + % for asset in request.webassets_env['assets'].renderers(): + ${asset.render()|n} + % endfor + +which will correctly render references to the assets via CSS "" +and JavaScript " + + ... + + + +Renderer Registration +===================== + +Webassets provides default renderers for CSS (named ``"css"``) and for +JavaScript (named ``"js"``). If you need to add more renderers, or +change the default rendering, this can be done via renderer +registration. + +A renderer is either a string in `str.format syntax +`_, +or a callable that receives the following keyword arguments: + +* `type`: the renderer type, i.e. the `name`. +* `bundle`: the Bundle object being rendered. +* `url`: the currently being rendered asset URL. +* `content`: the asset content (for inline renderings only). + +An example custom ``less`` renderer that can handle less being +compiled either client-side or server-side (the environment will +indicate this via the `debug` flag): + +.. code-block:: python + + def my_less_renderer(type, bundle, url): + dolessc = not bundle.env.debug or bundle.env.config.get('less_run_in_debug') + rel = 'stylesheet' if dolessc else 'stylesheet/less' + return ''.format(rel=rel, url=url) + + def my_less_inline_renderer(type, bundle, url, content): + dolessc = not bundle.env.debug or bundle.env.config.get('less_run_in_debug') + type = 'text/css' if dolessc else 'text/less' + return ''.format(type=type, content=content) + + +You can register renderers in particular ``Environment`` objects +(recommended) or you can also register renderers globally (only +recommended in rare situations). + +To register the renderer in an environment: + +.. code-block:: python + + env.register_renderer('less', my_less_renderer, my_less_inline_renderer) + + +And to register the renderer globally (usually not recommended): + +.. code-block:: python + + from webassets.renderer import register_global_renderer + register_global_renderer('less', my_less_renderer, my_less_inline_renderer) + + +Note that in the above examples, we registered both a referencing +renderer as well as an inline renderer. If we had specified only the +former, then the inline renderer would default to that one as well. From b4e901e65ca6088e482f71f8d1da4c7f99991422 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 17:03:15 -0500 Subject: [PATCH 05/19] removed unnecessary pyramid references --- docs/bundles.rst | 6 +++--- docs/renderer.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/bundles.rst b/docs/bundles.rst index 72c95dc1..e6f47d93 100644 --- a/docs/bundles.rst +++ b/docs/bundles.rst @@ -160,12 +160,12 @@ which allows you do something like this: {% assets filters="cssmin,datauri", output="gen/packed.css", "common/jquery.css", "site/base.css", "site/widgets.css" %} ... -You can also delegate contextual rendering of assets to webassets -(using Mako and pyramid-webassets environment): +You can also delegate contextual rendering of assets references to +webassets (here, using Mako): .. code-block:: mako - % for asset in request.webassets_env['assets'].renderers(): + % for asset in my_webassets_env['assets'].renderers(): ${asset.render()|n} % endfor diff --git a/docs/renderer.rst b/docs/renderer.rst index 07b0852d..fd1fe466 100644 --- a/docs/renderer.rst +++ b/docs/renderer.rst @@ -56,7 +56,7 @@ Then, in a Mako template: - % for asset in request.webassets_env['app'].renderers(): + % for asset in my_webassets_env['app'].renderers(): ${asset.render()|n} % endfor From 5e009ffe18dd267e073e90a2b183a74e100bfc9b Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 17:04:38 -0500 Subject: [PATCH 06/19] small typo --- docs/bundles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bundles.rst b/docs/bundles.rst index e6f47d93..81c98510 100644 --- a/docs/bundles.rst +++ b/docs/bundles.rst @@ -160,7 +160,7 @@ which allows you do something like this: {% assets filters="cssmin,datauri", output="gen/packed.css", "common/jquery.css", "site/base.css", "site/widgets.css" %} ... -You can also delegate contextual rendering of assets references to +You can also delegate contextual rendering of asset references to webassets (here, using Mako): .. code-block:: mako From eff8f5a5e87605b8026ebdaddbe8d0f2aa28df28 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 22 Dec 2013 17:22:00 -0500 Subject: [PATCH 07/19] another typo --- src/webassets/bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webassets/bundle.py b/src/webassets/bundle.py index 8daed971..1a7ee786 100644 --- a/src/webassets/bundle.py +++ b/src/webassets/bundle.py @@ -697,7 +697,7 @@ def renderers(self, env=None, inline=None, *args, **kwargs): If `inline` is specified (i.e. not ``None``), then this will be specified as the default inlining mode of each - renderer. Note, however, that this can be overriden then on a + renderer. Note, however, that this can be overridden then on a per-renderer basis. ''' env = self._get_env(env) From d875bfd490cbd2f50dd9e7b8b9b31fa1ff19c127 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Tue, 7 Jan 2014 17:45:32 -0500 Subject: [PATCH 08/19] added "renderer" support to bundle loaders --- src/webassets/loaders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webassets/loaders.py b/src/webassets/loaders.py index 1cd6eb77..60c263d0 100644 --- a/src/webassets/loaders.py +++ b/src/webassets/loaders.py @@ -64,7 +64,9 @@ def _get_bundle(self, data): output=data.get('output', None), debug=data.get('debug', None), extra=data.get('extra', {}), - depends=data.get('depends', None)) + depends=data.get('depends', None), + renderer=data.get('renderer', None), + ) return Bundle(*list(self._yield_bundle_contents(data)), **kwargs) def _get_bundles(self, obj, known_bundles=None): From 964483b86dee9058358da024a0fe94229dfa66d5 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Wed, 22 Jan 2014 14:59:42 -0500 Subject: [PATCH 09/19] corrected documentation --- docs/renderer.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/renderer.rst b/docs/renderer.rst index fd1fe466..525f3274 100644 --- a/docs/renderer.rst +++ b/docs/renderer.rst @@ -93,6 +93,7 @@ or a callable that receives the following keyword arguments: * `bundle`: the Bundle object being rendered. * `url`: the currently being rendered asset URL. * `content`: the asset content (for inline renderings only). +* `env`: the environment currently in effect for the rendering. An example custom ``less`` renderer that can handle less being compiled either client-side or server-side (the environment will From ac9b9bff442188dcc31d1ef4b3d4b87b7f74781b Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sat, 1 Feb 2014 18:13:29 -0500 Subject: [PATCH 10/19] added "default" renderer and pushed more work into renderer.py --- src/webassets/bundle.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/webassets/bundle.py b/src/webassets/bundle.py index 1a7ee786..efec1ad1 100644 --- a/src/webassets/bundle.py +++ b/src/webassets/bundle.py @@ -10,7 +10,6 @@ from .updater import SKIP_CACHE from .exceptions import BundleError, BuildError from .utils import cmp_debug_levels, urlparse -from .renderer import BundleRenderer __all__ = ('Bundle', 'get_all_bundle_files',) @@ -687,23 +686,33 @@ def urls(self, env=None, *args, **kwargs): urls.extend(bundle._urls(env, extra_filters, *args, **kwargs)) return urls - def renderers(self, env=None, inline=None, *args, **kwargs): + def renderers(self, env=None, inline=None, default=None, *args, **kwargs): ''' Returns a generator of renderers for this bundle. This operates almost identically to :meth:`.urls()`, so this may return one BundleRenderer (usually in production) or - multiple BundleRenderers (usually in debug mode). + multiple BundleRenderers (usually in debug mode or when the + bundles uses multiple renderer types). - If `inline` is specified (i.e. not ``None``), then this will - be specified as the default inlining mode of each - renderer. Note, however, that this can be overridden then on a - per-renderer basis. + :Parameters: + + * `inline` : bool, optional, default: null + + If specified (and not ``None``), this will be used as the + default inlining mode of each renderer. Note, however, that + this can be overridden then on a per-renderer basis. + + * `default` : str, optional, default: null + + If specified (and not ``None``), this will be used as the + value to the `default` parameter to each `render()` call. + Note, however, that this can be overridden then on a + per-renderer basis. ''' - env = self._get_env(env) - for bundle, extra_filters in self.iterbuild(env): - for url in bundle._urls(env, extra_filters, *args, **kwargs): - yield BundleRenderer(env, bundle, url, inline) + from .renderer import bundle_renderer_iter + return bundle_renderer_iter( + self, self._get_env(env), inline, default, *args, **kwargs) def pull_external(env, filename): From 592c6ca2675ea5edc26413d96ffbf5a1b2e33103 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sat, 1 Feb 2014 18:13:59 -0500 Subject: [PATCH 11/19] added support for mixed-renderer bundles --- src/webassets/renderer.py | 76 ++++++++++++++++++----- tests/test_renderer.py | 123 +++++++++++++++++++++++++++++++++----- 2 files changed, 169 insertions(+), 30 deletions(-) diff --git a/src/webassets/renderer.py b/src/webassets/renderer.py index 25298a30..67b920be 100644 --- a/src/webassets/renderer.py +++ b/src/webassets/renderer.py @@ -1,6 +1,8 @@ from webassets import six -__all__ = 'register_global_renderer', 'BundleRenderer' +from .bundle import Bundle + +__all__ = 'register_global_renderer' global_renderers = dict() @@ -96,37 +98,83 @@ def make_template_renderer(template): # '') +def same_renderer(bundle, renderer): + if bundle.renderer is not None and bundle.renderer != renderer: + return False + for sub in bundle.contents: + if isinstance(sub, Bundle): + if not same_renderer(sub, renderer): + return False + return True + + +def bundle_renderer_iter(bundle, env, inline, default, *args, **kwargs): + default = bundle.renderer or default + # first, check for mixed-renderer bundles + if same_renderer(bundle, default): + for bundle, extra_filters in bundle.iterbuild(env): + for url in bundle._urls(env, extra_filters, *args, **kwargs): + yield BundleRenderer(env, bundle, url, inline, default) + return + def copy(bundle, renderer, index=0): + ret = Bundle(renderer=renderer) + # copying all attributes except 'contents' and 'renderer'... + for attr in ('env', 'output', 'filters', 'debug', \ + 'depends', 'version', 'extra'): + setattr(ret, attr, getattr(bundle, attr)) + if index != 0: + ret.output += '.part-%d' % (index,) + return (ret, index + 1) + cur, idx = copy(bundle, default) + for sub in bundle.contents: + if not isinstance(sub, Bundle) or same_renderer(sub, default): + if cur is None: + cur, idx = copy(bundle, default, idx) + cur.contents += (sub,) + continue + if cur and cur.contents: + for br in bundle_renderer_iter(cur, env, inline, default, *args, **kwargs): + yield br + cur = None + for br in bundle_renderer_iter(sub, env, inline, default, *args, **kwargs): + yield br + if cur and cur.contents: + for br in bundle_renderer_iter(cur, env, inline, default, *args, **kwargs): + yield br + + class BundleRenderer(object): - def __init__(self, env, bundle, url, inline): - self.env = env - self.bundle = bundle - self.url = url - self.inline = inline + def __init__(self, env, bundle, url, inline=None, default=None): + self.env = env + self.bundle = bundle + self.url = url + self.inline = inline + self.default = default def _get_renderer(self, name): rend = None if hasattr(self.env, 'renderers'): - rend = self.env.renderers.get(name, None) + rend = self.env.renderers.get(name, None) if rend is None: rend = global_renderers.get(name, None) if rend is None: raise ValueError('Cannot find renderer "%s"' % (str(name),)) return rend - def render(self, inline=None): + def render(self, inline=None, default=None): if inline or ( inline is None and self.inline ): - return self._render_inline() - return self._render_ref() + return self._render_inline(default=default or self.default) + return self._render_ref(default=default or self.default) - def _render_ref(self): - typ = self.bundle.renderer + def _render_ref(self, default=None): + typ = self.bundle.renderer or default rend = self._get_renderer(typ)[0] return rend( type=typ, bundle=self.bundle, url=self.url, env=self.env) - def _render_inline(self): - typ = self.bundle.renderer + def _render_inline(self, default=None): + typ = self.bundle.renderer or default rend = self._get_renderer(typ)[1] buf = six.StringIO() self.bundle.build(force=True, output=buf, env=self.env) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 774e2e6d..a57add1d 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -20,6 +20,7 @@ def _build(self, *a, **kw): if output: output.write(self.extra.get('data', 'MockData')) self.MockBundle = MockBundleWithContent + self.env.register_renderer('custom', '<{url}>', '[{url}]') def test_missing_renderer(self): b = self.MockBundle('a', output='out', renderer='no-such-renderer') @@ -28,27 +29,26 @@ def test_missing_renderer(self): self.assertEqual( str(cm.exception), 'Cannot find renderer "no-such-renderer"') - def test_render_reference_css(self): - b = self.MockBundle('a.css', output='out.css', renderer='css') + def test_reference_css(self): + b = self.MockBundle('a', output='out.css', renderer='css') self.assertEqual( [r.render() for r in b.renderers()], ['']) - def test_render_reference_js(self): - b = self.MockBundle('a.js', output='out.js', renderer='js') + def test_reference_js(self): + b = self.MockBundle('a', output='out.js', renderer='js') self.assertEqual( [r.render() for r in b.renderers()], ['']) - def test_render_reference_custom(self): - self.env.register_renderer('custom', '[REF: {url}]', '[RAW: {url}]') - b = self.MockBundle('a.cstm', output='out.cstm', renderer='custom') + def test_reference_custom(self): + b = self.MockBundle('a', output='out.cstm', renderer='custom') self.assertEqual( [r.render() for r in b.renderers()], - ['[REF: /out.cstm]']) + ['']) - def test_render_inline_css(self): - b = self.MockBundle('a.css', output='out.css', renderer='css', + def test_inline_css(self): + b = self.MockBundle('a', output='out.css', renderer='css', extra=dict(data='some-css')) self.assertEqual( [r.render(inline=True) for r in b.renderers()], @@ -57,8 +57,8 @@ def test_render_inline_css(self): some-css /*]]>*/-->''']) - def test_render_inline_js(self): - b = self.MockBundle('a.js', output='out.js', renderer='js', + def test_inline_js(self): + b = self.MockBundle('a', output='out.js', renderer='js', extra=dict(data='some-js')) self.assertEqual( [r.render(inline=True) for r in b.renderers()], @@ -67,9 +67,100 @@ def test_render_inline_js(self): some-js //-->''']) - def test_render_defaultinline_custom(self): - self.env.register_renderer('custom', '[REF: {url}]', '[RAW: {url}]') - b = self.MockBundle('a.cstm', output='out.cstm', renderer='custom') + def test_defaultinline_custom(self): + b = self.MockBundle('a', output='a.out', renderer='custom') self.assertEqual( [r.render() for r in b.renderers(inline=True)], - ['[RAW: /out.cstm]']) + ['[/a.out]']) + + def test_inherit(self): + a = self.MockBundle('a', output='a.out') + b = self.MockBundle('b', output='b.out') + c = self.MockBundle(a, b, output='c.out') + self.assertEqual( + [r.render() for r in c.renderers(default='custom')], + ['']) + + def test_inherit_debug(self): + self.env.debug = True + a = self.MockBundle('a', output='a.out') + b = self.MockBundle('b', output='b.out') + c = self.MockBundle(a, b, output='c.out') + self.assertEqual( + [r.render() for r in c.renderers(default='custom')], + ['', '']) + + def test_multiple(self): + a = self.MockBundle('a', output='a.out', renderer='custom') + b = self.MockBundle('b', output='b.out', renderer='custom') + c = self.MockBundle(a, b, output='c.out', renderer='custom') + self.assertEqual( + [r.render() for r in c.renderers(inline=False)], + ['']) + self.assertEqual( + [r.render() for r in c.renderers(inline=True)], + ['[/c.out]']) + + def test_multiple_debug(self): + self.env.debug = True + a = self.MockBundle('a', output='a.out', renderer='custom') + b = self.MockBundle('b', output='b.out', renderer='custom') + c = self.MockBundle(a, b, output='c.out', renderer='custom') + self.assertEqual( + [r.render() for r in c.renderers(inline=False)], + ['', '']) + self.assertEqual( + [r.render() for r in c.renderers(inline=True)], + ['[/a]', '[/b]']) + + def test_mixed(self): + self.env.register_renderer('custom2', '<<{url}>>', '[[{url}]]') + a = self.MockBundle('a', output='a.out', renderer='custom') + b = self.MockBundle('b', output='b.out', renderer='custom2') + c = self.MockBundle(a, b, output='c.out', renderer='custom') + self.assertEqual( + [r.render() for r in c.renderers(inline=False)], + ['', '<>']) + self.assertEqual( + [r.render() for r in c.renderers(inline=True)], + ['[/c.out]', '[[/b.out]]']) + + def test_mixed_debug(self): + self.env.debug = True + self.env.register_renderer('custom2', '<<{url}>>', '[[{url}]]') + a = self.MockBundle('a', output='a.out', renderer='custom') + b = self.MockBundle('b', output='b.out', renderer='custom2') + c = self.MockBundle(a, b, output='c.out', renderer='custom') + self.assertEqual( + [r.render() for r in c.renderers(inline=False)], + ['', '<>']) + self.assertEqual( + [r.render() for r in c.renderers(inline=True)], + ['[/a]', '[[/b]]']) + + def test_mixed_interleaved(self): + self.env.register_renderer('custom2', '<<{url}>>', '[[{url}]]') + a = self.MockBundle('a', output='a.out', renderer='custom') + b = self.MockBundle('b', output='b.out', renderer='custom2') + c = self.MockBundle('c', output='c.out', renderer='custom') + d = self.MockBundle(a, b, c, output='d.out', renderer='custom') + self.assertEqual( + [r.render() for r in d.renderers(inline=False)], + ['', '<>', '']) + self.assertEqual( + [r.render() for r in d.renderers(inline=True)], + ['[/d.out]', '[[/b.out]]', '[/d.out.part-1]']) + + def test_mixed_interleaved_debug(self): + self.env.debug = True + self.env.register_renderer('custom2', '<<{url}>>', '[[{url}]]') + a = self.MockBundle('a', output='a.out', renderer='custom') + b = self.MockBundle('b', output='b.out', renderer='custom2') + c = self.MockBundle('c', output='c.out', renderer='custom') + d = self.MockBundle(a, b, c, output='d.out', renderer='custom') + self.assertEqual( + [r.render() for r in d.renderers(inline=False)], + ['', '<>', '']) + self.assertEqual( + [r.render() for r in d.renderers(inline=True)], + ['[/a]', '[[/b]]', '[/c]']) From 8039942f518ba49e542a652297fe58cc287acc02 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sat, 1 Feb 2014 18:15:39 -0500 Subject: [PATCH 12/19] added more documentation --- docs/renderer.rst | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/docs/renderer.rst b/docs/renderer.rst index 525f3274..fb3b1be7 100644 --- a/docs/renderer.rst +++ b/docs/renderer.rst @@ -15,9 +15,31 @@ method, which returns a `BundleRenderer` and the `Environment` class' Bundle Rendering ================ -Bundles have a method ``renderers()`` that returns a generator of one -or more BundleRenderers that actually do the rendering. For example, -to render a CSS link or inline stylesheet, you can do the following: +Bundles have a method ``.renderers()`` that returns a generator of one +or more BundleRenderers that manage the rendering. The primary method +of a BundleRenderer is the ``.render()`` method, which actually +returns the rendered result. + +Renderers are inherited by child bundles from parent bundles if their +renderer is set to ``None``. Note that renderers do not propagate from +child bundles to parent (container) bundles. + +Both the ``Bundle.renderers()`` and ``BundleRenderer.render()`` methods +take the following optional parameters: + +* `inline`: whether or not to render a reference to the asset as or to + to inline the asset directly. Note that some renderers can only do + one or the other. + +* `default`: specify a default renderer that is inherited down the + bundle container stack. + +If not renderer is defined or inherited, then the default renderer is +used, which simply renders the asset URL (when referenced) or the +asset contents (when inlined). + +For example, to render a CSS link or inline stylesheet, you can do the +following: .. code-block:: python @@ -130,7 +152,23 @@ And to register the renderer globally (usually not recommended): from webassets.renderer import register_global_renderer register_global_renderer('less', my_less_renderer, my_less_inline_renderer) - Note that in the above examples, we registered both a referencing renderer as well as an inline renderer. If we had specified only the former, then the inline renderer would default to that one as well. + +And here an example of registering a simpler string-based renderer: + +.. code-block:: python + + env.register_renderer( + + # the name of the renderer: + 'less', + + # the "by reference" rendering: + '', + + # and optionally the "inline" rendering (which defaults + # to using the "by reference" renderer): + '' + ) From 77d169645f9bb0f9b1270e6cc271927ccced3d48 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sat, 1 Feb 2014 18:32:19 -0500 Subject: [PATCH 13/19] added global "less" renderer --- docs/renderer.rst | 51 ++++++++++++++++++++------------------- src/webassets/renderer.py | 34 +++++++++++++++----------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/docs/renderer.rst b/docs/renderer.rst index fb3b1be7..ef6485d3 100644 --- a/docs/renderer.rst +++ b/docs/renderer.rst @@ -38,8 +38,8 @@ If not renderer is defined or inherited, then the default renderer is used, which simply renders the asset URL (when referenced) or the asset contents (when inlined). -For example, to render a CSS link or inline stylesheet, you can do the -following: +For example, to render a CSS link reference or inline stylesheet, you +can do the following: .. code-block:: python @@ -102,10 +102,10 @@ Would generate something like the following output: Renderer Registration ===================== -Webassets provides default renderers for CSS (named ``"css"``) and for -JavaScript (named ``"js"``). If you need to add more renderers, or -change the default rendering, this can be done via renderer -registration. +Webassets provides default renderers for CSS (named ``"css"``), +JavaScript (named ``"js"``), and LESS CSS (named ``"less"``). If you +need to add more renderers, or change the default rendering, this can +be done via renderer registration. A renderer is either a string in `str.format syntax `_, @@ -117,21 +117,21 @@ or a callable that receives the following keyword arguments: * `content`: the asset content (for inline renderings only). * `env`: the environment currently in effect for the rendering. -An example custom ``less`` renderer that can handle less being -compiled either client-side or server-side (the environment will -indicate this via the `debug` flag): +An example custom ``svg`` renderer that can handle SVG being +rasterized either client-side or server-side (note that this assumes +that there is a filter that rasterizes SVGs to PNGs running that is +sensitive to the `debug` and `svg_run_in_debug` flags): .. code-block:: python - def my_less_renderer(type, bundle, url): - dolessc = not bundle.env.debug or bundle.env.config.get('less_run_in_debug') - rel = 'stylesheet' if dolessc else 'stylesheet/less' - return ''.format(rel=rel, url=url) + def my_svg_renderer(type, bundle, url): + return '' - def my_less_inline_renderer(type, bundle, url, content): - dolessc = not bundle.env.debug or bundle.env.config.get('less_run_in_debug') - type = 'text/css' if dolessc else 'text/less' - return ''.format(type=type, content=content) + def my_svg_inline_renderer(type, bundle, url, content): + from base64 import b64encode + dosvgc = not bundle.env.debug or bundle.env.config.get('svg_run_in_debug') + type = 'image/png' if dosvgc else 'image/svg' + return ''.format(type=type, content=content) You can register renderers in particular ``Environment`` objects @@ -142,7 +142,7 @@ To register the renderer in an environment: .. code-block:: python - env.register_renderer('less', my_less_renderer, my_less_inline_renderer) + env.register_renderer('svg', my_svg_renderer, my_svg_inline_renderer) And to register the renderer globally (usually not recommended): @@ -150,25 +150,26 @@ And to register the renderer globally (usually not recommended): .. code-block:: python from webassets.renderer import register_global_renderer - register_global_renderer('less', my_less_renderer, my_less_inline_renderer) + register_global_renderer('svg', my_svg_renderer, my_svg_inline_renderer) Note that in the above examples, we registered both a referencing renderer as well as an inline renderer. If we had specified only the former, then the inline renderer would default to that one as well. -And here an example of registering a simpler string-based renderer: +And here an example of registering a simpler string-based renderer +(but which will always render a reference to the image even when +inlining is requested): .. code-block:: python env.register_renderer( # the name of the renderer: - 'less', + 'svg', # the "by reference" rendering: - '', + '' - # and optionally the "inline" rendering (which defaults - # to using the "by reference" renderer): - '' + # an "inline" renderer is not specified, so it will + # default to the above "by reference" renderer ) diff --git a/src/webassets/renderer.py b/src/webassets/renderer.py index 67b920be..b7c695f8 100644 --- a/src/webassets/renderer.py +++ b/src/webassets/renderer.py @@ -82,20 +82,26 @@ def make_template_renderer(template): '', '') -# todo: register renderers for less. the complication with that, is -# that it is sensitive to whether or not the less is compiled -# client-side or server-side... - -# # in server-side compiled mode -# register_global_renderer( -# 'less', -# '', -# '') -# # in client-side compiled mode -# register_global_renderer( -# 'less', -# '', -# '') +# register a less renderer +# todo: perhaps this should be registered by the 'less' filter?... + +LESS_REFERENCE_FMT = '' +LESS_INLINE_FMT = '''\ +''' + +def less_renderer(type, bundle, url, env, **kw): + runlessc = not env.debug or env.config.get('less_run_in_debug', True) + rel = 'stylesheet' if runlessc else 'stylesheet/less' + return LESS_REFERENCE_FMT.format(rel=rel, url=url) + +def less_inline_renderer(type, bundle, url, content, env, **kw): + runlessc = not env.debug or env.config.get('less_run_in_debug', True) + type = 'text/css' if runlessc else 'text/less' + return LESS_INLINE_FMT.format(type=type, content=content) + +register_global_renderer('less', less_renderer, less_inline_renderer) def same_renderer(bundle, renderer): From 47fe427b3a8c177baadebfe495247fd3329dba1c Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sat, 1 Feb 2014 18:36:39 -0500 Subject: [PATCH 14/19] added less renderer unit tests --- tests/test_renderer.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index a57add1d..0ceaff48 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -35,6 +35,20 @@ def test_reference_css(self): [r.render() for r in b.renderers()], ['']) + def test_reference_less(self): + b = self.MockBundle('a', output='out.css', renderer='less') + self.assertEqual( + [r.render() for r in b.renderers()], + ['']) + + def test_reference_less_debug(self): + self.env.debug = True + self.env.config['less_run_in_debug'] = False + b = self.MockBundle('a', output='out.css', renderer='less') + self.assertEqual( + [r.render() for r in b.renderers()], + ['']) + def test_reference_js(self): b = self.MockBundle('a', output='out.js', renderer='js') self.assertEqual( @@ -57,6 +71,30 @@ def test_inline_css(self): some-css /*]]>*/-->''']) + def test_inline_less(self): + b = self.MockBundle('a', output='out.css', renderer='less', + extra=dict(data='some-less')) + self.assertEqual( + [r.render(inline=True) for r in b.renderers()], + ['''\ +''']) + + + def test_inline_less_debug(self): + self.env.debug = True + self.env.config['less_run_in_debug'] = False + b = self.MockBundle('a', output='out.css', renderer='less', + extra=dict(data='some-less')) + self.assertEqual( + [r.render(inline=True) for r in b.renderers()], + ['''\ +''']) + + def test_inline_js(self): b = self.MockBundle('a', output='out.js', renderer='js', extra=dict(data='some-js')) From 9c56672a5607690d8d0339f3d263966aee77e000 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 2 Feb 2014 15:51:00 -0500 Subject: [PATCH 15/19] implemented renderer merge_checker for merging of compatible content from different renderers --- src/webassets/env.py | 5 +- src/webassets/renderer.py | 108 +++++++++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 34 deletions(-) diff --git a/src/webassets/env.py b/src/webassets/env.py index c39f915f..4bc1a6b3 100644 --- a/src/webassets/env.py +++ b/src/webassets/env.py @@ -697,7 +697,8 @@ def _get_url_mapping(self): modifying this setting directly. """) - def register_renderer(self, name, renderer, inline_renderer=None): + def register_renderer(self, name, renderer, + inline_renderer=None, merge_checker=None): ''' Registers renderers to be used only for renderings done within the context of this environment. @@ -706,7 +707,7 @@ def register_renderer(self, name, renderer, inline_renderer=None): :func:`webassets.renderer.register_global_renderer()`. ''' self.renderers[name] = prepare_renderer( - name, renderer, inline_renderer) + name, renderer, inline_renderer, merge_checker) class DictConfigStorage(ConfigStorage): """Using a lower-case dict for configuration values. diff --git a/src/webassets/renderer.py b/src/webassets/renderer.py index b7c695f8..fdde60eb 100644 --- a/src/webassets/renderer.py +++ b/src/webassets/renderer.py @@ -6,8 +6,13 @@ global_renderers = dict() +class Renderer(object): + def __init__(self, reference, inline, mergeable): + self.reference = reference + self.inline = inline + self.mergeable = mergeable -def prepare_renderer(name, renderer, inline_renderer=None): +def prepare_renderer(name, renderer, inline_renderer=None, merge_checker=None): ''' For internal use only -- prepares the renderers to be stored in the internal lookup tables. @@ -18,10 +23,10 @@ def prepare_renderer(name, renderer, inline_renderer=None): inline_renderer = make_template_renderer(inline_renderer) if inline_renderer is None: inline_renderer = renderer - return (renderer, inline_renderer) + return Renderer(renderer, inline_renderer, merge_checker) -def register_global_renderer(name, renderer, inline_renderer=None): +def register_global_renderer(name, renderer, inline_renderer=None, merge_checker=None): ''' Register the `renderer` under the name `name` globally. If `inline_renderer` is ``None``, it will default to the using the @@ -43,9 +48,20 @@ def register_global_renderer(name, renderer, inline_renderer=None): * `type`: the renderer type, i.e. the `name`. * `url`: the currently being rendered asset URL. * `content`: the asset content (for inline renderings only). + * `env`: the current environment object. + + The optional parameter `merge_checker` specifies a callable that + is used when rendering bundles that contain renderers of different + types. It must return a boolean (mergeable or not mergeable) or + None (unknown). It is called with the following keyword + parameters: + + * `parent`: the "parent" (i.e. container bundle) renderer type. + * `child`: the "child" (i.e. the contained bundle) renderer type. + * `env`: the current environment object. ''' global_renderers[name] = prepare_renderer( - name, renderer, inline_renderer) + name, renderer, inline_renderer, merge_checker) def make_template_renderer(template): @@ -60,6 +76,17 @@ def make_template_renderer(template): return str(template).format +def get_renderer(env, name): + ret = None + if hasattr(env, 'renderers'): + ret = env.renderers.get(name, None) + if ret is None: + ret = global_renderers.get(name, None) + if ret is None: + raise ValueError('Cannot find renderer "%s"' % (str(name),)) + return ret + + # register a default renderer that simply outputs the data as-is. not # particularly useful, but at least that way it is visible, and # developers will hopefully realize that it is a mis-configuration... @@ -92,24 +119,47 @@ def make_template_renderer(template): /*]]>*/-->''' def less_renderer(type, bundle, url, env, **kw): - runlessc = not env.debug or env.config.get('less_run_in_debug', True) - rel = 'stylesheet' if runlessc else 'stylesheet/less' - return LESS_REFERENCE_FMT.format(rel=rel, url=url) + runlessc = not env.debug or env.config.get('less_run_in_debug', True) + rel = 'stylesheet' if runlessc else 'stylesheet/less' + return LESS_REFERENCE_FMT.format(rel=rel, url=url) def less_inline_renderer(type, bundle, url, content, env, **kw): - runlessc = not env.debug or env.config.get('less_run_in_debug', True) - type = 'text/css' if runlessc else 'text/less' - return LESS_INLINE_FMT.format(type=type, content=content) + runlessc = not env.debug or env.config.get('less_run_in_debug', True) + type = 'text/css' if runlessc else 'text/less' + return LESS_INLINE_FMT.format(type=type, content=content) -register_global_renderer('less', less_renderer, less_inline_renderer) +def less_merge_checker(parent, child, env): + if parent == 'less' and child == 'css': + return True + if parent != 'css' or child != 'less': + return None + return not env.debug or env.config.get('less_run_in_debug', True) - -def same_renderer(bundle, renderer): +register_global_renderer( + 'less', less_renderer, less_inline_renderer, less_merge_checker) + + +def mergeable_renderer_types(env, parent, child): + if parent == child: # paranoia + return True + mergeable = get_renderer(env, parent).mergeable + if mergeable is not None: + ret = mergeable(parent=parent, child=child, env=env) + if ret is not None: + return ret + mergeable = get_renderer(env, child).mergeable + if mergeable is not None: + ret = mergeable(parent=parent, child=child, env=env) + if ret is not None: + return ret + return False + +def mergeable_renderer(env, bundle, renderer): if bundle.renderer is not None and bundle.renderer != renderer: - return False + return mergeable_renderer_types(env, renderer, bundle.renderer) for sub in bundle.contents: if isinstance(sub, Bundle): - if not same_renderer(sub, renderer): + if not mergeable_renderer(env, sub, renderer): return False return True @@ -117,7 +167,7 @@ def same_renderer(bundle, renderer): def bundle_renderer_iter(bundle, env, inline, default, *args, **kwargs): default = bundle.renderer or default # first, check for mixed-renderer bundles - if same_renderer(bundle, default): + if mergeable_renderer(env, bundle, default): for bundle, extra_filters in bundle.iterbuild(env): for url in bundle._urls(env, extra_filters, *args, **kwargs): yield BundleRenderer(env, bundle, url, inline, default) @@ -129,11 +179,17 @@ def copy(bundle, renderer, index=0): 'depends', 'version', 'extra'): setattr(ret, attr, getattr(bundle, attr)) if index != 0: - ret.output += '.part-%d' % (index,) + # todo: this is a hack. the problem is that when a bundle + # gets fragmented for multi-renderer support, it needs a + # different output location... + if ret.output is None: + import uuid + ret.output = 'bundle-fragment-' + str(uuid.uuid4()).replace('-', '') + ret.output += ':%d' % (index,) return (ret, index + 1) cur, idx = copy(bundle, default) for sub in bundle.contents: - if not isinstance(sub, Bundle) or same_renderer(sub, default): + if not isinstance(sub, Bundle) or mergeable_renderer(env, sub, default): if cur is None: cur, idx = copy(bundle, default, idx) cur.contents += (sub,) @@ -158,16 +214,6 @@ def __init__(self, env, bundle, url, inline=None, default=None): self.inline = inline self.default = default - def _get_renderer(self, name): - rend = None - if hasattr(self.env, 'renderers'): - rend = self.env.renderers.get(name, None) - if rend is None: - rend = global_renderers.get(name, None) - if rend is None: - raise ValueError('Cannot find renderer "%s"' % (str(name),)) - return rend - def render(self, inline=None, default=None): if inline or ( inline is None and self.inline ): return self._render_inline(default=default or self.default) @@ -175,16 +221,14 @@ def render(self, inline=None, default=None): def _render_ref(self, default=None): typ = self.bundle.renderer or default - rend = self._get_renderer(typ)[0] - return rend( + return get_renderer(self.env, typ).reference( type=typ, bundle=self.bundle, url=self.url, env=self.env) def _render_inline(self, default=None): typ = self.bundle.renderer or default - rend = self._get_renderer(typ)[1] buf = six.StringIO() self.bundle.build(force=True, output=buf, env=self.env) buf = buf.getvalue() - return rend( + return get_renderer(self.env, typ).inline( type=typ, bundle=self.bundle, url=self.url, content=buf, env=self.env) From 320651eb4510f7c8f35e040d8cd0878e202c1b96 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 2 Feb 2014 15:51:33 -0500 Subject: [PATCH 16/19] updated unit tests for shortened multi-part naming --- tests/test_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 0ceaff48..5eebfa55 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -184,10 +184,10 @@ def test_mixed_interleaved(self): d = self.MockBundle(a, b, c, output='d.out', renderer='custom') self.assertEqual( [r.render() for r in d.renderers(inline=False)], - ['', '<>', '']) + ['', '<>', '']) self.assertEqual( [r.render() for r in d.renderers(inline=True)], - ['[/d.out]', '[[/b.out]]', '[/d.out.part-1]']) + ['[/d.out]', '[[/b.out]]', '[/d.out:1]']) def test_mixed_interleaved_debug(self): self.env.debug = True From 1d4262611102b591266de125ef5482d93bf99dca Mon Sep 17 00:00:00 2001 From: metagriffin Date: Sun, 2 Feb 2014 15:51:53 -0500 Subject: [PATCH 17/19] added renderer "merge_checker" docs --- docs/renderer.rst | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/renderer.rst b/docs/renderer.rst index ef6485d3..e4690814 100644 --- a/docs/renderer.rst +++ b/docs/renderer.rst @@ -117,6 +117,23 @@ or a callable that receives the following keyword arguments: * `content`: the asset content (for inline renderings only). * `env`: the environment currently in effect for the rendering. +Finally, renderers can also specify whether or not their content can +be merged with another renderer. For example, a "less" file *can* be +merged with a "css" file if and only if the less is being compiled to +css. If in debug mode and "less_run_in_debug" is falsy, then "less" +and "css" cannot be merged. A "merge_checker" is specified as the +third argument in the registration (or via keyword), and must accept +the following keyword arguments: + +* `parent`: the container bundle renderer type (e.g. ``"css"``). +* `child`: the contained bundle renderer type (e.g. ``"less"``). +* `env`: the environment currently in effect for the rendering. + +The `merge_checker` can return truthy (the contents can be merged), +falsy (the contents cannot be merged), or None (this renderer does not +know). All affected renderers will be queried regarding mergeability, +until a non-None response is given. + An example custom ``svg`` renderer that can handle SVG being rasterized either client-side or server-side (note that this assumes that there is a filter that rasterizes SVGs to PNGs running that is @@ -124,15 +141,23 @@ sensitive to the `debug` and `svg_run_in_debug` flags): .. code-block:: python - def my_svg_renderer(type, bundle, url): + def svg_renderer(type, bundle, url): return '' - def my_svg_inline_renderer(type, bundle, url, content): + def svg_inline_renderer(type, bundle, url, content): from base64 import b64encode dosvgc = not bundle.env.debug or bundle.env.config.get('svg_run_in_debug') type = 'image/png' if dosvgc else 'image/svg' return ''.format(type=type, content=content) + def svg_mergeable(parent, child, env): + # (this is a completely bogus implementation -- see + # :func:`webassets.renderer.less_merge_checker` for a + # good example.) + if parent == 'svg' and child == 'xml-comment': + return True + return None + You can register renderers in particular ``Environment`` objects (recommended) or you can also register renderers globally (only @@ -142,7 +167,7 @@ To register the renderer in an environment: .. code-block:: python - env.register_renderer('svg', my_svg_renderer, my_svg_inline_renderer) + env.register_renderer('svg', svg_renderer, svg_inline_renderer, svg_mergeable) And to register the renderer globally (usually not recommended): @@ -150,7 +175,8 @@ And to register the renderer globally (usually not recommended): .. code-block:: python from webassets.renderer import register_global_renderer - register_global_renderer('svg', my_svg_renderer, my_svg_inline_renderer) + register_global_renderer('svg', svg_renderer, svg_inline_renderer, svg_mergeable) + Note that in the above examples, we registered both a referencing renderer as well as an inline renderer. If we had specified only the From a1725b5b4dc53c34536e6668956b8401ab103884 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Mon, 3 Feb 2014 12:06:37 -0500 Subject: [PATCH 18/19] implemented renderer type filtering/selection --- src/webassets/bundle.py | 26 ++++++++++++++++++++++++-- src/webassets/renderer.py | 31 +++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/webassets/bundle.py b/src/webassets/bundle.py index efec1ad1..9f83f1ed 100644 --- a/src/webassets/bundle.py +++ b/src/webassets/bundle.py @@ -686,7 +686,8 @@ def urls(self, env=None, *args, **kwargs): urls.extend(bundle._urls(env, extra_filters, *args, **kwargs)) return urls - def renderers(self, env=None, inline=None, default=None, *args, **kwargs): + def renderers(self, types=None, inline=None, default=None, env=None, + *args, **kwargs): ''' Returns a generator of renderers for this bundle. @@ -697,6 +698,21 @@ def renderers(self, env=None, inline=None, default=None, *args, **kwargs): :Parameters: + * `types` : {str, list(str)}, optional, default: null + + If specified, restricts the returned renderers to the + selected set. If the selection is a string-type, it is + converted into a list by splitting at commas (","). Then, + each potential renderer is matched against the set, and if + any match, the renderer is included. If the match starts + with a bang ("!"), it is a negative match, and the entire + set becomes an exclusive set instead of inclusive. It is a + syntax error to mix both positive and negative matches. + Examples: + + * ``"less,css"``: selects any "less" or "css" renderers + * ``"!js"``: selects any renderers that are not "js" + * `inline` : bool, optional, default: null If specified (and not ``None``), this will be used as the @@ -709,10 +725,16 @@ def renderers(self, env=None, inline=None, default=None, *args, **kwargs): value to the `default` parameter to each `render()` call. Note, however, that this can be overridden then on a per-renderer basis. + + * `env` : object, optional, default: null + + Override the default bundle rendering environment with the + specified `env`. ''' from .renderer import bundle_renderer_iter return bundle_renderer_iter( - self, self._get_env(env), inline, default, *args, **kwargs) + self, types, inline, default, self._get_env(env), + *args, **kwargs) def pull_external(env, filename): diff --git a/src/webassets/renderer.py b/src/webassets/renderer.py index fdde60eb..16aa0657 100644 --- a/src/webassets/renderer.py +++ b/src/webassets/renderer.py @@ -163,14 +163,29 @@ def mergeable_renderer(env, bundle, renderer): return False return True - -def bundle_renderer_iter(bundle, env, inline, default, *args, **kwargs): +def matches_types(types, type): + if types is None: + return True + default = False + for typ in types: + if typ.startswith('!'): + default = True + typ = typ[1:] + if typ == type: + return not default + return default + +def bundle_renderer_iter(bundle, types, inline, default, env, *args, **kwargs): + if types is not None and isinstance(types, six.string_types): + types = [typ.strip() for typ in types.split(',')] default = bundle.renderer or default # first, check for mixed-renderer bundles if mergeable_renderer(env, bundle, default): - for bundle, extra_filters in bundle.iterbuild(env): - for url in bundle._urls(env, extra_filters, *args, **kwargs): - yield BundleRenderer(env, bundle, url, inline, default) + for sub, extra_filters in bundle.iterbuild(env): + for url in sub._urls(env, extra_filters, *args, **kwargs): + if not matches_types(types, sub.renderer or default): + continue + yield BundleRenderer(env, sub, url, inline, default) return def copy(bundle, renderer, index=0): ret = Bundle(renderer=renderer) @@ -195,13 +210,13 @@ def copy(bundle, renderer, index=0): cur.contents += (sub,) continue if cur and cur.contents: - for br in bundle_renderer_iter(cur, env, inline, default, *args, **kwargs): + for br in bundle_renderer_iter(cur, types, inline, default, env, *args, **kwargs): yield br cur = None - for br in bundle_renderer_iter(sub, env, inline, default, *args, **kwargs): + for br in bundle_renderer_iter(sub, types, inline, default, env, *args, **kwargs): yield br if cur and cur.contents: - for br in bundle_renderer_iter(cur, env, inline, default, *args, **kwargs): + for br in bundle_renderer_iter(cur, types, inline, default, env, *args, **kwargs): yield br From a32ad5fb75ae8a99a31c1bb395f7b01ed5429d16 Mon Sep 17 00:00:00 2001 From: metagriffin Date: Mon, 3 Feb 2014 12:06:49 -0500 Subject: [PATCH 19/19] added unit test for renderer filtering --- tests/test_renderer.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 5eebfa55..74cf08ea 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -202,3 +202,24 @@ def test_mixed_interleaved_debug(self): self.assertEqual( [r.render() for r in d.renderers(inline=True)], ['[/a]', '[[/b]]', '[/c]']) + + def test_selection(self): + self.env.register_renderer('ra', '', '[a:{url}]') + self.env.register_renderer('rb', '', '[b:{url}]') + self.env.register_renderer('rc', '', '[c:{url}]') + a = self.MockBundle('a', output='a.out', renderer='ra') + b = self.MockBundle('b', output='b.out', renderer='rb') + c = self.MockBundle('c', output='c.out', renderer='rc') + d = self.MockBundle(a, b, c, output='d.out') + self.assertEqual( + [r.render() for r in d.renderers('ra')], + ['']) + self.assertEqual( + [r.render() for r in d.renderers('ra,rb')], + ['', '']) + self.assertEqual( + [r.render() for r in d.renderers('!rc')], + ['', '']) + self.assertEqual( + [r.render() for r in d.renderers('!ra,!rc')], + [''])