Exemplo n.º 1
0
class Version(
        six.with_metaclass(
            RegistryMetaclass(clazz=lambda: Version,
                              attribute='determine_version',
                              desc='a version implementation'))):
    """A Version class that can be assigned to the ``Environment.versioner``
    attribute.

    Given a bundle, this must determine its "version". This version can then
    be used in the output filename of the bundle, or appended to the url as a
    query string, in order to expire cached assets.

    A version could be a timestamp, a content hash, or a git revision etc.

    As a user, all you need to care about, in most cases, is whether you want
    to set the ``Environment.versioner`` attribute to ``hash`` or ``timestamp``.

    A single instance can be used with different environments.
    """
    def determine_version(self, bundle, ctx, hunk=None):
        """Return a string that represents the current version of the given
        bundle.

        This method is called on two separate occasions:

        1) After a bundle has been built and is about to be saved. If the
           output filename contains a placeholder, this method is asked for the
           version. This mode is indicated by the ``hunk`` argument being
           available.

        2) When a version is required for an already built file, either
           because:

              *) An URL needs to be constructed.
              *) It needs to be determined if a bundle needs an update.

           *This will only occur* if *no manifest* is used. If there is a
           manifest, it would be used to determine the version instead.

        Support for option (2) is optional. If not supported, then in those
        cases a manifest needs to be configured. ``VersionIndeterminableError``
        should be raised with a message why.
        """
        raise NotImplementedError()

    def set_version(self, bundle, ctx, filename, version):
        """Hook called after a bundle has been built. Some version classes
Exemplo n.º 2
0
class BaseUpdater(six.with_metaclass(RegistryMetaclass(
    clazz=lambda: BaseUpdater, attribute='needs_rebuild',
    desc='an updater implementation'))):
    """Base updater class.

    Child classes that define an ``id`` attribute are accessible via their
    string id in the configuration.

    A single instance can be used with different environments.
    """

    def needs_rebuild(self, bundle, env):
        """Returns ``True`` if the given bundle needs to be rebuilt,
        ``False`` otherwise.
        """
        raise NotImplementedError()

    def build_done(self, bundle, env):
        """This will be called once a bundle has been successfully built.
Exemplo n.º 3
0
class Manifest(
        six.with_metaclass(
            RegistryMetaclass(clazz=lambda: Manifest,
                              desc='a manifest implementation'))):
    """Persists information about the versions bundles are at.

    The Manifest plays a role only if you insert the bundle version in your
    output filenames, or append the version as a querystring to the url (via
    the url_expire option). It serves two purposes:

        - Without a manifest, it may be impossible to determine the version
          at runtime. In a deployed app, the media files may be stored on
          a different server entirely, and be inaccessible from the application
          code. The manifest, if shipped with your application, is what still
          allows to construct the proper URLs.

        - Even if it were possible to determine the version at runtime without
          a manifest, it may be a costly process, and using a manifest may
          give you better performance. If you use a hash-based version for
          example, this hash would need to be recalculated every time a new
          process is started. (*)

    (*) It needs to happen only once per process, because Bundles are smart
        enough to cache their own version in memory.

    A special case is the ``Environment.auto_build`` option. A manifest
    implementation should re-read its data from its out-of-process data
    source on every request, if ``auto_build`` is enabled. Otherwise, if your
    application is served by multiple processes, then after an automatic
    rebuild in one process all other processes would continue to serve an old
    version of the file (or attach an old version to the query string).

    A manifest instance is currently not guaranteed to function correctly
    with multiple Environment instances.
    """
    def remember(self, bundle, ctx, version):
        raise NotImplementedError()

    def query(self, bundle, ctx):
        raise NotImplementedError()
Exemplo n.º 4
0
class ExternalTool(six.with_metaclass(ExternalToolMetaclass, Filter)):
    """Subclass that helps creating filters that need to run an external
    program.

    You are encouraged to use this when possible, as it helps consistency.

    In the simplest possible case, subclasses only have to define one or more
    of the following attributes, without needing to write any code:

    ``argv``
       The command line that will be passed to subprocess.Popen. New-style
       format strings can be used to access all kinds of data: The arguments
       to the filter method, as well as the filter instance via ``self``:

            argv = ['{self.binary}', '--input', '{source_path}', '--cwd',
                    '{self.env.directory}']

    ``method``
        The filter method to implement. One of ``input``, ``output`` or
        ``open``.
    """

    argv = []
    method = None

    def open(self, out, source_path, **kw):
        self._evaluate([out, source_path], kw, out)

    def input(self, _in, out, **kw):
        self._evaluate([_in, out], kw, out, _in)

    def output(self, _in, out, **kw):
        self._evaluate([_in, out], kw, out, _in)

    def _evaluate(self, args, kwargs, out, data=None):
        # For now, still support Python 2.5, but the format strings in argv
        # are not supported (making the feature mostly useless). For this
        # reason none of the builtin filters is using argv currently.
        if hasattr(str, 'format'):
            # Add 'self' to the keywords available in format strings
            kwargs = kwargs.copy()
            kwargs.update({'self': self})

            # Resolve all the format strings in argv
            def replace(arg):
                try:
                    return arg.format(*args, **kwargs)
                except KeyError as e:
                    # Treat "output" and "input" variables special, they
                    # are dealt with in :meth:`subprocess` instead.
                    if e.args[0] not in ('input', 'output'):
                        raise
                    return arg

            argv = list(map(replace, self.argv))
        else:
            argv = self.argv
        self.subprocess(argv, out, data=data)

    @classmethod
    def subprocess(cls, argv, out, data=None):
        """Execute the commandline given by the list in ``argv``.

        If a byestring is given via ``data``, it is piped into data.

        ``argv`` may contain two placeholders:

        ``{input}``
            If given, ``data`` will be written to a temporary file instead
            of data. The placeholder is then replaced with that file.

        ``{output}``
            Will be replaced by a temporary filename. The return value then
            will be the content of this file, rather than stdout.
        """
        class tempfile_on_demand(object):
            def __repr__(self):
                if not hasattr(self, 'filename'):
                    self.fd, self.filename = tempfile.mkstemp()
                return self.filename

            @property
            def created(self):
                return hasattr(self, 'filename')

        # Replace input and output placeholders
        input_file = tempfile_on_demand()
        output_file = tempfile_on_demand()
        if hasattr(str, 'format'):  # Support Python 2.5 without the feature
            argv = list(
                map(
                    lambda item: item.format(input=input_file,
                                             output=output_file), argv))

        try:
            data = (data.read() if hasattr(data, 'read') else data)
            if data is not None:
                data = data.encode('utf-8')

            if input_file.created:
                if not data:
                    raise ValueError(
                        '{input} placeholder given, but no data passed')
                with os.fdopen(input_file.fd, 'wb') as f:
                    f.write(data)
                    # No longer pass to stdin
                    data = None

            proc = subprocess.Popen(
                argv,
                # we cannot use the in/out streams directly, as they might be
                # StringIO objects (which are not supported by subprocess)
                stdout=subprocess.PIPE,
                stdin=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=os.name == 'nt')
            stdout, stderr = proc.communicate(data)
            if proc.returncode:
                raise FilterError(
                    '%s: subprocess returned a non-success result code: '
                    '%s, stdout=%s, stderr=%s' %
                    (cls.name
                     or cls.__name__, proc.returncode, stdout, stderr))
            else:
                if output_file.created:
                    with open(output_file.filename, 'rb') as f:
                        out.write(f.read().decode('utf-8'))
                else:
                    out.write(stdout.decode('utf-8'))
        finally:
            if output_file.created:
                os.unlink(output_file.filename)
            if input_file.created:
                os.unlink(input_file.filename)