diff piecrust/sources/base.py @ 3:f485ba500df3

Gigantic change to basically make PieCrust 2 vaguely functional. - Serving works, with debug window. - Baking works, multi-threading, with dependency handling. - Various things not implemented yet.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 10 Aug 2014 23:43:16 -0700
parents
children 474c9882decf
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/sources/base.py	Sun Aug 10 23:43:16 2014 -0700
@@ -0,0 +1,339 @@
+import re
+import os
+import os.path
+import logging
+from werkzeug.utils import cached_property
+from piecrust import CONTENT_DIR
+from piecrust.configuration import ConfigurationError
+from piecrust.page import Page
+
+
+REALM_USER = 0
+REALM_THEME = 1
+REALM_NAMES = {
+        REALM_USER: 'User',
+        REALM_THEME: 'Theme'}
+
+
+MODE_PARSING = 0
+MODE_CREATING = 1
+
+
+logger = logging.getLogger(__name__)
+
+
+page_ref_pattern = re.compile(r'(?P<src>[\w]+)\:(?P<path>.*?)(;|$)')
+
+
+class PageNotFoundError(Exception):
+    pass
+
+
+class InvalidFileSystemEndpointError(Exception):
+    def __init__(self, source_name, fs_endpoint):
+        super(InvalidFileSystemEndpointError, self).__init__(
+                "Invalid file-system endpoint for source '%s': %s" %
+                (source_name, fs_endpoint))
+
+
+class PageFactory(object):
+    """ A class responsible for creating a page.
+    """
+    def __init__(self, source, rel_path, metadata):
+        self.source = source
+        self.rel_path = rel_path
+        self.metadata = metadata
+
+    @property
+    def ref_spec(self):
+        return '%s:%s' % (self.source.name, self.rel_path)
+
+    @cached_property
+    def path(self):
+        return self.source.resolveRef(self.rel_path)
+
+    def buildPage(self):
+        repo = self.source.app.env.page_repository
+        if repo is not None:
+            cache_key = '%s:%s' % (self.source.name, self.rel_path)
+            return repo.get(cache_key, self._doBuildPage)
+        return self._doBuildPage()
+
+    def _doBuildPage(self):
+        logger.debug("Building page: %s" % self.path)
+        page = Page(self.source, self.metadata, self.rel_path)
+        # Load it right away, especially when using the page repository,
+        # because we'll be inside a critical scope.
+        page._load()
+        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 = 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:" %
+                    self._page_ref)
+
+        self._load()
+        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.
+    """
+    def __init__(self, app, name, config):
+        self.app = app
+        self.name = name
+        self.config = config
+        self._factories = None
+        self._provider_type = None
+
+    def __getattr__(self, name):
+        try:
+            return self.config[name]
+        except KeyError:
+            raise AttributeError()
+
+    @property
+    def is_theme_source(self):
+        return self.realm == REALM_THEME
+
+    @property
+    def root_dir(self):
+        if self.is_theme_source:
+            return self.app.theme_dir
+        return self.app.root_dir
+
+    def getPageFactories(self):
+        if self._factories is None:
+            self._factories = list(self.buildPageFactories())
+        return self._factories
+
+    def buildPageFactories(self):
+        raise NotImplementedError()
+
+    def resolveRef(self, ref_path):
+        raise NotImplementedError()
+
+    def findPagePath(self, metadata, mode):
+        raise NotImplementedError()
+
+    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 cls is None:
+                raise ConfigurationError("Unknown data provider type: %s" %
+                        self.data_type)
+            self._provider_type = cls
+
+        return self._provider_type(self, page, user_data)
+
+    def getTaxonomyPageRef(self, tax_name):
+        tax_pages = self.config.get('taxonomy_pages')
+        if tax_pages is None:
+            return None
+        return tax_pages.get(tax_name)
+
+
+class IPreparingSource:
+    def setupPrepareParser(self, parser, app):
+        raise NotImplementedError()
+
+    def buildMetadata(self, args):
+        raise NotImplementedError()
+
+
+class ArraySource(PageSource):
+    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):
+    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, CONTENT_DIR, self.fs_endpoint)
+        self.supported_extensions = app.config.get('site/auto_formats').keys()
+
+    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):
+            rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path)
+            dirnames[:] = filter(self._filterPageDirname, dirnames)
+            for f in filter(self._filterPageFilename, filenames):
+                slug, ext = os.path.splitext(os.path.join(rel_dirpath, f))
+                if slug.startswith('./') or slug.startswith('.\\'):
+                    slug = slug[2:]
+                if slug == '_index':
+                    slug = ''
+                metadata = {'path': slug}
+                fac_path = f
+                if rel_dirpath != '.':
+                    fac_path = os.path.join(rel_dirpath, f)
+                yield PageFactory(self, fac_path, metadata)
+
+    def resolveRef(self, ref_path):
+        return os.path.join(self.fs_endpoint_path, ref_path)
+
+    def findPagePath(self, metadata, mode):
+        uri_path = metadata['path']
+        if 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 == '':
+                return '%s.*' % path
+            return 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):
+                return path, metadata
+
+        return None, None
+
+    def _filterPageDirname(self, d):
+        return not d.endswith('-assets')
+
+    def _filterPageFilename(self, f):
+        name, ext = os.path.splitext(f)
+        return (f[0] != '.' and
+                f[-1] != '~' and
+                ext.lstrip('.') in self.supported_extensions and
+                f not in ['Thumbs.db'])
+
+
+class DefaultPageSource(SimplePageSource, IPreparingSource):
+    SOURCE_NAME = 'default'
+
+    def __init__(self, app, name, config):
+        super(DefaultPageSource, self).__init__(app, name, config)
+
+    def setupPrepareParser(self, parser, app):
+        parser.add_argument('uri', help='The URI for the new page.')
+
+    def buildMetadata(self, args):
+        return {'path': args.uri}
+