Skip to content

Feature/issue 117 #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
952c151
highly experimental first stab at versioning image files etc
May 17, 2012
19b2e54
added output folder for external assets
eadmundo May 19, 2012
383609c
changed webassets.externalassets to webassets.external
Jul 24, 2012
848dc38
first pass at bringing API of ExternalAssets more into line with Bundle
Jul 25, 2012
725bf1f
changed cssrewrite filter to accomodate external keyword for external…
Jul 25, 2012
b02d7dd
use multiple external assets in cssrewrite filter
Jul 26, 2012
9a7cacc
first part of shared file specification between Bundle and ExternalAs…
Jul 26, 2012
4b75ac7
removed merge conflicts
Jul 31, 2012
165fd7b
Merge remote-tracking branch 'upstream/master' into feature/issue-117
Aug 24, 2012
fd305e2
standalone external assets build works again after merge
Oct 16, 2012
adc9062
Merge branch 'master' into feature/issue-117
Oct 16, 2012
66d6326
use resolver method in url rewriter
Oct 23, 2012
1c63c99
jinja webasset tag
Oct 23, 2012
2fee10d
don't resolve contents twice
Oct 24, 2012
39ee628
We want to see if the versioned file exists, not the source
tgecho Dec 5, 2012
c49de0c
Fix for issue with load_path settings causing a list of paths to be r…
tgecho Dec 5, 2012
6fb0d34
Added minimal starting docs for external assets
tgecho Dec 5, 2012
d97bf7e
It appears that we're doing some uneeded duplicate coverting, and thi…
tgecho Dec 8, 2012
65fc414
Merge pull request #2 from tgecho/feature/issue-117
eadmundo Dec 11, 2012
cf93fb2
Merge pull request #3 from tgecho/feature/external-assets-docs
eadmundo Dec 11, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

*.~*
*.pyc

# OS X
.DS_Store
46 changes: 46 additions & 0 deletions docs/external_assets.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.. _external_assets:

===============
External Assets
===============

An external assets bundle is used to manage images, webfonts and other assets
that you wouldn't normally include in another bundle. Files will have a cache
buster applied (see :doc:`URL Expiry </expiring>`), and the
:ref:`cssrewrite <filters-cssrewrite>` filter can modify css files to point to
the versioned filenames.


Registering external files
--------------------------

An external assets bundle takes any number of input patterns and one output
directory.

.. code-block:: python

ExternalAssets('images/*', 'more_images/*', output='versioned_images')

The output directory is relative to the ``directory`` setting of your
:doc:`environment <environment>`. All files found matching the input patterns
will be copied (with rewritten filenames) to this directory.


Using rewritten files
---------------------

CSS files using the :ref:`cssrewrite <filters-cssrewrite>` filter will be
automatically adapted to use the versioned filenames.

.. code-block:: python

Bundle('style.css', filters=['cssrewrite'])


If you need to get the specific url for a file, you can request it from the
bundle directly using :meth:`ExternalAssets.url` directly.

.. code-block:: python

>>> env['images'].url('logo.png')
/static/logo.c49de0ce.png
1 change: 1 addition & 0 deletions docs/generic/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Further Reading

/environment
/bundles
/external_assets
/script
/builtin_filters
/custom_filters
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ of framework used:

environment
bundles
external_assets
script
builtin_filters
expiring
Expand Down
3 changes: 2 additions & 1 deletion src/webassets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__version__ = (0, 8, 'dev')


# Make a couple frequently used things available right here.
# Make a few frequently used things available right here.
from bundle import Bundle
from external import ExternalAssets
from env import Environment
88 changes: 3 additions & 85 deletions src/webassets/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,15 @@
from merge import (FileHunk, UrlHunk, FilterTool, merge, merge_filters,
select_filters, MoreThanOneFilterError)
from updater import SKIP_CACHE
from container import Container, has_placeholder, is_url
from exceptions import BundleError, BuildError
from utils import cmp_debug_levels


__all__ = ('Bundle', 'get_all_bundle_files',)


def is_url(s):
if not isinstance(s, str):
return False
parsed = urlparse.urlsplit(s)
return bool(parsed.scheme and parsed.netloc) and len(parsed.scheme) > 1


def has_placeholder(s):
return '%(version)s' in s


class Bundle(object):
class Bundle(Container):
"""A bundle is the unit webassets uses to organize groups of media files,
which filters to apply and where to store them.

Expand All @@ -46,6 +36,7 @@ class Bundle(object):
"""

def __init__(self, *contents, **options):
super(Container, self).__init__()
self.env = None
self.contents = contents
self.output = options.pop('output', None)
Expand Down Expand Up @@ -86,13 +77,6 @@ def _set_filters(self, value):
self._filters = [get_filter(f) for f in filters]
filters = property(_get_filters, _set_filters)

def _get_contents(self):
return self._contents
def _set_contents(self, value):
self._contents = 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
Expand All @@ -111,61 +95,6 @@ def _set_extra(self, value):
template tags, and can be used to attach things like a CSS
'media' value.""")

def resolve_contents(self, env=None, force=False):
"""Return an actual list of source files.

What the user specifies as the bundle contents cannot be
processed directly. There may be glob patterns of course. We
may need to search the load path. It's common for third party
extensions to provide support for referencing assets spread
across multiple directories.

This passes everything through :class:`Environment.resolver`,
through which this process can be customized.

At this point, we also validate source paths to complain about
missing files early.

The return value is a list of 2-tuples ``(original_item,
abspath)``. In the case of urls and nested bundles both tuple
values are the same.

Set ``force`` to ignore any cache, and always re-resolve
glob patterns.
"""
env = self._get_env(env)

# TODO: We cache the values, which in theory is problematic, since
# due to changes in the env object, the result of the globbing may
# change. Not to mention that a different env object may be passed
# in. We should find a fix for this.
if getattr(self, '_resolved_contents', None) is None or force:
resolved = []
for item in self.contents:
try:
result = env.resolver.resolve_source(item)
except IOError, e:
raise BundleError(e)
if not isinstance(result, list):
result = [result]

# Exclude the output file.
# TODO: This will not work for nested bundle contents. If it
# doesn't work properly anyway, should be do it in the first
# place? If there are multiple versions, it will fail as well.
# TODO: There is also the question whether we can/should
# exclude glob duplicates.
if self.output:
try:
result.remove(self.resolve_output(env))
except (ValueError, BundleError):
pass

resolved.extend(map(lambda r: (item, r), result))

self._resolved_contents = resolved
return self._resolved_contents

def _get_depends(self):
return self._depends
def _set_depends(self, value):
Expand Down Expand Up @@ -228,17 +157,6 @@ def get_version(self, env=None, refresh=False):
self.version = version
return self.version

def resolve_output(self, env=None, version=None):
"""Return the full, absolute output path.

If a %(version)s placeholder is used, it is replaced.
"""
env = self._get_env(env)
output = env.resolver.resolve_output_to_path(self.output, self)
if has_placeholder(output):
output = output % {'version': version or self.get_version(env)}
return output

def __hash__(self):
"""This is used to determine when a bundle definition has changed so
that a rebuild is required.
Expand Down
105 changes: 105 additions & 0 deletions src/webassets/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import urlparse
import os
from exceptions import ContainerError, BundleError

try:
# Current version of glob2 does not let us access has_magic :/
import glob2 as glob
from glob import has_magic
except ImportError:
import glob
from glob import has_magic

def has_placeholder(s):
return '%(version)s' in s

def is_url(s):
if not isinstance(s, str):
return False
parsed = urlparse.urlsplit(s)
return bool(parsed.scheme and parsed.netloc) and len(parsed.scheme) > 1

class Container(object):

def __init__(self):
pass

def _get_contents(self):
return self._contents
def _set_contents(self, value):
self._contents = value
self._resolved_contents = None
contents = property(_get_contents, _set_contents)

def _get_env(self, env):
# Note how bool(env) can be False, due to __len__.
env = env if env is not None else self.env
if env is None:
raise ContainerError('Container not connected to an environment')
return env

def resolve_output(self, env=None, version=None):
"""Return the full, absolute output path.

If a %(version)s placeholder is used, it is replaced.
"""
env = self._get_env(env)
output = env.resolver.resolve_output_to_path(self.output, self)
if has_placeholder(output):
output = output % {'version': version or self.get_version(env)}
return output

def resolve_contents(self, env=None, force=False):
"""Return an actual list of source files.

What the user specifies as the bundle contents cannot be
processed directly. There may be glob patterns of course. We
may need to search the load path. It's common for third party
extensions to provide support for referencing assets spread
across multiple directories.

This passes everything through :class:`Environment.resolver`,
through which this process can be customized.

At this point, we also validate source paths to complain about
missing files early.

The return value is a list of 2-tuples ``(original_item,
abspath)``. In the case of urls and nested bundles both tuple
values are the same.

Set ``force`` to ignore any cache, and always re-resolve
glob patterns.
"""
env = self._get_env(env)

# TODO: We cache the values, which in theory is problematic, since
# due to changes in the env object, the result of the globbing may
# change. Not to mention that a different env object may be passed
# in. We should find a fix for this.
if getattr(self, '_resolved_contents', None) is None or force:
resolved = []
for item in self.contents:
try:
result = env.resolver.resolve_source(item)
except IOError, e:
raise BundleError(e)
if not isinstance(result, list):
result = [result]

# Exclude the output file.
# TODO: This will not work for nested bundle contents. If it
# doesn't work properly anyway, should be do it in the first
# place? If there are multiple versions, it will fail as well.
# TODO: There is also the question whether we can/should
# exclude glob duplicates.
if self.output:
try:
result.remove(self.resolve_output(env))
except (ValueError, BundleError):
pass

resolved.extend(map(lambda r: (item, r), result))

self._resolved_contents = resolved
return self._resolved_contents
19 changes: 17 additions & 2 deletions src/webassets/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
import urlparse
from itertools import chain
import warnings

from bundle import Bundle, is_url
from external import ExternalAssets

try:
import glob2 as glob
from glob import has_magic
except ImportError:
import glob
from glob import has_magic

from bundle import Bundle, is_url
from cache import get_cache
from version import get_versioner, get_manifest
from updater import get_updater
Expand Down Expand Up @@ -267,6 +270,17 @@ def resolve_source(self, item):

return self.search_for_source(item)

def resolve_source_to_path(self, file_name):
"""Given ``item`` from a Bundle's contents, this has to
return the final value to use, usually an absolute
filesystem path. Unlike :meth:`search_for_source` this
will only return the first matching path it finds.
"""
source = self.resolve_source(file_name)
if isinstance(source, list):
return source[0]
return source

def resolve_output_to_path(self, target, bundle):
"""Given ``target``, this has to return the absolute
filesystem path to which the output file of ``bundle``
Expand Down Expand Up @@ -339,6 +353,7 @@ class BaseEnvironment(object):
def __init__(self, **config):
self._named_bundles = {}
self._anon_bundles = []
self.external_assets = None
self._config = self.config_storage_class(self)
self.resolver = self.resolver_class(self)

Expand Down Expand Up @@ -406,7 +421,7 @@ def register(self, name, *args, **kwargs):
if len(args) == 0:
raise TypeError('at least two arguments are required')
else:
if len(args) == 1 and not kwargs and isinstance(args[0], Bundle):
if len(args) == 1 and not kwargs and (isinstance(args[0], Bundle) or isinstance(args[0], ExternalAssets)):
bundle = args[0]
else:
bundle = Bundle(*args, **kwargs)
Expand Down
12 changes: 10 additions & 2 deletions src/webassets/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
__all__ = ('BundleError', 'BuildError', 'FilterError',
'EnvironmentError', 'ImminentDeprecationWarning')
__all__ = ('BundleError', 'BuildError', 'ContainerError', 'FilterError',
'EnvironmentError', 'ExternalAssetsError', 'ImminentDeprecationWarning', )


class EnvironmentError(Exception):
pass


class ExternalAssetsError(Exception):
pass


class ContainerError(Exception):
pass


class BundleError(Exception):
pass

Expand Down
Loading