# HG changeset patch # User Ludovic Chabant # Date 1430086060 25200 # Node ID dd25bd3ce1f93d8f6e8c7b80ae2f0281044e23f9 # Parent ff595828a36482a634b0761a3eb5882a763e05d5 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. diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/app.py --- 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 diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/baking/baker.py --- 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) diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/commands/builtin/scaffolding.py --- 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' % ( diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/importing/wordpress.py --- 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' diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/page.py --- 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): diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/serving.py --- 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, diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/sources/autoconfig.py --- 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() diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/sources/base.py --- 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): diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/sources/default.py --- 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('\\/') diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/sources/pageref.py --- 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[\w]+)\:(?P.*?)(;|$)') @@ -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 diff -r ff595828a364 -r dd25bd3ce1f9 piecrust/sources/posts.py --- 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\d{4})', - 'month': '(?P\d{2})', - 'day': '(?P\d{2})', - 'slug': '(?P.*)', - 'ext': '(?P.*)' - } - 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\d{4})', + 'month': '(?P\d{2})', + 'day': '(?P\d{2})', + 'slug': '(?P.*)', + 'ext': '(?P.*)' + } + 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) diff -r ff595828a364 -r dd25bd3ce1f9 tests/test_sources_autoconfig.py --- 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 diff -r ff595828a364 -r dd25bd3ce1f9 tests/test_sources_base.py --- 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, '