Mercurial > piecrust2
changeset 242:f130365568ff
internal: Code reorganization to put less stuff in `sources.base`.
Interfaces that sources can implement are in `sources.interfaces`. The default
page source is in `sources.default`. The `SimplePageSource` is gone since most
subclasses only wanted to do *some* stuff the same, but *lots* of stuff
slightly different. I may have to revisit the code to extract exactly the code
that's in common.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 18 Feb 2015 18:35:03 -0800 |
parents | 85a6c7ba5e3b |
children | 26e59f837558 d9d5c5de02a8 |
files | piecrust/commands/builtin/scaffolding.py piecrust/data/base.py piecrust/data/iterators.py piecrust/data/linker.py piecrust/data/paginator.py piecrust/data/provider.py piecrust/plugins/builtin.py piecrust/sources/array.py piecrust/sources/autoconfig.py piecrust/sources/base.py piecrust/sources/default.py piecrust/sources/interfaces.py piecrust/sources/mixins.py piecrust/sources/pageref.py piecrust/sources/posts.py piecrust/sources/prose.py piecrust/taxonomies.py tests/test_data_paginator.py tests/test_sources_autoconfig.py tests/test_sources_base.py |
diffstat | 20 files changed, 693 insertions(+), 471 deletions(-) [+] |
line wrap: on
line diff
--- a/piecrust/commands/builtin/scaffolding.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/commands/builtin/scaffolding.py Wed Feb 18 18:35:03 2015 -0800 @@ -9,7 +9,8 @@ from piecrust import RESOURCES_DIR from piecrust.chefutil import print_help_item from piecrust.commands.base import ExtendableChefCommand, ChefCommandExtension -from piecrust.sources.base import IPreparingSource, MODE_CREATING +from piecrust.sources.base import MODE_CREATING +from piecrust.sources.interfaces import IPreparingSource from piecrust.uriutil import multi_replace
--- a/piecrust/data/base.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/data/base.py Wed Feb 18 18:35:03 2015 -0800 @@ -7,29 +7,6 @@ logger = logging.getLogger(__name__) -class IPaginationSource(object): - """ Defines the interface for a source that can be used as the data - for an iterator or a pagination. - """ - def getItemsPerPage(self): - raise NotImplementedError() - - def getSourceIterator(self): - raise NotImplementedError() - - def getSorterIterator(self, it): - raise NotImplementedError() - - def getTailIterator(self, it): - raise NotImplementedError() - - def getPaginationFilter(self, page): - raise NotImplementedError() - - def getSettingAccessor(self): - raise NotImplementedError() - - class LazyPageConfigData(object): """ An object that represents the configuration header of a page, but also allows for additional data. It's meant to be exposed
--- a/piecrust/data/iterators.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/data/iterators.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,7 +1,7 @@ import logging -from piecrust.data.base import IPaginationSource from piecrust.data.filters import PaginationFilter from piecrust.events import Event +from piecrust.sources.interfaces import IPaginationSource logger = logging.getLogger(__name__)
--- a/piecrust/data/linker.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/data/linker.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,8 +1,9 @@ import logging import collections -from piecrust.data.base import PaginationData, IPaginationSource +from piecrust.data.base import PaginationData from piecrust.data.iterators import PageIterator -from piecrust.sources.base import IListableSource, build_pages +from piecrust.sources.base import build_pages +from piecrust.sources.interfaces import IPaginationSource, IListableSource logger = logging.getLogger(__name__)
--- a/piecrust/data/paginator.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/data/paginator.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,9 +1,9 @@ import math import logging from werkzeug.utils import cached_property -from piecrust.data.base import IPaginationSource from piecrust.data.filters import PaginationFilter from piecrust.data.iterators import PageIterator +from piecrust.sources.interfaces import IPaginationSource logger = logging.getLogger(__name__)
--- a/piecrust/data/provider.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/data/provider.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,6 +1,6 @@ import time from piecrust.data.iterators import PageIterator -from piecrust.sources.base import ArraySource +from piecrust.sources.array import ArraySource class DataProvider(object):
--- a/piecrust/plugins/builtin.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/plugins/builtin.py Wed Feb 18 18:35:03 2015 -0800 @@ -28,7 +28,7 @@ from piecrust.processing.sass import SassProcessor from piecrust.processing.sitemap import SitemapProcessor from piecrust.processing.util import ConcatProcessor -from piecrust.sources.base import DefaultPageSource +from piecrust.sources.default import DefaultPageSource from piecrust.sources.posts import ( FlatPostsSource, ShallowPostsSource, HierarchyPostsSource) from piecrust.sources.autoconfig import (
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/sources/array.py Wed Feb 18 18:35:03 2015 -0800 @@ -0,0 +1,43 @@ +from piecrust.sources.base import PageSource +from piecrust.sources.mixins import SimplePaginationSourceMixin + + +class CachedPageFactory(object): + """ A `PageFactory` (in appearance) that already has a page built. + """ + def __init__(self, page): + self._page = page + + @property + def rel_path(self): + return self._page.rel_path + + @property + def metadata(self): + return self._page.source_metadata + + @property + def ref_spec(self): + return self._page.ref_spec + + @property + def path(self): + return self._page.path + + def buildPage(self): + return self._page + + +class ArraySource(PageSource, SimplePaginationSourceMixin): + def __init__(self, app, inner_source, name='array', config=None): + super(ArraySource, self).__init__(app, name, config or {}) + self.inner_source = inner_source + + @property + def page_count(self): + return len(self.inner_source) + + def getPageFactories(self): + for p in self.inner_source: + yield CachedPageFactory(p) +
--- a/piecrust/sources/autoconfig.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/sources/autoconfig.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,26 +1,32 @@ import re import os import os.path -import glob import logging from piecrust.configuration import ConfigurationError -from piecrust.data.iterators import SettingSortIterator from piecrust.sources.base import ( - SimplePageSource, IPreparingSource, SimplePaginationSourceMixin, - PageNotFoundError, InvalidFileSystemEndpointError, - PageFactory, MODE_CREATING, MODE_PARSING) + PageSource, PageFactory, InvalidFileSystemEndpointError) +from piecrust.sources.default import ( + filter_page_dirname, filter_page_filename) +from piecrust.sources.interfaces import IListableSource +from piecrust.sources.mixins import SimplePaginationSourceMixin logger = logging.getLogger(__name__) -class AutoConfigSourceBase(SimplePageSource, - SimplePaginationSourceMixin): +class AutoConfigSourceBase(PageSource, SimplePaginationSourceMixin, + IListableSource): """ Base class for page sources that automatically apply configuration settings to their generated pages based on those pages' paths. """ def __init__(self, app, name, config): super(AutoConfigSourceBase, self).__init__(app, name, config) + self.fs_endpoint = config.get('fs_endpoint', name) + self.fs_endpoint_path = os.path.join(self.root_dir, self.fs_endpoint) + self.supported_extensions = list( + app.config.get('site/auto_formats').keys()) + self.default_auto_format = app.config.get('site/default_auto_format') + self.capture_mode = config.get('capture_mode', 'path') if self.capture_mode not in ['path', 'dirname', 'filename']: raise ConfigurationError("Capture mode in source '%s' must be " @@ -28,46 +34,57 @@ name) def buildPageFactories(self): + logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path) if not os.path.isdir(self.fs_endpoint_path): raise InvalidFileSystemEndpointError(self.name, self.fs_endpoint_path) for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path): - if not filenames: - continue - rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path) + dirnames[:] = list(filter(filter_page_dirname, dirnames)) # If `capture_mode` is `dirname`, we don't need to recompute it # for each filename, so we do it here. if self.capture_mode == 'dirname': - config = self.extractConfigFragment(rel_dirpath) + config = self._extractConfigFragment(rel_dirpath) - for f in filenames: + for f in filter(filter_page_filename, filenames): if self.capture_mode == 'path': path = os.path.join(rel_dirpath, f) - config = self.extractConfigFragment(path) + config = self._extractConfigFragment(path) elif self.capture_mode == 'filename': - config = self.extractConfigFragment(f) + config = self._extractConfigFragment(f) fac_path = f if rel_dirpath != '.': fac_path = os.path.join(rel_dirpath, f) - slug = self.makeSlug(rel_dirpath, f) + slug = self._makeSlug(fac_path) metadata = { 'slug': slug, 'config': config} yield PageFactory(self, fac_path, metadata) - def makeSlug(self, rel_dirpath, filename): + def resolveRef(self, ref_path): + return os.path.normpath( + os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/"))) + + def listPath(self, rel_path): raise NotImplementedError() - def extractConfigFragment(self, rel_path): + def getDirpath(self, rel_path): + return os.path.dirname(rel_path) + + def getBasename(self, rel_path): + filename = os.path.basename(rel_path) + name, _ = os.path.splitext(filename) + return name + + def _makeSlug(self, rel_path): raise NotImplementedError() - def findPagePath(self, metadata, mode): + def _extractConfigFragment(self, rel_path): raise NotImplementedError() @@ -88,13 +105,13 @@ self.supported_extensions = list( app.config.get('site/auto_formats').keys()) - def makeSlug(self, rel_dirpath, filename): - slug, ext = os.path.splitext(filename) + def _makeSlug(self, rel_path): + slug, ext = os.path.splitext(os.path.basename(rel_path)) if ext.lstrip('.') not in self.supported_extensions: slug += ext return slug - def extractConfigFragment(self, rel_path): + def _extractConfigFragment(self, rel_path): if rel_path == '.': values = [] else: @@ -126,10 +143,32 @@ if slug == metadata['slug']: path = os.path.join(dirpath, f) rel_path = os.path.relpath(path, self.fs_endpoint_path) - config = self.extractConfigFragment(dirpath) + config = self._extractConfigFragment(rel_path) metadata = {'slug': slug, 'config': config} return rel_path, metadata + def listPath(self, rel_path): + rel_path = rel_path.lstrip('\\/') + path = os.path.join(self.fs_endpoint_path, rel_path) + names = sorted(os.listdir(path)) + items = [] + for name in names: + if os.path.isdir(os.path.join(path, name)): + if filter_page_dirname(name): + rel_subdir = os.path.join(rel_path, name) + items.append((True, name, rel_subdir)) + else: + if filter_page_filename(name): + cur_rel_path = os.path.join(rel_path, name) + slug = self._makeSlug(cur_rel_path) + config = self._extractConfigFragment(cur_rel_path) + metadata = {'slug': slug, 'config': config} + fac = PageFactory(self, cur_rel_path, metadata) + + name, _ = os.path.splitext(name) + items.append((False, name, fac)) + return items + class OrderedPageSource(AutoConfigSourceBase): """ A page source that assigns an "order" to its pages based on a @@ -141,70 +180,140 @@ re_pattern = re.compile(r'(^|/)(?P<num>\d+)_') def __init__(self, app, name, config): - config['capture_mode'] = 'filename' + config['capture_mode'] = 'path' super(OrderedPageSource, self).__init__(app, name, config) self.setting_name = config.get('setting_name', 'order') self.default_value = config.get('default_value', 0) self.supported_extensions = list( app.config.get('site/auto_formats').keys()) - def makeSlug(self, rel_dirpath, filename): - slug, ext = os.path.splitext(filename) - if ext.lstrip('.') not in self.supported_extensions: - slug += ext - slug = self.re_pattern.sub(r'\1', slug) - slug = os.path.join(rel_dirpath, slug).replace('\\', '/') - if slug.startswith('./'): - slug = slug[2:] - return slug - - def extractConfigFragment(self, rel_path): - m = self.re_pattern.match(rel_path) - if m is not None: - val = int(m.group('num')) - else: - val = self.default_value - return {self.setting_name: val} - def findPagePath(self, metadata, mode): uri_path = metadata.get('slug', '') - if uri_path != '': - uri_parts = ['*_%s' % p for p in uri_path.split('/')] - else: - uri_parts = ['*__index'] - uri_parts.insert(0, self.fs_endpoint_path) - path = os.path.join(*uri_parts) + if uri_path == '': + uri_path = '_index' - _, ext = os.path.splitext(uri_path) - if ext == '': - path += '.*' + path = self.fs_endpoint_path + uri_parts = uri_path.split('/') + for i, p in enumerate(uri_parts): + if i == len(uri_parts) - 1: + # Last part, this is the filename. We need to check for either + # the name, or the name with the prefix, but also handle a + # possible extension. + p_pat = r'(\d+_)?' + re.escape(p) + + _, ext = os.path.splitext(uri_path) + if ext == '': + p_pat += r'\.[\w\d]+' - possibles = glob.glob(path) - - if len(possibles) == 0: - return None, None + found = False + for name in os.listdir(path): + if re.match(p_pat, name): + path = os.path.join(path, name) + found = True + break + if not found: + return None, None + else: + # Find each sub-directory. It can either be a directory with + # the name itself, or the name with a number prefix. + p_pat = r'(\d+_)?' + re.escape(p) + found = False + for name in os.listdir(path): + if re.match(p_pat, name): + path = os.path.join(path, name) + found = True + break + if not found: + return None, None - if len(possibles) > 1: - raise Exception("More than one path matching: %s" % uri_path) - - path = possibles[0] fac_path = os.path.relpath(path, self.fs_endpoint_path) - - _, filename = os.path.split(path) - config = self.extractConfigFragment(filename) + config = self._extractConfigFragment(fac_path) metadata = {'slug': uri_path, 'config': config} return fac_path, metadata def getSorterIterator(self, it): accessor = self.getSettingAccessor() - return SettingSortIterator(it, self.setting_name, - value_accessor=accessor) + return OrderTrailSortIterator(it, self.setting_name + '_trail', + value_accessor=accessor) + + def listPath(self, rel_path): + rel_path = rel_path.lstrip('/') + path = self.fs_endpoint_path + if rel_path != '': + parts = rel_path.split('/') + for p in parts: + p_pat = r'(\d+_)?' + re.escape(p) + for name in os.listdir(path): + if re.match(p_pat, name): + path = os.path.join(path, name) + break + else: + raise Exception("No such path: %s" % rel_path) + + items = [] + names = sorted(os.listdir(path)) + for name in names: + if os.path.isdir(os.path.join(path, name)): + if filter_page_dirname(name): + rel_subdir = os.path.join(rel_path, name) + items.append((True, name, rel_subdir)) + else: + if filter_page_filename(name): + slug = self._makeSlug(os.path.join(rel_path, name)) + + fac_path = name + if rel_path != '.': + fac_path = os.path.join(rel_path, name) + fac_path = fac_path.replace('\\', '/') + + config = self._extractConfigFragment(fac_path) + metadata = {'slug': slug, 'config': config} + fac = PageFactory(self, fac_path, metadata) + + name, _ = os.path.splitext(name) + items.append((False, name, fac)) + return items + + def _makeSlug(self, rel_path): + slug, ext = os.path.splitext(rel_path) + if ext.lstrip('.') not in self.supported_extensions: + slug += ext + slug = self.re_pattern.sub(r'\1', slug) + return slug + + def _extractConfigFragment(self, rel_path): + values = [] + for m in self.re_pattern.finditer(rel_path): + val = int(m.group('num')) + values.append(val) + + if len(values) == 0: + values.append(self.default_value) + + return { + self.setting_name: values[-1], + self.setting_name + '_trail': values} def _populateMetadata(self, rel_path, metadata, mode=None): _, filename = os.path.split(rel_path) - config = self.extractConfigFragment(filename) + config = self._extractConfigFragment(filename) metadata['config'] = config slug = metadata['slug'] metadata['slug'] = self.re_pattern.sub(r'\1', slug) + +class OrderTrailSortIterator(object): + def __init__(self, it, trail_name, value_accessor): + self.it = it + self.trail_name = trail_name + self.value_accessor = value_accessor + + def __iter__(self): + return iter(sorted(self.it, key=self._key_getter)) + + def _key_getter(self, item): + values = self.value_accessor(item, self.trail_name) + key = ''.join(values) + return key +
--- a/piecrust/sources/base.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/sources/base.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,11 +1,6 @@ -import re -import os -import os.path import logging from werkzeug.utils import cached_property from piecrust.configuration import ConfigurationError -from piecrust.data.base import IPaginationSource, PaginationData -from piecrust.data.filters import PaginationFilter from piecrust.page import Page @@ -23,19 +18,12 @@ logger = logging.getLogger(__name__) -page_ref_pattern = re.compile(r'(?P<src>[\w]+)\:(?P<path>.*?)(;|$)') - - def build_pages(app, factories): with app.env.page_repository.startBatchGet(): for f in factories: yield f.buildPage() -class PageNotFoundError(Exception): - pass - - class InvalidFileSystemEndpointError(Exception): def __init__(self, source_name, fs_endpoint): super(InvalidFileSystemEndpointError, self).__init__( @@ -75,120 +63,6 @@ return page -class CachedPageFactory(object): - """ A `PageFactory` (in appearance) that already has a page built. - """ - def __init__(self, page): - self._page = page - - @property - def rel_path(self): - return self._page.rel_path - - @property - def metadata(self): - return self._page.source_metadata - - @property - def ref_spec(self): - return self._page.ref_spec - - @property - def path(self): - return self._page.path - - def buildPage(self): - return self._page - - -class PageRef(object): - """ A reference to a page, with support for looking a page in different - realms. - """ - def __init__(self, app, page_ref): - self.app = app - self._page_ref = page_ref - self._paths = None - self._first_valid_path_index = -2 - self._exts = list(app.config.get('site/auto_formats').keys()) - - @property - def exists(self): - try: - self._checkPaths() - return True - except PageNotFoundError: - return False - - @property - def source_name(self): - self._checkPaths() - return self._paths[self._first_valid_path_index][0] - - @property - def source(self): - return self.app.getSource(self.source_name) - - @property - def rel_path(self): - self._checkPaths() - return self._paths[self._first_valid_path_index][1] - - @property - def path(self): - self._checkPaths() - return self._paths[self._first_valid_path_index][2] - - @property - def possible_rel_paths(self): - self._load() - return [p[1] for p in self._paths] - - @property - def possible_paths(self): - self._load() - return [p[2] for p in self._paths] - - def _load(self): - if self._paths is not None: - return - - it = list(page_ref_pattern.finditer(self._page_ref)) - if len(it) == 0: - raise Exception("Invalid page ref: %s" % self._page_ref) - - self._paths = [] - for m in it: - source_name = m.group('src') - source = self.app.getSource(source_name) - if source is None: - raise Exception("No such source: %s" % source_name) - rel_path = m.group('path') - path = source.resolveRef(rel_path) - if '%ext%' in rel_path: - for e in self._exts: - self._paths.append((source_name, - rel_path.replace('%ext%', e), - path.replace('%ext%', e))) - else: - self._paths.append((source_name, rel_path, path)) - - def _checkPaths(self): - if self._first_valid_path_index >= 0: - return - if self._first_valid_path_index == -1: - raise PageNotFoundError( - "No valid paths were found for page reference: %s" % - self._page_ref) - - self._load() - self._first_valid_path_index = -1 - for i, path_info in enumerate(self._paths): - if os.path.isfile(path_info[2]): - self._first_valid_path_index = i - break - - class PageSource(object): """ A source for pages, e.g. a directory with one file per page. """ @@ -235,11 +109,11 @@ def buildDataProvider(self, page, user_data): if self._provider_type is None: cls = next((pt for pt in self.app.plugin_loader.getDataProviders() - if pt.PROVIDER_NAME == self.data_type), - None) + if pt.PROVIDER_NAME == self.data_type), + None) if cls is None: - raise ConfigurationError("Unknown data provider type: %s" % - self.data_type) + raise ConfigurationError( + "Unknown data provider type: %s" % self.data_type) self._provider_type = cls return self._provider_type(self, page, user_data) @@ -250,230 +124,3 @@ return None return tax_pages.get(tax_name) - -class IPreparingSource: - def setupPrepareParser(self, parser, app): - raise NotImplementedError() - - def buildMetadata(self, args): - raise NotImplementedError() - - -class IListableSource: - def listPath(self, rel_path): - raise NotImplementedError() - - def getDirpath(self, rel_path): - raise NotImplementedError() - - def getBasename(self, rel_path): - raise NotImplementedError() - - -class SimplePaginationSourceMixin(IPaginationSource): - def getItemsPerPage(self): - return self.config['items_per_page'] - - def getSourceIterator(self): - return SourceFactoryIterator(self) - - def getSorterIterator(self, it): - return DateSortIterator(it) - - def getTailIterator(self, it): - return PaginationDataBuilderIterator(it) - - def getPaginationFilter(self, page): - conf = (page.config.get('items_filters') or - page.app.config.get('site/items_filters')) - if conf == 'none' or conf == 'nil' or conf == '': - conf = None - if conf is not None: - f = PaginationFilter() - f.addClausesFromConfig(conf) - return f - return None - - def getSettingAccessor(self): - return lambda i, n: i.config.get(n) - - -class ArraySource(PageSource, SimplePaginationSourceMixin): - def __init__(self, app, inner_source, name='array', config=None): - super(ArraySource, self).__init__(app, name, config or {}) - self.inner_source = inner_source - - @property - def page_count(self): - return len(self.inner_source) - - def getPageFactories(self): - for p in self.inner_source: - yield CachedPageFactory(p) - - -class SimplePageSource(PageSource, IListableSource, IPreparingSource, - SimplePaginationSourceMixin): - def __init__(self, app, name, config): - super(SimplePageSource, self).__init__(app, name, config) - self.fs_endpoint = config.get('fs_endpoint', name) - self.fs_endpoint_path = os.path.join(self.root_dir, self.fs_endpoint) - self.supported_extensions = list(app.config.get('site/auto_formats').keys()) - self.default_auto_format = app.config.get('site/default_auto_format') - - def buildPageFactories(self): - logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path) - if not os.path.isdir(self.fs_endpoint_path): - if self.ignore_missing_dir: - return - raise InvalidFileSystemEndpointError(self.name, self.fs_endpoint_path) - - for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path): - rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path) - dirnames[:] = list(filter(self._filterPageDirname, dirnames)) - for f in filter(self._filterPageFilename, filenames): - fac_path = f - if rel_dirpath != '.': - fac_path = os.path.join(rel_dirpath, f) - slug = self._makeSlug(fac_path) - metadata = {'slug': slug} - fac_path = fac_path.replace('\\', '/') - self._populateMetadata(fac_path, metadata) - yield PageFactory(self, fac_path, metadata) - - def resolveRef(self, ref_path): - return os.path.normpath( - os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/"))) - - def findPagePath(self, metadata, mode): - uri_path = metadata.get('slug', '') - if not uri_path: - uri_path = '_index' - path = os.path.join(self.fs_endpoint_path, uri_path) - _, ext = os.path.splitext(path) - - if mode == MODE_CREATING: - if ext == '': - path = '%s.%s' % (path, self.default_auto_format) - rel_path = os.path.relpath(path, self.fs_endpoint_path) - rel_path = rel_path.replace('\\', '/') - self._populateMetadata(rel_path, metadata, mode) - return rel_path, metadata - - if ext == '': - paths_to_check = [ - '%s.%s' % (path, e) - for e in self.supported_extensions] - else: - paths_to_check = [path] - for path in paths_to_check: - if os.path.isfile(path): - rel_path = os.path.relpath(path, self.fs_endpoint_path) - rel_path = rel_path.replace('\\', '/') - self._populateMetadata(rel_path, metadata, mode) - return rel_path, metadata - - return None, None - - def listPath(self, rel_path): - rel_path = rel_path.lstrip('\\/') - path = os.path.join(self.fs_endpoint_path, rel_path) - names = sorted(os.listdir(path)) - items = [] - for name in names: - if os.path.isdir(os.path.join(path, name)): - if self._filterPageDirname(name): - rel_subdir = os.path.join(rel_path, name) - items.append((True, name, rel_subdir)) - else: - if self._filterPageFilename(name): - slug = self._makeSlug(os.path.join(rel_path, name)) - metadata = {'slug': slug} - - fac_path = name - if rel_path != '.': - fac_path = os.path.join(rel_path, name) - fac_path = fac_path.replace('\\', '/') - - self._populateMetadata(fac_path, metadata) - fac = PageFactory(self, fac_path, metadata) - - name, _ = os.path.splitext(name) - items.append((False, name, fac)) - return items - - def getDirpath(self, rel_path): - return os.path.dirname(rel_path) - - def getBasename(self, rel_path): - filename = os.path.basename(rel_path) - name, _ = os.path.splitext(filename) - return name - - def setupPrepareParser(self, parser, app): - parser.add_argument('uri', help='The URI for the new page.') - - def buildMetadata(self, args): - return {'slug': args.uri} - - def _makeSlug(self, rel_path): - slug, ext = os.path.splitext(rel_path) - slug = slug.replace('\\', '/') - if ext.lstrip('.') not in self.supported_extensions: - slug += ext - if slug.startswith('./'): - slug = slug[2:] - if slug == '_index': - slug = '' - return slug - - def _filterPageDirname(self, d): - return not d.endswith('-assets') - - def _filterPageFilename(self, f): - return (f[0] != '.' and # .DS_store and other crap - f[-1] != '~' and # Vim temp files and what-not - f not in ['Thumbs.db']) # Windows bullshit - - def _populateMetadata(self, rel_path, metadata, mode=None): - pass - - -class DefaultPageSource(SimplePageSource): - SOURCE_NAME = 'default' - - def __init__(self, app, name, config): - super(DefaultPageSource, self).__init__(app, name, config) - - -class SourceFactoryIterator(object): - def __init__(self, source): - self.source = source - self.it = None # This is to permit recursive traversal of the - # iterator chain. It acts as the end. - - def __iter__(self): - return self.source.getPages() - - -class DateSortIterator(object): - def __init__(self, it, reverse=True): - self.it = it - self.reverse = reverse - - def __iter__(self): - return iter(sorted(self.it, - key=lambda x: x.datetime, reverse=self.reverse)) - - -class PaginationDataBuilderIterator(object): - def __init__(self, it): - self.it = it - - def __iter__(self): - for page in self.it: - if page is None: - yield None - else: - yield PaginationData(page) -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/sources/default.py Wed Feb 18 18:35:03 2015 -0800 @@ -0,0 +1,145 @@ +import os +import os.path +import logging +from piecrust.sources.base import ( + PageFactory, PageSource, InvalidFileSystemEndpointError, + MODE_CREATING) +from piecrust.sources.interfaces import IListableSource, IPreparingSource +from piecrust.sources.mixins import SimplePaginationSourceMixin + + +logger = logging.getLogger(__name__) + + +def filter_page_dirname(d): + return not (d.startswith('.') or d.endswith('-assets')) + + +def filter_page_filename(f): + return (f[0] != '.' and # .DS_store and other crap + f[-1] != '~' and # Vim temp files and what-not + f not in ['Thumbs.db']) # Windows bullshit + + +class DefaultPageSource(PageSource, IListableSource, IPreparingSource, + SimplePaginationSourceMixin): + SOURCE_NAME = 'default' + + def __init__(self, app, name, config): + super(DefaultPageSource, self).__init__(app, name, config) + self.fs_endpoint = config.get('fs_endpoint', name) + self.fs_endpoint_path = os.path.join(self.root_dir, self.fs_endpoint) + self.supported_extensions = list( + app.config.get('site/auto_formats').keys()) + self.default_auto_format = app.config.get('site/default_auto_format') + + def buildPageFactories(self): + logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path) + if not os.path.isdir(self.fs_endpoint_path): + if self.ignore_missing_dir: + return + raise InvalidFileSystemEndpointError(self.name, + self.fs_endpoint_path) + + for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path): + rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path) + dirnames[:] = list(filter(filter_page_dirname, dirnames)) + for f in filter(filter_page_filename, filenames): + fac_path = f + if rel_dirpath != '.': + fac_path = os.path.join(rel_dirpath, f) + slug = self._makeSlug(fac_path) + metadata = {'slug': slug} + fac_path = fac_path.replace('\\', '/') + self._populateMetadata(fac_path, metadata) + yield PageFactory(self, fac_path, metadata) + + def resolveRef(self, ref_path): + return os.path.normpath( + os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/"))) + + def findPagePath(self, metadata, mode): + uri_path = metadata.get('slug', '') + if not uri_path: + uri_path = '_index' + path = os.path.join(self.fs_endpoint_path, uri_path) + _, ext = os.path.splitext(path) + + if mode == MODE_CREATING: + if ext == '': + path = '%s.%s' % (path, self.default_auto_format) + rel_path = os.path.relpath(path, self.fs_endpoint_path) + rel_path = rel_path.replace('\\', '/') + self._populateMetadata(rel_path, metadata, mode) + return rel_path, metadata + + if ext == '': + paths_to_check = [ + '%s.%s' % (path, e) + for e in self.supported_extensions] + else: + paths_to_check = [path] + for path in paths_to_check: + if os.path.isfile(path): + rel_path = os.path.relpath(path, self.fs_endpoint_path) + rel_path = rel_path.replace('\\', '/') + self._populateMetadata(rel_path, metadata, mode) + return rel_path, metadata + + return None, None + + def listPath(self, rel_path): + rel_path = rel_path.lstrip('\\/') + path = os.path.join(self.fs_endpoint_path, rel_path) + names = sorted(os.listdir(path)) + items = [] + for name in names: + if os.path.isdir(os.path.join(path, name)): + if filter_page_dirname(name): + rel_subdir = os.path.join(rel_path, name) + items.append((True, name, rel_subdir)) + else: + if filter_page_filename(name): + slug = self._makeSlug(os.path.join(rel_path, name)) + metadata = {'slug': slug} + + fac_path = name + if rel_path != '.': + fac_path = os.path.join(rel_path, name) + fac_path = fac_path.replace('\\', '/') + + self._populateMetadata(fac_path, metadata) + fac = PageFactory(self, fac_path, metadata) + + name, _ = os.path.splitext(name) + items.append((False, name, fac)) + return items + + def getDirpath(self, rel_path): + return os.path.dirname(rel_path) + + def getBasename(self, rel_path): + filename = os.path.basename(rel_path) + name, _ = os.path.splitext(filename) + return name + + def setupPrepareParser(self, parser, app): + parser.add_argument('uri', help='The URI for the new page.') + + def buildMetadata(self, args): + return {'slug': args.uri} + + def _makeSlug(self, rel_path): + slug, ext = os.path.splitext(rel_path) + slug = slug.replace('\\', '/') + if ext.lstrip('.') not in self.supported_extensions: + slug += ext + if slug.startswith('./'): + slug = slug[2:] + if slug == '_index': + slug = '' + return slug + + def _populateMetadata(self, rel_path, metadata, mode=None): + pass +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/sources/interfaces.py Wed Feb 18 18:35:03 2015 -0800 @@ -0,0 +1,50 @@ + + +class IPaginationSource(object): + """ Defines the interface for a source that can be used as the data + for an iterator or a pagination. + """ + def getItemsPerPage(self): + raise NotImplementedError() + + def getSourceIterator(self): + raise NotImplementedError() + + def getSorterIterator(self, it): + raise NotImplementedError() + + def getTailIterator(self, it): + raise NotImplementedError() + + def getPaginationFilter(self, page): + raise NotImplementedError() + + def getSettingAccessor(self): + raise NotImplementedError() + + +class IListableSource: + """ Defines the interface for a source that can be iterated on in a + hierarchical manner, for use with the `family` data endpoint. + """ + def listPath(self, rel_path): + raise NotImplementedError() + + def getDirpath(self, rel_path): + raise NotImplementedError() + + def getBasename(self, rel_path): + raise NotImplementedError() + + +class IPreparingSource: + """ Defines the interface for a source whose pages can be created by the + `chef prepare` command. + """ + def setupPrepareParser(self, parser, app): + raise NotImplementedError() + + def buildMetadata(self, args): + raise NotImplementedError() + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/sources/mixins.py Wed Feb 18 18:35:03 2015 -0800 @@ -0,0 +1,139 @@ +import os +import os.path +import logging +from piecrust.data.base import PaginationData +from piecrust.data.filters import PaginationFilter +from piecrust.sources.base import PageFactory +from piecrust.sources.interfaces import IPaginationSource, IListableSource + + +logger = logging.getLogger(__name__) + + +class SourceFactoryIterator(object): + def __init__(self, source): + self.source = source + + # This is to permit recursive traversal of the + # iterator chain. It acts as the end. + self.it = None + + def __iter__(self): + return self.source.getPages() + + +class DateSortIterator(object): + def __init__(self, it, reverse=True): + self.it = it + self.reverse = reverse + + def __iter__(self): + return iter(sorted(self.it, + key=lambda x: x.datetime, reverse=self.reverse)) + + +class PaginationDataBuilderIterator(object): + def __init__(self, it): + self.it = it + + def __iter__(self): + for page in self.it: + if page is None: + yield None + else: + yield PaginationData(page) + + +def page_setting_accessor(item, name): + return item.config.get(name) + + +class SimplePaginationSourceMixin(IPaginationSource): + """ Implements the `IPaginationSource` interface in a standard way that + should fit most page sources. + """ + def getItemsPerPage(self): + return self.config['items_per_page'] + + def getSourceIterator(self): + return SourceFactoryIterator(self) + + def getSorterIterator(self, it): + return DateSortIterator(it) + + def getTailIterator(self, it): + return PaginationDataBuilderIterator(it) + + def getPaginationFilter(self, page): + conf = (page.config.get('items_filters') or + page.app.config.get('site/items_filters')) + if conf == 'none' or conf == 'nil' or conf == '': + conf = None + if conf is not None: + f = PaginationFilter() + f.addClausesFromConfig(conf) + return f + return None + + def getSettingAccessor(self): + return page_setting_accessor + + +class SimpleListableSourceMixin(IListableSource): + """ Implements the `IListableSource` interface for sources that map to + simple file-system structures. + """ + def listPath(self, rel_path): + rel_path = rel_path.lstrip('\\/') + path = self._getFullPath(rel_path) + names = self._sortFilenames(os.listdir(path)) + + items = [] + for name in names: + if os.path.isdir(os.path.join(path, name)): + if self._filterPageDirname(name): + rel_subdir = os.path.join(rel_path, name) + items.append((True, name, rel_subdir)) + else: + if self._filterPageFilename(name): + slug = self._makeSlug(os.path.join(rel_path, name)) + metadata = {'slug': slug} + + fac_path = name + if rel_path != '.': + fac_path = os.path.join(rel_path, name) + fac_path = fac_path.replace('\\', '/') + + self._populateMetadata(fac_path, metadata) + fac = PageFactory(self, fac_path, metadata) + + name, _ = os.path.splitext(name) + items.append((False, name, fac)) + return items + + def getDirpath(self, rel_path): + return os.path.dirname(rel_path) + + def getBasename(self, rel_path): + filename = os.path.basename(rel_path) + name, _ = os.path.splitext(filename) + return name + + def _getFullPath(self, rel_path): + return os.path.join(self.fs_endpoint_path, rel_path) + + def _sortFilenames(self, names): + return sorted(names) + + def _filterPageDirname(self, name): + return True + + def _filterPageFilename(self, name): + return True + + def _makeSlug(self, rel_path): + return rel_path.replace('\\', '/') + + def _populateMetadata(self, rel_path, metadata, mode=None): + pass +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/sources/pageref.py Wed Feb 18 18:35:03 2015 -0800 @@ -0,0 +1,99 @@ +import re +import os.path + + +page_ref_pattern = re.compile(r'(?P<src>[\w]+)\:(?P<path>.*?)(;|$)') + + +class PageNotFoundError(Exception): + pass + + +class PageRef(object): + """ A reference to a page, with support for looking a page in different + realms. + """ + def __init__(self, app, page_ref): + self.app = app + self._page_ref = page_ref + self._paths = None + self._first_valid_path_index = -2 + self._exts = list(app.config.get('site/auto_formats').keys()) + + @property + def exists(self): + try: + self._checkPaths() + return True + except PageNotFoundError: + return False + + @property + def source_name(self): + self._checkPaths() + return self._paths[self._first_valid_path_index][0] + + @property + def source(self): + return self.app.getSource(self.source_name) + + @property + def rel_path(self): + self._checkPaths() + return self._paths[self._first_valid_path_index][1] + + @property + def path(self): + self._checkPaths() + return self._paths[self._first_valid_path_index][2] + + @property + def possible_rel_paths(self): + self._load() + return [p[1] for p in self._paths] + + @property + def possible_paths(self): + self._load() + return [p[2] for p in self._paths] + + def _load(self): + if self._paths is not None: + return + + it = list(page_ref_pattern.finditer(self._page_ref)) + if len(it) == 0: + raise Exception("Invalid page ref: %s" % self._page_ref) + + self._paths = [] + for m in it: + source_name = m.group('src') + source = self.app.getSource(source_name) + if source is None: + raise Exception("No such source: %s" % source_name) + rel_path = m.group('path') + path = source.resolveRef(rel_path) + if '%ext%' in rel_path: + for e in self._exts: + self._paths.append( + (source_name, + rel_path.replace('%ext%', e), + path.replace('%ext%', e))) + else: + self._paths.append((source_name, rel_path, path)) + + def _checkPaths(self): + if self._first_valid_path_index >= 0: + return + if self._first_valid_path_index == -1: + raise PageNotFoundError( + "No valid paths were found for page reference: %s" % + self._page_ref) + + self._load() + self._first_valid_path_index = -1 + for i, path_info in enumerate(self._paths): + if os.path.isfile(path_info[2]): + self._first_valid_path_index = i + break +
--- a/piecrust/sources/posts.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/sources/posts.py Wed Feb 18 18:35:03 2015 -0800 @@ -4,10 +4,12 @@ import glob import logging import datetime -from piecrust.sources.base import (PageSource, IPreparingSource, - SimplePaginationSourceMixin, - PageNotFoundError, InvalidFileSystemEndpointError, - PageFactory, MODE_CREATING, MODE_PARSING) +from piecrust.sources.base import ( + PageSource, InvalidFileSystemEndpointError, PageFactory, + MODE_CREATING, MODE_PARSING) +from piecrust.sources.interfaces import IPreparingSource +from piecrust.sources.mixins import SimplePaginationSourceMixin +from piecrust.sources.pageref import PageNotFoundError logger = logging.getLogger(__name__)
--- a/piecrust/sources/prose.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/sources/prose.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,16 +1,14 @@ import os import os.path import logging -from piecrust.sources.base import ( - SimplePageSource, SimplePaginationSourceMixin, - MODE_CREATING) +from piecrust.sources.base import MODE_CREATING +from piecrust.sources.default import DefaultPageSource logger = logging.getLogger(__name__) -class ProseSource(SimplePageSource, - SimplePaginationSourceMixin): +class ProseSource(DefaultPageSource): SOURCE_NAME = 'prose' def __init__(self, app, name, config):
--- a/piecrust/taxonomies.py Mon Feb 16 08:25:08 2015 -0800 +++ b/piecrust/taxonomies.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,4 +1,4 @@ -from piecrust.sources.base import PageRef, PageNotFoundError +from piecrust.sources.pageref import PageRef, PageNotFoundError class Taxonomy(object):
--- a/tests/test_data_paginator.py Mon Feb 16 08:25:08 2015 -0800 +++ b/tests/test_data_paginator.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,7 +1,7 @@ import math import pytest -from piecrust.data.base import IPaginationSource from piecrust.data.paginator import Paginator +from piecrust.sources.interfaces import IPaginationSource class MockSource(list, IPaginationSource):
--- a/tests/test_sources_autoconfig.py Mon Feb 16 08:25:08 2015 -0800 +++ b/tests/test_sources_autoconfig.py Wed Feb 18 18:35:03 2015 -0800 @@ -88,10 +88,16 @@ (mock_fs(), [], []), (mock_fs().withPage('test/something.md'), ['something.md'], - [{'slug': 'something', 'config': {'foo': 0}}]), + [{'slug': 'something', + 'config': {'foo': 0, 'foo_trail': [0]}}]), (mock_fs().withPage('test/08_something.md'), ['08_something.md'], - [{'slug': 'something', 'config': {'foo': 8}}]) + [{'slug': 'something', + 'config': {'foo': 8, 'foo_trail': [8]}}]), + (mock_fs().withPage('test/02_there/08_something.md'), + ['02_there/08_something.md'], + [{'slug': 'there/something', + 'config': {'foo': 8, 'foo_trail': [2, 8]}}]), ]) def test_ordered_source_factories(fs, expected_paths, expected_metadata): site_config = { @@ -120,22 +126,27 @@ (mock_fs(), 'missing', None, None), (mock_fs().withPage('test/something.md'), 'something', 'something.md', - {'slug': 'something', 'config': {'foo': 0}}), + {'slug': 'something', + 'config': {'foo': 0, 'foo_trail': [0]}}), (mock_fs().withPage('test/bar/something.md'), 'bar/something', 'bar/something.md', - {'slug': 'bar/something', 'config': {'foo': 0}}), + {'slug': 'bar/something', + 'config': {'foo': 0, 'foo_trail': [0]}}), (mock_fs().withPage('test/42_something.md'), 'something', '42_something.md', - {'slug': 'something', 'config': {'foo': 42}}), + {'slug': 'something', + 'config': {'foo': 42, 'foo_trail': [42]}}), (mock_fs().withPage('test/bar/42_something.md'), 'bar/something', 'bar/42_something.md', - {'slug': 'bar/something', 'config': {'foo': 42}}), + {'slug': 'bar/something', + 'config': {'foo': 42, 'foo_trail': [42]}}), ((mock_fs() .withPage('test/42_something.md') .withPage('test/43_other_something.md')), 'something', '42_something.md', - {'slug': 'something', 'config': {'foo': 42}}), + {'slug': 'something', + 'config': {'foo': 42, 'foo_trail': [42]}}), ]) def test_ordered_source_find(fs, route_path, expected_path, expected_metadata): @@ -152,7 +163,7 @@ with mock_fs_scope(fs): app = fs.getApp() s = app.getSource('test') - route_metadata = {'path': route_path} + route_metadata = {'slug': route_path} fac_path, metadata = s.findPagePath(route_metadata, MODE_PARSING) assert fac_path == expected_path assert metadata == expected_metadata
--- a/tests/test_sources_base.py Mon Feb 16 08:25:08 2015 -0800 +++ b/tests/test_sources_base.py Wed Feb 18 18:35:03 2015 -0800 @@ -1,7 +1,7 @@ import os import pytest from piecrust.app import PieCrust -from piecrust.sources.base import PageRef, PageNotFoundError +from piecrust.sources.pageref import PageRef, PageNotFoundError from .mockutil import mock_fs, mock_fs_scope