예제 #1
0
class event(compat.metaclass(metaevent, object)):
    """This helper class provides an easy mechanism to give user feedback of
    created, changed or deleted files.  As side-effect every it allows you to
    register your own functions to these events.

    Acrylamid has the following, fairly self-explanatory events: ``create``,
    ``update``, ``skip``, ``identical`` and ``remove``. A callback receives
    the current namespace and the path. The namespace might be None if not
    specified by the originator, but it is recommended to achieve a informal
    standard:

       * the views supply their lowercase name such as ``'entry'`` or
         ``'archive'`` as namespace.
       * asset generation uses the ``'assets'`` namespace.
       * the init, import and new task use their name as namespace.

    To make this clear, the following example just records all newly created
    items from the entry view:

    .. code-block:: python

        from acrylamid.hepers import event

        skipped = []

        def callback(ns, path):

            if ns == 'entry':
                skipped.add(path)

        event.register(callback, to=['create'])

    .. Note:: This class is a singleton and should not be initialized

    .. method:: count(event)

       :param event: count calls of this particular event
       :type event: string"""

    # intercept these event
    events = ('create', 'update', 'remove', 'skip', 'identical')

    callbacks = defaultdict(list)
    counter = defaultdict(int)

    def __init__(self):
        raise TypeError("You can't construct event.")

    def register(self, callback, to=[]):
        """Register a callback to a list of events. Everytime the event
        eventuates, your callback gets called with all arguments of this
        particular event handler.

        :param callback: a function
        :param to: a list of events when your function gets called"""

        for item in to:
            event.callbacks[item].append(callback)

    def count(self, event):
        return self.counter.get(event, 0)

    def reset(self):
        for key in self.counter:
            self.counter[key] = 0

    def create(self, path, ctime=None):
        if ctime:
            log.info("create  [%.2fs] %s", ctime, path)
        else:
            log.info("create  %s", path)

    def update(self, path, ctime=None):
        if ctime:
            log.info("update  [%.2fs] %s", ctime, path)
        else:
            log.info("update  %s", path)

    def skip(self, path):
        log.skip("skip  %s", path)

    def identical(self, path):
        log.skip("identical  %s", path)

    def remove(self, path):
        log.info("remove  %s", path)
예제 #2
0
class Filter(compat.metaclass(meta, object)):
    """All text transformation is done via filters. A filter takes some text and
    returns it modified or untouched. Per default custom filters are stored in
    ``filters/`` directory inside your blog. On startup, Acrylamid will parse this
    plugin, report accidential syntax errors and uses this filter if required.

    .. code-block:: python

        from acrylamid.filters import Filter

        class Example(Filter):

            match = ['keyword', 'another']

            def transform(self, content, entry, *args):
                return content

    This is a minimal filter implementation that does nothing but returning
    the content that you can apply with ``filter: keyword``. A Filter may
    provide an :func:`init` that gets called once before we apply
    :func:`transform` to the content.

    .. attribute:: version

       Current version of this filter. If you made fundamental changes to your
       filter you can increment the version and all cached entries using that
       filter will recompile automatically on next run.

    .. attribute:: priority

       A filter chain is sorted by priority, so if you do textual modification
       you should have a priority ≥ 70.0 (default for Markdown, reST and so
       on).

    .. attribute:: match

       A list of strings or regular expressions (mixed works too) that will
       match this filter and uses this in the rendering process.

    .. attribute:: conflicts

       A list of strings (no regular expressions!) that describe conflicting
       :doc:`filters`. For example conflicts Markdown with ``['rst', 'plain',
       'textile']``. It is sufficient that one filter provides conflicting
       filters.

    .. attribute:: uses

       Override this property to include configuration and/or environment
       parameters. They are used to determine whether a cache object is still
       valid or not.

       You don't have to include configuration variables within the namespace
       of the filter yourself, as ``conf.fetch(self.cname)`` is automatically
       included into the filter hash.

    .. method:: init(self, conf, env)

       At demand initialization. A filter gets only initialized when he's
       actually used. This part is executed only once before :func:`transform`
       and should be used to import plugins or set some constants. Note that you
       may also check explicitly for ImportErrors from a statement like ``import
       foo`` that will not throw an :class:`ImportError` because we delay the
       actual import. Just make write ``foo.bar`` in :func:`init` and when it
       throws an ImportError, it is automatically handled.

       Have a look at ``acrylamid/filters/md.py`` or ``acrylamid/filters/typography.py``
       for example implementations.

       :param conf: :doc:`conf.py` dictionary
       :param env: environment dictionary

    .. method:: transform(self, content, entry, *args)

       Modify the content and return it. Each continuous transformation is
       automatically saved to disk (= caching). Don't import modules here,
       use module space or :func:`init` for that.

       :param content: a text you can modify
       :param entry: current :class:`readers.Entry`
       :param args: a list of additional arguments
    """

    initialized = False
    conflicts = []
    priority = 50.0
    version = 1

    def __init__(self, conf, env, fname, *args):

        self.conf = conf
        self.env = env

        self.cname = self.__class__.__name__.lower()  # common name
        self.name = fname
        self.args = args

        # precalculate __hash__ because we need it quite often in tree
        self.hv = helpers.hash(
            self.cname, tuple(self.args), self.version, self.priority,
            helpers.hash(self.uses), helpers.hash(self.conf.fetch(self.cname + '_')))

    def __repr__(self):
        return "<%s@%s %2.f:%s>" % (self.cname, self.version, self.priority, self.name)

    def __hash__(self):
        return self.hv

    def __eq__(self, other):
        return True if hash(other) == hash(self) else False

    @property
    def uses(self):
        return ''
예제 #3
0
class Reader(compat.metaclass(abc.ABCMeta, object)):
    """This class represents a single entry. Every property from this class is
    available during templating including custom key-value pairs from the
    header. The formal structure is first a YAML with some key/value pairs and
    then the actual content. For example::

        ---
        title: My Title
        date: 12.04.2012, 14:12
        tags: [some, values]

        custom: key example
        image: /path/to/my/image.png
        ---

        Here we start!

    Where you can access the image path via ``entry.image``.

    For convenience Acrylamid maps "filter" and "tag" automatically to "filters"
    and "tags" and also converts a single string into an array containing only
    one string.

    :param filename: valid path to an entry
    :param conf: acrylamid configuration

    .. attribute:: lang

       Language used in this article. This is important for the hyphenation pattern."""
    def __init__(self, conf, meta):

        self.props = Metadata((k, v) for k, v in iteritems(conf) if k in [
            'author', 'lang', 'email', 'date_format', 'entry_permalink',
            'page_permalink'
        ])

        self.props.update(meta)
        self.type = meta.get('type', 'entry')

        # redirect singular -> plural
        for key, to in [('tag', 'tags'), ('filter', 'filters'),
                        ('template', 'layout')]:
            if key in self.props:
                self.props.redirect(key, to)

        self.filters = self.props.get('filters', [])
        self.hashvalue = hash(self.filename, self.title, self.date.ctime())

    @abc.abstractmethod
    def __hash__(self):
        return

    @abc.abstractproperty
    def source(self):
        return

    @abc.abstractproperty
    def modified(self):
        return

    @abc.abstractproperty
    def lastmodified(self):
        return

    def getfilters(self):
        return self._filters

    def setfilters(self, filters):
        if isinstance(filters, string_types):
            filters = [filters]
        self._filters = FilterTree(filters)

    filters = property(getfilters, setfilters)

    def gettype(self):
        """="Type of this entry. Can be either ``'entry'`` or ``'page'``"""
        return self._type

    def settype(self, value):
        if value not in ('entry', 'page'):
            raise ValueError("item type must be 'entry' or 'page'")
        self._type = value

    type = property(gettype, settype, doc=gettype.__doc__)

    def hasproperty(self, prop):
        """Test whether BaseEntry has prop in `self.props`."""
        return prop in self.props

    @property
    def date(self):
        return datetime.now()

    def __iter__(self):
        for key in self.props:
            yield key

        for key in (attr for attr in dir(self) if not attr.startswith('_')):
            yield key

    def __contains__(self, other):
        return other in self.props or other in self.__dict__

    def __getattr__(self, attr):
        try:
            return self.props[attr]
        except KeyError:
            raise AttributeError(attr)

    __getitem__ = lambda self, attr: getattr(self, attr)
예제 #4
0
class Base(metaclass(abc.ABCMeta, View)):

    priority = 75.0

    @abc.abstractproperty
    def type(self):
        return None

    def init(self, conf, env, template='main.html'):
        self.template = template

    def next(self, entrylist, i):
        return None

    def prev(self, entrylist, i):
        return None

    def generate(self, conf, env, data):

        pathes, entrylist = set(), data[self.type]
        unmodified = not env.modified and not conf.modified

        for i, entry in enumerate(entrylist):

            if entry.hasproperty('permalink'):
                path = joinurl(conf['output_dir'], entry.permalink)
            else:
                path = joinurl(conf['output_dir'], expand(self.path, entry))

            if isfile(path) and path in pathes:
                try:
                    os.remove(path)
                finally:
                    other = [
                        e.filename for e in entrylist
                        if e is not entry and e.permalink == entry.permalink
                    ][0]
                    log.error("title collision %s caused by %s and %s",
                              entry.permalink, entry.filename, other)
                    raise SystemExit

            pathes.add(path)
            next, prev = self.next(entrylist, i), self.prev(entrylist, i)

            # per-entry template
            tt = env.engine.fromfile(env,
                                     entry.props.get('layout', self.template))

            if all([
                    isfile(path), unmodified, not tt.modified,
                    not entry.modified, not modified(*references(entry))
            ]):
                event.skip(self.name, path)
            else:
                html = tt.render(conf=conf,
                                 entry=entry,
                                 env=union(
                                     env,
                                     entrylist=[entry],
                                     type=self.__class__.__name__.lower(),
                                     prev=prev,
                                     next=next,
                                     route=expand(self.path, entry)))
                yield html, path

            # check if any resources need to be moved
            if entry.hasproperty('copy'):
                for res_src in entry.resources:
                    res_dest = join(dirname(path), basename(res_src))
                    # Note, presence of res_src check in FileReader.getresources
                    if isfile(res_dest
                              ) and getmtime(res_dest) > getmtime(res_src):
                        event.skip(self.name, res_dest)
                        continue
                    try:
                        fp = io.open(res_src, 'rb')
                        # use mkfile rather than yield so different ns can be specified (and filtered by sitemap)
                        mkfile(fp,
                               res_dest,
                               ns='resource',
                               force=env.options.force,
                               dryrun=env.options.dryrun)
                    except IOError as e:
                        log.warn(
                            "Failed to copy resource '%s' whilst processing '%s' (%s)"
                            % (res_src, entry.filename, e.strerror))