changeset 363:dd25bd3ce1f9

serve: Refactoring and fixes to be able to serve taxonomy pages. * Page sources' `findPagePath` is renamed to `findPageFactory`, so that it also returns source metadata. * Page refs now store possible hits more properly, and use the previous point to also store metadata. As a result, they can also return a proper factory. * Implement `findPageFactory` correctly in all built-in sources. * When the Chef server matches a taxonomy page, get the source metadata from the page ref in order to make a more proper page. * Make the `getRoute(s)` functions explicitely say if they want taxonomy routes or not.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 26 Apr 2015 15:07:40 -0700
parents ff595828a364
children 81480d0219ba
files piecrust/app.py piecrust/baking/baker.py piecrust/commands/builtin/scaffolding.py piecrust/importing/wordpress.py piecrust/page.py piecrust/serving.py piecrust/sources/autoconfig.py piecrust/sources/base.py piecrust/sources/default.py piecrust/sources/pageref.py piecrust/sources/posts.py tests/test_sources_autoconfig.py tests/test_sources_base.py
diffstat 13 files changed, 159 insertions(+), 101 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/app.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/app.py	Sun Apr 26 15:07:40 2015 -0700
@@ -531,14 +531,15 @@
                 return source
         return None
 
-    def getRoutes(self, source_name, skip_taxonomies=False):
+    def getRoutes(self, source_name, *, skip_taxonomies=False):
         for route in self.routes:
             if route.source_name == source_name:
                 if not skip_taxonomies or route.taxonomy_name is None:
                     yield route
 
-    def getRoute(self, source_name, source_metadata):
-        for route in self.getRoutes(source_name, True):
+    def getRoute(self, source_name, source_metadata, *, skip_taxonomies=False):
+        for route in self.getRoutes(source_name,
+                                    skip_taxonomies=skip_taxonomies):
             if route.matchesMetadata(source_metadata):
                 return route
         return None
--- a/piecrust/baking/baker.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/baking/baker.py	Sun Apr 26 15:07:40 2015 -0700
@@ -178,7 +178,8 @@
                                             fac.path)
                 record.addEntry(entry)
 
-                route = self.app.getRoute(source.name, fac.metadata)
+                route = self.app.getRoute(source.name, fac.metadata,
+                                          skip_taxonomies=True)
                 if route is None:
                     entry.errors.append(
                             "Can't get route for page: %s" % fac.ref_spec)
--- a/piecrust/commands/builtin/scaffolding.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/commands/builtin/scaffolding.py	Sun Apr 26 15:07:40 2015 -0700
@@ -64,8 +64,8 @@
         app = ctx.app
         source = ctx.args.source
         metadata = source.buildMetadata(ctx.args)
-        rel_path, metadata = source.findPagePath(metadata, MODE_CREATING)
-        path = source.resolveRef(rel_path)
+        factory = source.findPageFactory(metadata, MODE_CREATING)
+        path = factory.path
         name, ext = os.path.splitext(path)
         if ext == '.*':
             path = '%s.%s' % (
--- a/piecrust/importing/wordpress.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/importing/wordpress.py	Sun Apr 26 15:07:40 2015 -0700
@@ -112,7 +112,7 @@
             source = self._pages_source
         else:
             raise Exception("Unknown post type: %s" % post_info['type'])
-        rel_path, fac_metadata = source.findPagePath(finder, MODE_CREATING)
+        factory = source.findPageFactory(finder, MODE_CREATING)
 
         metadata = post_info['metadata'].copy()
         for name in ['title', 'author', 'status', 'post_id', 'post_guid',
@@ -129,7 +129,7 @@
 
         status = metadata.get('status')
         if status == 'publish':
-            path = source.resolveRef(rel_path)
+            path = factory.path
             create_page(self.app, path, metadata, text)
         elif status == 'draft':
             filename = '-'.join(metadata['title'].split(' ')) + '.html'
--- a/piecrust/page.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/page.py	Sun Apr 26 15:07:40 2015 -0700
@@ -57,7 +57,8 @@
 
     @cached_property
     def path(self):
-        return self.source.resolveRef(self.rel_path)
+        path, _ = self.source.resolveRef(self.rel_path)
+        return path
 
     @cached_property
     def path_mtime(self):
--- a/piecrust/serving.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/serving.py	Sun Apr 26 15:07:40 2015 -0700
@@ -231,19 +231,17 @@
         for route, route_metadata in routes:
             source = app.getSource(route.source_name)
             if route.taxonomy_name is None:
-                rel_path, fac_metadata = source.findPagePath(
-                        route_metadata, MODE_PARSING)
-                if rel_path is not None:
+                factory = source.findPageFactory(route_metadata, MODE_PARSING)
+                if factory is not None:
                     break
             else:
                 taxonomy = app.getTaxonomy(route.taxonomy_name)
                 route_terms = route_metadata.get(taxonomy.term_name)
                 if route_terms is not None:
                     tax_page_ref = taxonomy.getPageRef(source.name)
-                    rel_path = tax_page_ref.rel_path
-                    source = tax_page_ref.source
+                    factory = tax_page_ref.getFactory()
                     tax_terms = route.unslugifyTaxonomyTerm(route_terms)
-                    fac_metadata = {taxonomy.term_name: tax_terms}
+                    factory.metadata[taxonomy.term_name] = tax_terms
                     break
         else:
             raise SourceNotFoundError(
@@ -251,8 +249,7 @@
                     (req_path, [r.source_name for r, _ in routes]))
 
         # Build the page.
-        fac = PageFactory(source, rel_path, fac_metadata)
-        page = fac.buildPage()
+        page = factory.buildPage()
         # We force the rendering of the page because it could not have
         # changed, but include pages that did change.
         render_ctx = PageRenderingContext(page, req_path, page_num,
--- a/piecrust/sources/autoconfig.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/sources/autoconfig.py	Sun Apr 26 15:07:40 2015 -0700
@@ -67,9 +67,21 @@
                 yield PageFactory(self, fac_path, metadata)
 
     def resolveRef(self, ref_path):
-        return os.path.normpath(
+        path = os.path.normpath(
                 os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/")))
 
+        config = None
+        if self.capture_mode == 'dirname':
+            config = self._extractConfigFragment(os.path.dirname(ref_path))
+        elif self.capture_mode == 'path':
+            config = self._extractConfigFragment(ref_path)
+        elif self.capture_mode == 'filename':
+            config = self._extractConfigFragment(os.path.basename(ref_path))
+
+        slug = self._makeSlug(ref_path)
+        metadata = {'slug': slug, 'config': config}
+        return path, metadata
+
     def listPath(self, rel_path):
         raise NotImplementedError()
 
@@ -140,7 +152,7 @@
 
         return {self.setting_name: values}
 
-    def findPagePath(self, metadata, mode):
+    def findPageFactory(self, metadata, mode):
         # Pages from this source are effectively flattened, so we need to
         # find pages using a brute-force kinda way.
         for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path):
@@ -151,7 +163,8 @@
                     rel_path = os.path.relpath(path, self.fs_endpoint_path)
                     config = self._extractConfigFragment(rel_path)
                     metadata = {'slug': slug, 'config': config}
-                    return rel_path, metadata
+                    return PageFactory(self, rel_path, metadata)
+        return None
 
     def listPath(self, rel_path):
         rel_path = rel_path.lstrip('\\/')
@@ -196,7 +209,7 @@
         self.supported_extensions = list(
                 app.config.get('site/auto_formats').keys())
 
-    def findPagePath(self, metadata, mode):
+    def findPageFactory(self, metadata, mode):
         uri_path = metadata.get('slug', '')
         if uri_path == '':
             uri_path = '_index'
@@ -221,7 +234,7 @@
                         found = True
                         break
                 if not found:
-                    return None, None
+                    return None
             else:
                 # Find each sub-directory. It can either be a directory with
                 # the name itself, or the name with a number prefix.
@@ -233,13 +246,13 @@
                         found = True
                         break
                 if not found:
-                    return None, None
+                    return None
 
         fac_path = os.path.relpath(path, self.fs_endpoint_path)
         config = self._extractConfigFragment(fac_path)
         metadata = {'slug': uri_path, 'config': config}
 
-        return fac_path, metadata
+        return PageFactory(self, fac_path, metadata)
 
     def getSorterIterator(self, it):
         accessor = self.getSettingAccessor()
--- a/piecrust/sources/base.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/sources/base.py	Sun Apr 26 15:07:40 2015 -0700
@@ -45,7 +45,8 @@
 
     @cached_property
     def path(self):
-        return self.source.resolveRef(self.rel_path)
+        path, _ = self.source.resolveRef(self.rel_path)
+        return path
 
     def buildPage(self):
         repo = self.source.app.env.page_repository
@@ -94,10 +95,10 @@
         return build_pages(self.app, self.getPageFactories())
 
     def getPage(self, metadata):
-        rel_path, metadata = self.findPagePath(metadata, MODE_PARSING)
-        if rel_path is None:
+        factory = self.findPageFactory(metadata, MODE_PARSING)
+        if factory is None:
             return None
-        return Page(self, metadata, rel_path)
+        return factory.buildPage()
 
     def getPageFactories(self):
         if self._factories is None:
@@ -108,9 +109,12 @@
         raise NotImplementedError()
 
     def resolveRef(self, ref_path):
+        """ Returns the full path and source metadata given a source
+            (relative) path, like a ref-spec.
+        """
         raise NotImplementedError()
 
-    def findPagePath(self, metadata, mode):
+    def findPageFactory(self, metadata, mode):
         raise NotImplementedError()
 
     def buildDataProvider(self, page, user_data):
--- a/piecrust/sources/default.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/sources/default.py	Sun Apr 26 15:07:40 2015 -0700
@@ -55,10 +55,14 @@
                 yield PageFactory(self, fac_path, metadata)
 
     def resolveRef(self, ref_path):
-        return os.path.normpath(
+        path = os.path.normpath(
                 os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/")))
+        slug = self._makeSlug(ref_path)
+        metadata = {'slug': slug}
+        self._populateMetadata(ref_path, metadata)
+        return path, metadata
 
-    def findPagePath(self, metadata, mode):
+    def findPageFactory(self, metadata, mode):
         uri_path = metadata.get('slug', '')
         if not uri_path:
             uri_path = '_index'
@@ -71,7 +75,7 @@
             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 PageFactory(self, rel_path, metadata)
 
         if ext == '':
             paths_to_check = [
@@ -84,9 +88,9 @@
                 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 PageFactory(self, rel_path, metadata)
 
-        return None, None
+        return None
 
     def listPath(self, rel_path):
         rel_path = rel_path.lstrip('\\/')
--- a/piecrust/sources/pageref.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/sources/pageref.py	Sun Apr 26 15:07:40 2015 -0700
@@ -1,5 +1,6 @@
 import re
 import os.path
+from piecrust.sources.base import PageFactory
 
 
 page_ref_pattern = re.compile(r'(?P<src>[\w]+)\:(?P<path>.*?)(;|$)')
@@ -13,25 +14,35 @@
     """ A reference to a page, with support for looking a page in different
         realms.
     """
+    _INDEX_NEEDS_LOADING = -2
+    _INDEX_NOT_FOUND = -1
+
+    class _HitInfo(object):
+        def __init__(self, source_name, rel_path, path, metadata):
+            self.source_name = source_name
+            self.rel_path = rel_path
+            self.path = path
+            self.metadata = metadata
+
     def __init__(self, app, page_ref):
         self.app = app
         self._page_ref = page_ref
-        self._paths = None
-        self._first_valid_path_index = -2
+        self._hits = None
+        self._first_valid_hit_index = self._INDEX_NEEDS_LOADING
         self._exts = list(app.config.get('site/auto_formats').keys())
 
     @property
     def exists(self):
         try:
-            self._checkPaths()
+            self._checkHits()
             return True
         except PageNotFoundError:
             return False
 
     @property
     def source_name(self):
-        self._checkPaths()
-        return self._paths[self._first_valid_path_index][0]
+        self._checkHits()
+        return self._first_valid_hit.source_name
 
     @property
     def source(self):
@@ -39,61 +50,75 @@
 
     @property
     def rel_path(self):
-        self._checkPaths()
-        return self._paths[self._first_valid_path_index][1]
+        self._checkHits()
+        return self._first_valid_hit.rel_path
 
     @property
     def path(self):
-        self._checkPaths()
-        return self._paths[self._first_valid_path_index][2]
+        self._checkHits()
+        return self._first_valid_hit.path
+
+    @property
+    def metadata(self):
+        self._checkHits()
+        return self._first_valid_hit.metadata
 
     @property
     def possible_rel_paths(self):
         self._load()
-        return [p[1] for p in self._paths]
+        return [h.rel_path for h in self._hits]
 
     @property
     def possible_paths(self):
         self._load()
-        return [p[2] for p in self._paths]
+        return [h.path for h in self._hits]
+
+    def getFactory(self):
+        return PageFactory(self.source, self.rel_path, self.metadata)
+
+    @property
+    def _first_valid_hit(self):
+        return self._hits[self._first_valid_hit_index]
 
     def _load(self):
-        if self._paths is not None:
+        if self._hits 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 = []
+        self._hits = []
         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)
+            path, metadata = 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)))
+                    self._hits.append(self._HitInfo(
+                            source_name,
+                            rel_path.replace('%ext%', e),
+                            path.replace('%ext%', e),
+                            metadata))
             else:
-                self._paths.append((source_name, rel_path, path))
+                self._hits.append(
+                        self._HitInfo(source_name, rel_path, path, metadata))
 
-    def _checkPaths(self):
-        if self._first_valid_path_index >= 0:
+    def _checkHits(self):
+        if self._first_valid_hit_index >= 0:
             return
-        if self._first_valid_path_index == -1:
+        if self._first_valid_hit_index == self._INDEX_NOT_FOUND:
             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
+        self._first_valid_hit_index = self._INDEX_NOT_FOUND
+        for i, hit in enumerate(self._hits):
+            if os.path.isfile(hit.path):
+                self._first_valid_hit_index = i
                 break
 
--- a/piecrust/sources/posts.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/piecrust/sources/posts.py	Sun Apr 26 15:07:40 2015 -0700
@@ -30,9 +30,11 @@
         return self.__class__.PATH_FORMAT
 
     def resolveRef(self, ref_path):
-        return os.path.normpath(os.path.join(self.fs_endpoint_path, ref_path))
+        path = os.path.normpath(os.path.join(self.fs_endpoint_path, ref_path))
+        metadata = self._parseMetadataFromPath(ref_path)
+        return path, metadata
 
-    def findPagePath(self, metadata, mode):
+    def findPageFactory(self, metadata, mode):
         year = metadata.get('year')
         month = metadata.get('month')
         day = metadata.get('day')
@@ -46,7 +48,7 @@
             if day is not None:
                 day = int(day)
         except ValueError:
-            return None, None
+            return None
 
         ext = metadata.get('ext')
         if ext is None:
@@ -91,35 +93,10 @@
         elif mode == MODE_PARSING and not os.path.isfile(path):
             raise PageNotFoundError(path)
 
-        regex_repl = {
-                'year': '(?P<year>\d{4})',
-                'month': '(?P<month>\d{2})',
-                'day': '(?P<day>\d{2})',
-                'slug': '(?P<slug>.*)',
-                'ext': '(?P<ext>.*)'
-                }
-        path_format_re = re.sub(r'([\-\.])', r'\\\1', self.path_format)
-        pattern = path_format_re % regex_repl + '$'
-        m = re.search(pattern, path.replace('\\', '/'))
-        if not m:
-            raise Exception("Expected to be able to match path with path "
-                            "format: %s" % path)
-
-        year = int(m.group('year'))
-        month = int(m.group('month'))
-        day = int(m.group('day'))
-        timestamp = datetime.date(year, month, day)
-        fac_metadata = {
-                'year': year,
-                'month': month,
-                'day': day,
-                'slug': m.group('slug'),
-                'date': timestamp
-                }
-
         rel_path = os.path.relpath(path, self.fs_endpoint_path)
         rel_path = rel_path.replace('\\', '/')
-        return rel_path, fac_metadata
+        fac_metadata = self._parseMetadataFromPath(rel_path)
+        return PageFactory(self, rel_path, fac_metadata)
 
     def setupPrepareParser(self, parser, app):
         parser.add_argument('-d', '--date', help="The date of the post, "
@@ -155,6 +132,34 @@
             raise InvalidFileSystemEndpointError(self.name, self.fs_endpoint_path)
         return True
 
+    def _parseMetadataFromPath(self, path):
+        regex_repl = {
+                'year': '(?P<year>\d{4})',
+                'month': '(?P<month>\d{2})',
+                'day': '(?P<day>\d{2})',
+                'slug': '(?P<slug>.*)',
+                'ext': '(?P<ext>.*)'
+                }
+        path_format_re = re.sub(r'([\-\.])', r'\\\1', self.path_format)
+        pattern = path_format_re % regex_repl + '$'
+        m = re.search(pattern, path.replace('\\', '/'))
+        if not m:
+            raise Exception("Expected to be able to match path with path "
+                            "format: %s" % path)
+
+        year = int(m.group('year'))
+        month = int(m.group('month'))
+        day = int(m.group('day'))
+        timestamp = datetime.date(year, month, day)
+        metadata = {
+                'year': year,
+                'month': month,
+                'day': day,
+                'slug': m.group('slug'),
+                'date': timestamp
+                }
+        return metadata
+
     def _makeFactory(self, path, slug, year, month, day):
         path = path.replace('\\', '/')
         timestamp = datetime.date(year, month, day)
--- a/tests/test_sources_autoconfig.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/tests/test_sources_autoconfig.py	Sun Apr 26 15:07:40 2015 -0700
@@ -173,7 +173,10 @@
         app = fs.getApp()
         s = app.getSource('test')
         route_metadata = {'slug': route_path}
-        fac_path, metadata = s.findPagePath(route_metadata, MODE_PARSING)
-        assert fac_path == slashfix(expected_path)
-        assert metadata == expected_metadata
+        factory = s.findPageFactory(route_metadata, MODE_PARSING)
+        if factory is None:
+            assert expected_path is None and expected_metadata is None
+            return
+        assert factory.rel_path == slashfix(expected_path)
+        assert factory.metadata == expected_metadata
 
--- a/tests/test_sources_base.py	Sun Apr 26 08:34:27 2015 -0700
+++ b/tests/test_sources_base.py	Sun Apr 26 15:07:40 2015 -0700
@@ -41,12 +41,15 @@
         assert slugs == expected_slugs
 
 
-
-@pytest.mark.parametrize('ref_path, expected', [
-        ('foo.html', '/kitchen/test/foo.html'),
-        ('foo/bar.html', '/kitchen/test/foo/bar.html'),
+@pytest.mark.parametrize(
+        'ref_path, expected_path, expected_metadata',
+        [
+            ('foo.html', '/kitchen/test/foo.html', {'slug': 'foo'}),
+            ('foo/bar.html', '/kitchen/test/foo/bar.html',
+                {'slug': 'foo/bar'}),
         ])
-def test_default_source_resolve_ref(ref_path, expected):
+def test_default_source_resolve_ref(ref_path, expected_path,
+                                    expected_metadata):
     fs = mock_fs()
     fs.withConfig({
         'site': {
@@ -56,12 +59,13 @@
                 {'url': '/%path%', 'source': 'test'}]
             }
         })
-    expected = fs.path(expected).replace('/', os.sep)
+    expected_path = fs.path(expected_path).replace('/', os.sep)
     with mock_fs_scope(fs):
         app = PieCrust(fs.path('kitchen'), cache=False)
         s = app.getSource('test')
-        actual = s.resolveRef(ref_path)
-        assert actual == expected
+        actual_path, actual_metadata = s.resolveRef(ref_path)
+        assert actual_path == expected_path
+        assert actual_metadata == expected_metadata
 
 
 @pytest.mark.parametrize('page_ref, expected_source_name, expected_rel_path, '