Skip to content

Commit

Permalink
Document custom resolvers.
Browse files Browse the repository at this point in the history
  • Loading branch information
miracle2k committed Oct 13, 2012
1 parent ebdc547 commit 151e6fc
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 29 deletions.
127 changes: 127 additions & 0 deletions docs/generic/custom_resolver.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
.. _django_assets: https://github.com/miracle2k/django-assets


.. py:currentmodule:: webassets.env
Custom resolvers
================

The resolver is a pluggable object that webassets uses to find the
contents of a :class:`Bundle` on the filesystem, as well as to
generate the correct urls to these files.

For example, the default resolver searches the
:attr:`Environment.load_path`, or looks within
:attr:`Environment.directory`. The `webassets Django integration`__
will use Django's *staticfile finders* to look for files.

__ django_assets_

For normal usage, you will not need to write your own resolver, or
indeed need to know how they work. However, if you want to integrate
``webassets`` with another framework, or if you applicatoin is
complex enough that it requires custom file referencing, read on.


The API as webassets sees it
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``webassets`` expects to find the resolver via the
:attr:`Environment.resolver` property, and expects this object to
provide the following methods:

.. automethod:: Resolver.resolve_source

.. automethod:: Resolver.resolve_output_to_path

.. automethod:: Resolver.resolve_source_to_url

.. automethod:: Resolver.resolve_output_to_url


Methods to overwrite
~~~~~~~~~~~~~~~~~~~~

However, in practice, you will usually want to override the builtin
:class:`Resolver`, and customize it's behaviour where necessary. The
default resolver already splits what is is doing into multiple
methods; so that you can either override then, or
refer to them in your own implementation, as makes sense.

Instead of the official entry points above, may may instead prefer
to override the following methods of the default resolver class:

.. automethod:: Resolver.search_for_source

.. automethod:: Resolver.search_load_path


Helpers to use
~~~~~~~~~~~~~~

The following methods of the default resolver class you may find
useful as helpers while implementing your subclass:

.. automethod:: Resolver.consider_single_directory

.. automethod:: Resolver.glob

.. automethod:: Resolver.query_url_mapping



Example: A prefix resolver
--------------------------

The following is a simple resolver implementation that searches
for files in a different directory depending on the first
directory part.

.. code-block:: python
from webassets.env import Resolver
class PrefixResolver(Resolver):
def __init__(self, env, prefixmap):
super(PrefixResolver, self).__init__(env)
self.map = prefixmap
def search_for_source(self, item):
parts = item.split('/', 1)
if len(parts) < 2:
raise ValueError(
'"%s" not valid; a static path requires a prefix.' % item)
prefix, name = parts
if not prefix in self.map:
raise ValueError(('Prefix "%s" of static path "%s" is not '
'registered') % (prefix, item))
# For the rest, defer to base class method, which provides
# support for things like globbing.
return self.consider_single_directory(self.map[prefix], name)
Using it::

env = webassets.Environment(path, url)
env.resolver = PrefixResolver(env, {
'app1': '/var/www/app1/static',
'app2': '/srv/deploy/media/app2',
})
bundle = Bundle(
'app2/scripts/jquery.js',
'app1/*.js',
)


Other implementations
---------------------

- `django-assets Resolver <https://github.com/miracle2k/django-assets/blob/master/django_assets/env.py>`_
(search for ``class DjangoResolver``).
- `Flask-Assets Resolver <https://github.com/miracle2k/flask-assets/blob/master/src/flask_assets.py>`_
(search for ``class FlaskResolver``).
- `pyramid_webassets Resolver <https://github.com/sontek/pyramid_webassets/blob/master/pyramid_webassets/__init__.py>`_
(search for ``class PyramidResolver``).
1 change: 1 addition & 0 deletions docs/generic/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,5 @@ Further Reading
/css_compilers
/loaders
/integration/index
custom_resolver
/faq
103 changes: 74 additions & 29 deletions src/webassets/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ def __init__(self, env):
self.env = env

def glob(self, basedir, expr):
"""Runs when a glob expression needs to be resolved.
"""Generator that runs when a glob expression needs to be
resolved. Yields a list of absolute filenames.
"""
expr = path.join(basedir, expr)
for filename in glob.iglob(expr):
Expand All @@ -148,9 +149,11 @@ def glob(self, basedir, expr):
yield filename

def consider_single_directory(self, directory, item):
"""Resolve ``item`` within ``directory``, glob or non-glob style
"""Searches for ``item`` within ``directory``. Is able to
resolve glob instructions.
Primarily to be called from subclasses rather than overridden.
Subclasses can call this when they have narrowed done the
location of a bundle to a single directory.
"""
expr = path.join(directory, item)
if has_magic(expr):
Expand All @@ -162,12 +165,17 @@ def consider_single_directory(self, directory, item):
raise IOError("'%s' does not exist" % expr)

def search_env_directory(self, item):
"""Runs when :attr:`Environment.load_path` is not set.
"""This is called by :meth:`search_for_source` when no
:attr:`Environment.load_path` is set.
"""
return self.consider_single_directory(self.env.directory, item)

def search_load_path(self, item):
"""Runs when :attr:`Environment.load_path` is set.
"""This is called by :meth:`search_for_source` when a
:attr:`Environment.load_path` is set.
If you want to change how the load path is processed,
overwrite this method.
"""
if has_magic(item):
# We glob all paths.
Expand All @@ -187,14 +195,26 @@ def search_load_path(self, item):
item, self.env.load_path))

def search_for_source(self, item):
"""Runs when the item is a relative filesystem path.
"""Called by :meth:`resolve_source` after determining that
``item`` is a relative filesystem path.
You should always overwrite this method, and let
:meth:`resolve_source` deal with absolute paths, urls and
other types of items that a bundle may contain.
"""
if self.env.load_path:
return self.search_load_path(item)
else:
return self.search_env_directory(item)

def query_url_mapping(self, filepath):
"""Searches the environment-wide url mapping (based on the
urls assigned to each directory in the load path). Returns
the correct url for ``filepath``.
Subclasses should be sure that they really want to call this
method, instead of simply falling back to the ``super()``.
"""
# Build a list of dir -> url mappings
mapping = self.env.url_mapping.items()
mapping.append((self.env.directory, self.env.url))
Expand All @@ -213,55 +233,80 @@ def query_url_mapping(self, filepath):
raise ValueError('Cannot determine url for %s' % filepath)

def resolve_source(self, item):
"""Given ``item`` from a Bundle's contents, return an absolute
"""Given ``item`` from a Bundle's contents, this has to
return the final value to use, usually an absolute
filesystem path.
``item`` may include glob syntax, in which a list of paths
should be returned.
.. note::
It is also allowed to return urls and bundle instances
(or generally anything else the calling :class:`Bundle`
instance may be able to handle). Indeed this is the
reason why the name of this method does not imply a
return type.
The incoming is usually a relative path, but may also be
an absolute path, or a url. These you will commonly want to
return unmodified.
This method is also allowed to resolve ``item`` to multiple
values, in which case a list should be returned. This is
commonly used if ``item`` includes glob instructions
(wildcards).
.. note::
This is also allowed to return urls and bundles (or in fact
anything else the calling :class:`Bundle` instance may be
able to handle.
Instead of this, subclasses should consider implementing
:meth:`search_for_source` instead.
"""

# Pass through some things unscathed
if not isinstance(item, basestring):
# Don't stand in the way of custom values.
return item
if is_url(item) or path.isabs(item):
return item

return self.search_for_source(item)

def resolve_output_to_path(self, target, bundle):
"""Given ``target``, return the absolute path to which the
output file should be written.
"""Given ``target``, this has to return the absolute
filesystem path to which the output file of ``bundle``
should be written.
If a version-placeholder is used, it is still unresolved at
this point.
``target`` may be a relative or absolute path, and is
usually taking from the :attr:`Bundle.output` property.
If a version-placeholder is used (``%(version)s``, it is
still unresolved at this point.
"""
return path.join(self.env.directory, target)

def resolve_source_to_url(self, filepath, item):
"""Given the absolute path in ``filepath``, return the url
through which it is to be referenced.
"""Given the absolute filesystem path in ``filepath``, as
well as the original value from :attr:`Bundle.contents` which
resolved to this path, this must return the absolute url
through which the file is to be referenced.
The method is also passed the original ``item`` that resolved
to the given ``path``.
Depending on the use case, either the ``filepath`` or the
``item`` argument will be more helpful in generating the url.
It should raise a ``ValueError`` if a proper url cannot be
determined.
This method should raise a ``ValueError`` if the url cannot
be determined.
"""
return self.query_url_mapping(filepath)

def resolve_output_to_url(self, target):
"""Given the output ``target``, return the url through which
the output file can be referenced.
This is different from :meth:`resolve_source_to_url` in that
the absolute filesystem path is not available, for this step
needs to happen without filesystem access, for optimal
performance.
"""Given ``target``, this has to return the url through
which the output file can be referenced.
``target`` may be a relative or absolute path, and is
usually taking from the :attr:`Bundle.output` property.
This is different from :meth:`resolve_source_to_url` in
that you do not passed along the result of
:meth:`resolve_output_to_path`. This is because in many
use cases, the filesystem is not available at the point
where the output url is needed (the media server may on
a different machine).
"""
if not path.isabs(target):
# If relative, output files are written to env.directory,
Expand Down

0 comments on commit 151e6fc

Please sign in to comment.