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)
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 ''
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)
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))