def test_works(self): t = FilterTree() t.add([1, 2, 5], 'foo') t.add([1, 2, 3, 5], 'bar') t.add([7, ], 'baz') assert list(t.iter('foo')) == [[1, 2], [5, ]] assert list(t.iter('bar')) == [[1, 2], [3, 5]] assert list(t.iter('baz')) == [[7, ], ]
def works(self): t = FilterTree() t.add([1, 2, 5], 'foo') t.add([1, 2, 3, 5], 'bar') t.add([7, ], 'baz') assert list(t.iter('foo')) == [[1, 2], [5, ]] assert list(t.iter('bar')) == [[1, 2], [3, 5]] assert list(t.iter('baz')) == [[7, ], ]
def __init__(self, path, conf): self.filename = path self.mtime = os.path.getmtime(path) self.props = NestedProperties((k, v) for k, v in conf.iteritems() if k in ['author', 'lang', 'encoding', 'date_format', 'permalink_format', 'email']) native = conf.get('metastyle', '').lower() == 'native' with io.open(path, 'r', encoding=conf['encoding'], errors='replace') as fp: if native and any(filter(lambda ext: path.endswith(ext), ['.md', '.mkdown'])): i, meta = markdownstyle(fp) # elif native and any(filter(lambda ext: path.endswith(ext), ['.rst', '.rest'])): # i, meta = reststyle(fp) else: i, meta = yamlstyle(fp) # remap singular -> plural for key, to in {'tag': 'tags', 'filter': 'filters', 'static': 'draft'}.iteritems(): if key in meta: meta[to] = meta[key] del meta[key] self.offset = i self.props.update(meta) fx = self.props.get('filters', []) if isinstance(fx, basestring): fx = [fx] self.filters = FilterTree(fx)
def test_edge_cases(self): t = FilterTree() t.add([1, 2], 'foo') t.add([1, 2], 'bar') t.add([2, ], 'baz') assert list(t.iter('foo')) == [[1, 2], ] assert list(t.iter('bar')) == [[1, 2], ] assert list(t.iter('baz')) == [[2, ], ]
def edge_cases(self): t = FilterTree() t.add([1, 2], 'foo') t.add([1, 2], 'bar') t.add([2, ], 'baz') assert list(t.iter('foo')) == [[1, 2], ] assert list(t.iter('bar')) == [[1, 2], ] assert list(t.iter('baz')) == [[2, ], ]
def __init__(self, filename, conf): """parsing FileEntry's YAML header.""" self.filename = filename self.mtime = os.path.getmtime(filename) self.props = dict((k, v) for k, v in conf.iteritems() if k in ['author', 'lang', 'encoding', 'date_format', 'permalink_format', 'email']) i, props = read(filename, self.props['encoding'], remap={'tag': 'tags', 'filter': 'filters', 'static': 'draft'}) self.offset = i self.props.update(props) fx = self.props.get('filters', []) if isinstance(fx, basestring): fx = [fx] self.filters = FilterTree(fx)
def path(self): t = FilterTree() t.add([1, 3, 4, 7], 'foo') assert t.path('foo') == [1, 3, 4, 7]
def setfilters(self, filters): if isinstance(filters, string_types): filters = [filters] self._filters = FilterTree(filters)
def test_path(self): t = FilterTree() t.add([1, 3, 4, 7], 'foo') assert t.path('foo') == [1, 3, 4, 7]
class FileEntry: """This class gets it's data and metadata from the file specified by the filename argument. During templating, every (cached) property is available as well as additional key, value pairs defined in the YAML header. Note that *tag* is automatically mapped to *tags*, *filter* to *filters* and *static* to *draft*. If you have something like :: --- title: Foo image: /path/to/my/image.png --- it is available in jinja2 templates as entry.image""" __keys__ = ['permalink', 'date', 'year', 'month', 'day', 'filters', 'tags', 'title', 'author', 'content', 'description', 'lang', 'draft', 'extension', 'slug'] def __init__(self, filename, conf): """parsing FileEntry's YAML header.""" self.filename = filename self.mtime = os.path.getmtime(filename) self.props = dict((k, v) for k, v in conf.iteritems() if k in ['author', 'lang', 'encoding', 'date_format', 'permalink_format', 'email']) i, props = read(filename, self.props['encoding'], remap={'tag': 'tags', 'filter': 'filters', 'static': 'draft'}) self.offset = i self.props.update(props) fx = self.props.get('filters', []) if isinstance(fx, basestring): fx = [fx] self.filters = FilterTree(fx) def __repr__(self): return "<FileEntry f'%s'>" % self.filename @cached_property def permalink(self): """Actual permanent link, depends on entry's property and ``permalink_format``. If you set permalink in the YAML header, we use this as permalink otherwise the URL without trailing *index.html.*""" try: return self.props['permalink'] except KeyError: return expand(self.props['permalink_format'].rstrip('index.html'), self) @cached_property def date(self): """return :class:`datetime.datetime` object. Either converted from given key and ``date_format`` or fallback to modification timestamp of the file.""" # alternate formats from pelican.utils, thank you! # https://github.com/ametaireau/pelican/blob/master/pelican/utils.py formats = ['%Y-%m-%d %H:%M', '%Y/%m/%d %H:%M', '%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%Y-%d-%m', # Weird ones '%d/%m/%Y', '%d.%m.%Y', '%d.%m.%Y %H:%M', '%Y-%m-%d %H:%M:%S'] if 'date' not in self.props: log.warn("using mtime from %r" % self.filename) return datetime.fromtimestamp(self.mtime) string = re.sub(' +', ' ', self.props['date']) formats.insert(0, self.props['date_format']) for date_format in formats: try: return datetime.strptime(string, date_format) except ValueError: pass else: raise AcrylamidException("%r is not a valid date" % string) @property def year(self): """entry's year (Integer)""" return self.date.year @property def month(self): """entry's month (Integer)""" return self.date.month @property def day(self): """entry's day (Integer)""" return self.date.day @property def tags(self): """per-post list of applied tags, if any. If you applied a single string it is used as one-item array.""" fx = self.props.get('tags', []) if isinstance(fx, basestring): return [fx] return fx @property def title(self): """entry's title.""" return self.props.get('title', 'No Title!') @property def author(self): """entry's author as set in entry or from conf.py if unset""" return self.props['author'] @property def email(self): """the author's email address""" return self.props['email'] @property def draft(self): """If set to True, the entry will not appear in articles, index, feed and tag view.""" return True if self.props.get('draft', False) else False @property def lang(self): return self.props['lang'] @property def extension(self): """filename's extension without leading dot""" return os.path.splitext(self.filename)[1][1:] @property def source(self): with io.open(self.filename, 'r', encoding=self.props['encoding'], errors='replace') as f: return u''.join(f.readlines()[self.offset:]).strip() @property def content(self): """Returns the processed content. This one of the core functions of acrylamid: it compiles incrementally the filter chain using a tree representation and saves final output or intermediates to cache, so we can rapidly re-compile the whole content. The cache is rather dumb: acrylamid can not determine wether it's abandoned or differs only in a single character. Thus, to minimize the overhead the content is zlib-compressed.""" # previous value pv = None # this is our cache filename path = join(cache.cache_dir, self.md5) # growing dependencies of the filter chain deps = [] for fxs in self.filters.iter(context=self.context): # extend dependencies deps.extend(fxs) # key where we save this filter chain key = md5(*deps) try: rv = cache.get(path, key, mtime=self.mtime) if rv is None: res = self.source if pv is None else pv for f in fxs: res = f.transform(res, self, *f.args) pv = cache.set(path, key, res) self.has_changed = True else: pv = rv except (IndexError, AttributeError): # jinja2 will ignore these Exceptions, better to catch them before traceback.print_exc(file=sys.stdout) return pv @property def slug(self): """ascii safe entry title""" return safeslug(self.title) @property def description(self): """first 50 characters from the source""" # XXX this is really poor return self.source[:50].strip() + '...' @cached_property def md5(self): return md5(self.filename, self.title, self.date) @property def has_changed(self): """Check wether the entry has changed using the following conditions: - cache file does not exist -> has changed - cache file does not contain required filter intermediate -> has changed - entry's file is newer than the cache's one -> has changed - otherwise -> not changed """ path = join(cache.cache_dir, self.md5) deps = [] for fxs in self.filters.iter(self.context): # extend filter dependencies deps.extend(fxs) if not cache.has_key(path, md5(*deps)): return True else: return getmtime(self.filename) > cache.getmtime(path) def keys(self): return list(iter(self)) def __iter__(self): for k in self.__keys__: yield k def __getitem__(self, key): if key in self.__keys__: return getattr(self, key) return self.props[key]
class FileEntry(BaseEntry): def __init__(self, path, conf): self.filename = path self.mtime = os.path.getmtime(path) self.props = NestedProperties((k, v) for k, v in conf.iteritems() if k in ['author', 'lang', 'encoding', 'date_format', 'permalink_format', 'email']) native = conf.get('metastyle', '').lower() == 'native' with io.open(path, 'r', encoding=conf['encoding'], errors='replace') as fp: if native and any(filter(lambda ext: path.endswith(ext), ['.md', '.mkdown'])): i, meta = markdownstyle(fp) # elif native and any(filter(lambda ext: path.endswith(ext), ['.rst', '.rest'])): # i, meta = reststyle(fp) else: i, meta = yamlstyle(fp) # remap singular -> plural for key, to in {'tag': 'tags', 'filter': 'filters', 'static': 'draft'}.iteritems(): if key in meta: meta[to] = meta[key] del meta[key] self.offset = i self.props.update(meta) fx = self.props.get('filters', []) if isinstance(fx, basestring): fx = [fx] self.filters = FilterTree(fx) def __repr__(self): return "<FileEntry f'%s'>" % self.filename @cached_property def date(self): """parse date value and return :class:`datetime.datetime` object, fallback to modification timestamp of the file if unset. You can set a ``DATE_FORMAT`` in your :doc:`../conf.py` otherwise Acrylamid tries several format strings and throws an exception if no pattern works. As shortcut you can access ``date.day``, ``date.month``, ``date.year`` via ``entry.day``, ``entry.month`` and ``entry.year``.""" # alternate formats from pelican.utils, thank you! # https://github.com/ametaireau/pelican/blob/master/pelican/utils.py formats = ['%Y-%m-%d %H:%M', '%Y/%m/%d %H:%M', '%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%Y-%d-%m', # Weird ones '%d/%m/%Y', '%d.%m.%Y', '%d.%m.%Y %H:%M', '%Y-%m-%d %H:%M:%S'] if 'date' not in self.props: log.warn("using mtime from %r" % self.filename) return Date.fromtimestamp(self.mtime) string = re.sub(' +', ' ', self.props['date']) formats.insert(0, self.props['date_format']) for date_format in formats: try: return Date.strptime(string, date_format) except ValueError: pass else: raise AcrylamidException("%r is not a valid date" % string) @property def extension(self): """Filename's extension without leading dot""" return os.path.splitext(self.filename)[1][1:] @property def source(self): """Returns the actual, unmodified content.""" with io.open(self.filename, 'r', encoding=self.props['encoding'], errors='replace') as f: return u''.join(f.readlines()[self.offset:]).strip() @cached_property def md5(self): return md5(self.filename, self.title, self.date) @property def content(self): # previous value res = self.source # growing dependencies of the filter chain deps = [] for fxs in self.filters.iter(context=self.context): # extend dependencies deps.extend(fxs) try: for f in fxs: res = f.transform(res, self, *f.args) except (IndexError, AttributeError): # jinja2 will ignore these Exceptions, better to catch them before traceback.print_exc(file=sys.stdout) return res @property def has_changed(self): return True @has_changed.setter def has_changed(self, value): self._has_changed = value
def setfilters(self, filters): if isinstance(filters, basestring): filters = [filters] self._filters = FilterTree(filters)