Creating custom filters¶
Creating custom filters can be easy, or very easy.
Before we get to that though, it is first necessary to understand that there are two types of filters: input filters and output filters. Output filters are applied after the complete content after all a bundle’s contents have been merged together. Input filters, on the other hand, are applied to each source file after it is read from the disk. In the case of nested bundles, input filters will be passed down, with the input filters of a parent bundle are applied before the output filter of a child bundle:
child_bundle = Bundle('file.css', filters='yui_css')
Bundle(child_bundle, filters='cssrewrite')
In this example, because cssrewrite acts as an input filter, what will essentially happen is:
yui_css(cssrewrite(file.css))
To be even more specific, since a single filter can act as both an input and an output filter, the call chain will actually look something like this:
cssrewrite.output(yui_css.output((cssrewrite.input((yui_css.input(file.css)))))
The usual reason to use an input filter is that the filter’s transformation depends on the source file’s filename. For example, the cssrewrite filter needs to know the location of the source file relative to the final output file, so it can properly update relative references. Another example are CSS converters like less, which work relative to the input filename.
With that in mind…
The very easy way¶
In the simplest case, a filter is simply a function that takes two arguments, an input stream and an output stream.
def noop(_in, out, **kw):
out.write(_in.read())
That’s it! You can use this filter when defining your bundles:
bundle = Bundle('input.js', filters=(noop,))
If you are using Jinja2, you can also specify the callable inline, provided that it is available in the context:
{% assets filters=(noop, 'jsmin') ... %}
It even works when using Django templates, although here, you are of course more limited in terms of syntax; if you want to use multiple filters, you need to combine them:
{% assets filters=my_filters ... %}
Just make sure that the context variable my_filters
is set to
your function.
Note that you currently cannot write input filters in this way. Callables always act as output filters.
The easy way¶
This works by subclassing webassets.filter.Filter
. In doing so, you
need to write a bit more code, but you’ll be able to enjoy a few perks.
The noop
filter from the previous example, written as a class, would
look something like this:
from webassets.filter import Filter
class NoopFilter(Filter):
name = 'noop'
def output(self, _in, out, **kwargs):
out.write(_in.read())
def input(self, _in, out, **kwargs):
out.write(_in.read())
The output
and input
methods should look familiar. They’re basically
like the callable you are already familiar with, simply pulled inside a class.
Class-based filters have a name
attribute, which you need to set if you
want to register your filter globally.
The input
method will be called for every source file, the output
method will be applied once after a bundle’s contents have been concatenated.
Among the kwargs
you currently receive are:
source_path
(only forinput()
): The filename behind thein
stream, though note that other input filters may already have transformed it.output_path
: The final output path that your filters work will ultimatily end up in.
Note
Always make your filters accept arbitrary **kwargs
. The API does allow
for additional values to be passed along in the future.
Registering¶
The name
wouldn’t make much sense, if it couldn’t be used to reference
the filter. First, you need to register the class with the system though:
from webassets.filter import register_filter
register_filter(NoopFilter)
Or if you are using yaml then use the filters key for the environment:
directory: .
url: /
debug: True
updater: timestamp
filters:
- my_custom_package.my_filter
After that, you can use the filter like you would any of the built-in ones:
{% assets filters='jsmin,noop' ... %}
Options¶
Class-based filters are used as instances, and as such, you can easily
define a __init__
method that takes arguments. However, you should
make all parameters optional, if possible, or your filter will not be
usable through a name reference.
There might be another thing to consider. If a filter is specified multiple times, which sometimes can happen unsuspectingly when bundles are nested within each other, it will only be applied a single time. By default, all filters of the same class are considered the same. In almost all cases, this will be just fine.
However, in case you want your filter to be applicable multiple times
with different options, you can implement the unique
method and
return a hashable object that represents data unique to this instance:
class FooFilter(Filter):
def __init__(self, *args, **kwargs):
self.args, self.kwargs = args, kwargs
def unique(self):
return self.args, self.kwargs
This will cause two instances of this filter to be both applied, as long as the arguments given differ. Two instances with the exact same arguments will still be considered equal.
If you want each of your filter’s instances to be unique, you can simply do:
def unique(self):
return id(self)
Useful helpers¶
The Filter
base class provides some useful features.
setup()¶
It’s quite common that filters have dependencies - on other Python
libraries, external tools, etc. If you want to provide your filter
regardless of whether such dependencies are matched, and fail only
if the filter is actually used, implement a setup()
method on
your filter class:
class FooFilter(Filter):
def setup(self):
import foolib
self.foolib = foolib
def apply(self, _in, out):
self.foolib.convert(...)
options¶
Some filters will need to be configured. This can of course be done by
passing arguments into __init__
as explained above, but it restricts
you to configuring your filters in code, and can be tedious if necessary
every single time the filter is used.
In some cases, it makes more sense to have an option configured globally,
like the path to an external binary. A number of the built-in filters do
this, allowing you to both specify a config variable in the webassets
Environment
instance, or as an OS environment variable.
class FooFilter(Filter):
options = {
'binary': 'FOO_BIN'
}
If you define a an options
attribute on your filter class, these
options will automatically be supported both by your filter’s __init__,
as well as via a configuration or environment variable. In the example
above, you may pass binary
when creating a filter instance manually,
or define FOO_BIN
in Environment.config
, or as an OS environment
variable.
get_config()¶
In cases where the declarative approach of the options
attribute is
not enough, you can implement custom options yourself using the
Filter.get_config()
helper:
class FooFilter(Filter):
def setup(self):
self.bin = self.get_config('BINARY_PATH')
This will check first the configuration, then the environment for
BINARY_PATH
, and raise an exception if nothing is found.
get_config()
allows you to specify different names for the setting
and the environment variable:
self.get_config(setting='ASSETS_BINARY_PATH', env='BINARY_PATH')
It also supports disabling either of the two, causing only the other to be checked for the given name:
self.get_config(setting='ASSETS_BINARY_PATH', env=False)
Finally, you can easily make a value optional using the require
parameter. Instead of raising an exception, get_config()
then returns
None
. For example:
self.java = self.get_config('JAVA_BIN', require=False) or 'java'
Abstract base classes¶
In some cases, you might want to have a common base class for multiple
filters. You can make the base class abstract by setting name
to
None
explicitly. However, this is currently only relevant for the
built-in filters, since your own filters will not be registered
automatically in any case.
More?¶
You can have a look inside the webassets.filter
module source
code to see a large number of example filters.
Assets can be filtered through one or multiple filters, modifying their contents (think minification, compression).