Mercurial > piecrust2
changeset 6:f5ca5c5bed85
More Python 3 fixes, modularization, and new unit tests.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 16 Aug 2014 08:15:30 -0700 |
parents | 474c9882decf |
children | 343d08ef5668 |
files | piecrust/app.py piecrust/baking/baker.py piecrust/data/assetor.py piecrust/data/base.py piecrust/data/iterators.py piecrust/data/paginator.py piecrust/page.py piecrust/sources/base.py piecrust/sources/posts.py piecrust/uriutil.py tests/mockutil.py tests/test_baking_baker.py tests/test_data_assetor.py tests/test_data_paginator.py tests/test_pagebaker.py tests/test_server.py tests/test_serving.py |
diffstat | 17 files changed, 604 insertions(+), 166 deletions(-) [+] |
line wrap: on
line diff
--- a/piecrust/app.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/app.py Sat Aug 16 08:15:30 2014 -0700 @@ -1,7 +1,6 @@ import re import json import os.path -import types import codecs import hashlib import logging @@ -59,8 +58,8 @@ return path_times = [os.path.getmtime(p) for p in self.paths] - cache_key = hashlib.md5("version=%s&cache=%d" % ( - APP_VERSION, CACHE_VERSION)).hexdigest() + cache_key = hashlib.md5(("version=%s&cache=%d" % ( + APP_VERSION, CACHE_VERSION)).encode('utf8')).hexdigest() if self.cache.isValid('config.json', path_times): logger.debug("Loading configuration from cache...")
--- a/piecrust/baking/baker.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/baking/baker.py Sat Aug 16 08:15:30 2014 -0700 @@ -61,13 +61,15 @@ def getOutputPath(self, uri): bake_path = [self.out_dir] - decoded_uri = urllib.parse.unquote(uri.lstrip('/')).decode('utf8') + decoded_uri = urllib.parse.unquote(uri.lstrip('/')) if self.pretty_urls: bake_path.append(decoded_uri) bake_path.append('index.html') else: name, ext = os.path.splitext(decoded_uri) - if ext: + if decoded_uri == '': + bake_path.append('index.html') + elif ext: bake_path.append(decoded_uri) else: bake_path.append(decoded_uri + '.html') @@ -191,7 +193,7 @@ os.makedirs(out_dir, 0o755) with codecs.open(out_path, 'w', 'utf-8') as fp: - fp.write(rp.content.decode('utf-8')) + fp.write(rp.content) return ctx, rp
--- a/piecrust/data/assetor.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/data/assetor.py Sat Aug 16 08:15:30 2014 -0700 @@ -1,3 +1,4 @@ +import os import os.path import logging from piecrust.uriutil import multi_replace @@ -6,6 +7,10 @@ logger = logging.getLogger(__name__) +class UnsupportedAssetsError(Exception): + pass + + def build_base_url(app, uri, assets_path): base_url_format = app.env.base_asset_url_format site_root = app.config.get('site/root') @@ -13,6 +18,7 @@ pretty = app.config.get('site/pretty_urls') if not pretty: uri, _ = os.path.splitext(uri) + uri = uri.lstrip('/') base_url = multi_replace( base_url_format, { @@ -71,5 +77,8 @@ for _, __, filenames in os.walk(assets_dir): for fn in filenames: name, ext = os.path.splitext(fn) + if name in self._cache: + raise UnsupportedAssetsError( + "Multiple asset files are named '%s'." % name) self._cache[name] = base_url + fn
--- a/piecrust/data/base.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/data/base.py Sat Aug 16 08:15:30 2014 -0700 @@ -6,6 +6,23 @@ logger = logging.getLogger(__name__) +class IPaginationSource(object): + 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() + + 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 @@ -78,43 +95,19 @@ pass -def build_uri(page): - route = page.app.getRoute(page.source.name, page.source_metadata) - if route is None: - raise Exception("Can't get route for page: %s" % page.path) - return route.getUri(page.source_metadata) - - -def load_rendered_segment(data, name): - from piecrust.rendering import PageRenderingContext, render_page_segments - - uri = build_uri(data.page) - try: - ctx = PageRenderingContext(data.page, uri) - segs = render_page_segments(ctx) - except Exception as e: - logger.exception("Error rendering segments for '%s': %s" % (uri, e)) - raise - - for k, v in segs.items(): - data.mapLoader(k, None) - data.setValue(k, v) - - if 'content.abstract' in segs: - data.setValue('content', segs['content.abstract']) - data.setValue('has_more', True) - if name == 'content': - return segs['content.abstract'] - - return segs[name] - - class PaginationData(LazyPageConfigData): def __init__(self, page): super(PaginationData, self).__init__(page) + def _get_uri(self): + page = self._page + route = page.app.getRoute(page.source.name, page.source_metadata) + if route is None: + raise Exception("Can't get route for page: %s" % page.path) + return route.getUri(page.source_metadata) + def _loadCustom(self): - page_url = build_uri(self.page) + page_url = self._get_uri() self.setValue('url', page_url) self.setValue('slug', page_url) self.setValue('timestamp', @@ -128,5 +121,29 @@ segment_names = self.page.config.get('segments') for name in segment_names: - self.mapLoader(name, load_rendered_segment) + self.mapLoader(name, self._load_rendered_segment) + + def _load_rendered_segment(self, data, name): + from piecrust.rendering import PageRenderingContext, render_page_segments + assert self is data + uri = self._get_uri() + try: + ctx = PageRenderingContext(self._page, uri) + segs = render_page_segments(ctx) + except Exception as e: + logger.exception("Error rendering segments for '%s': %s" % (uri, e)) + raise + + for k, v in segs.items(): + self.mapLoader(k, None) + self.setValue(k, v) + + if 'content.abstract' in segs: + self.setValue('content', segs['content.abstract']) + self.setValue('has_more', True) + if name == 'content': + return segs['content.abstract'] + + return segs[name] +
--- a/piecrust/data/iterators.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/data/iterators.py Sat Aug 16 08:15:30 2014 -0700 @@ -1,5 +1,5 @@ import logging -from piecrust.data.base import PaginationData +from piecrust.data.base import IPaginationSource from piecrust.data.filters import PaginationFilter from piecrust.events import Event @@ -89,16 +89,6 @@ return sorted(self.it, cmp=self._comparer, reverse=self.reverse) -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 PaginationFilterIterator(object): def __init__(self, it, fil): self.it = it @@ -110,33 +100,13 @@ yield page -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): - for factory in self.source.getPageFactories(): - yield factory.buildPage() - - -class PaginationDataBuilderIterator(object): - def __init__(self, it): - self.it = it - - def __iter__(self): - for page in self.it: - yield PaginationData(page) - - class PageIterator(object): def __init__(self, source, current_page=None, pagination_filter=None, offset=0, limit=-1, locked=False): self._source = source self._current_page = current_page self._locked = False - self._pages = SourceFactoryIterator(source) + self._pages = source self._pagesData = None self._pagination_slicer = None self._has_sorter = False @@ -144,6 +114,11 @@ self._prev_page = None self._iter_event = Event() + if isinstance(source, IPaginationSource): + src_it = source.getSourceIterator() + if src_it is not None: + self._pages = src_it + # Apply any filter first, before we start sorting or slicing. if pagination_filter is not None: self._simpleNonSortedWrap(PaginationFilterIterator, @@ -203,7 +178,7 @@ return self._simpleNonSortedWrap(SettingFilterIterator, conf) return has_filter - raise AttributeError() + return self.__getattribute__(name) def skip(self, count): return self._simpleWrap(SliceIterator, count) @@ -268,7 +243,10 @@ def _ensureSorter(self): if self._has_sorter: return - self._pages = DateSortIterator(self._pages) + if isinstance(self._source, IPaginationSource): + sort_it = self._source.getSorterIterator(self._pages) + if sort_it is not None: + self._pages = sort_it self._has_sorter = True def _unload(self): @@ -282,10 +260,19 @@ self._ensureSorter() - it_chain = PaginationDataBuilderIterator(self._pages) + it_chain = self._pages + is_pgn_source = False + if isinstance(self._source, IPaginationSource): + is_pgn_source = True + tail_it = self._source.getTailIterator(self._pages) + if tail_it is not None: + it_chain = tail_it + self._pagesData = list(it_chain) - if self._current_page and self._pagination_slicer: - self._prev_page = PaginationData(self._pagination_slicer.prev_page) - self._next_page = PaginationData(self._pagination_slicer.next_page) + if is_pgn_source and self._current_page and self._pagination_slicer: + pn = [self._pagination_slicer.prev_page, + self._pagination_slicer.next_page] + pn_it = self._source.getTailIterator(iter(pn)) + self._prev_page, self._next_page = (list(pn_it))
--- a/piecrust/data/paginator.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/data/paginator.py Sat Aug 16 08:15:30 2014 -0700 @@ -1,6 +1,7 @@ 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 @@ -82,8 +83,11 @@ @cached_property def items_per_page(self): - return (self._parent_page.config.get('items_per_page') or - self._source.items_per_page) + if self._parent_page: + ipp = self._parent_page.config.get('items_per_page') + if ipp is not None: + return ipp + return self._source.getItemsPerPage() @property def items_this_page(self): @@ -152,7 +156,7 @@ return [] if radius <= 0 or total_page_count < (2 * radius + 1): - return list(range(1, total_page_count)) + return list(range(1, total_page_count + 1)) first_num = self._page_num - radius last_num = self._page_num + radius @@ -164,7 +168,7 @@ last_num = total_page_count first_num = max(1, first_num) last_num = min(total_page_count, last_num) - return list(range(first_num, last_num)) + return list(range(first_num, last_num + 1)) def page(self, index): return self._getPageUri(index) @@ -191,12 +195,10 @@ if self._pgn_filter is not None: f.addClause(self._pgn_filter.root_clause) - conf = (self._parent_page.config.get('items_filters') or - self._parent_page.app.config.get('site/items_filters')) - if conf == 'none' or conf == 'nil' or conf == '': - conf = None - if conf is not None: - f.addClausesFromConfig(conf) + if isinstance(self._source, IPaginationSource): + sf = self._source.getPaginationFilter(self._parent_page) + if sf is not None: + f.addClause(sf) return f @@ -209,7 +211,7 @@ return uri def _onIteration(self): - if not self._pgn_set_on_ctx: + if self._parent_page is not None and not self._pgn_set_on_ctx: eis = self._parent_page.app.env.exec_info_stack eis.current_page_info.render_ctx.setPagination(self) self._pgn_set_on_ctx = True
--- a/piecrust/page.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/page.py Sat Aug 16 08:15:30 2014 -0700 @@ -7,7 +7,6 @@ import logging import datetime import dateutil.parser -import threading from piecrust.configuration import (Configuration, ConfigurationError, parse_config_header) from piecrust.environment import PHASE_PAGE_PARSING
--- a/piecrust/sources/base.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/sources/base.py Sat Aug 16 08:15:30 2014 -0700 @@ -5,6 +5,8 @@ from werkzeug.utils import cached_property from piecrust import CONTENT_DIR from piecrust.configuration import ConfigurationError +from piecrust.data.base import IPaginationSource, PaginationData +from piecrust.data.filters import PaginationFilter from piecrust.page import Page @@ -247,6 +249,31 @@ 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 + + class ArraySource(PageSource): def __init__(self, app, inner_source, name='array', config=None): super(ArraySource, self).__init__(app, name, config or {}) @@ -325,7 +352,8 @@ f not in ['Thumbs.db']) -class DefaultPageSource(SimplePageSource, IPreparingSource): +class DefaultPageSource(SimplePageSource, IPreparingSource, + SimplePaginationSourceMixin): SOURCE_NAME = 'default' def __init__(self, app, name, config): @@ -337,3 +365,33 @@ def buildMetadata(self, args): return {'path': args.uri} + +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): + for factory in self.source.getPageFactories(): + yield factory.buildPage() + + +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: + yield PaginationData(page) +
--- a/piecrust/sources/posts.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/sources/posts.py Sat Aug 16 08:15:30 2014 -0700 @@ -6,6 +6,7 @@ import datetime from piecrust import CONTENT_DIR from piecrust.sources.base import (PageSource, IPreparingSource, + SimplePaginationSourceMixin, PageNotFoundError, InvalidFileSystemEndpointError, PageFactory, MODE_CREATING) @@ -13,7 +14,7 @@ logger = logging.getLogger(__name__) -class PostsSource(PageSource, IPreparingSource): +class PostsSource(PageSource, IPreparingSource, SimplePaginationSourceMixin): PATH_FORMAT = None def __init__(self, app, name, config):
--- a/piecrust/uriutil.py Mon Aug 11 22:36:47 2014 -0700 +++ b/piecrust/uriutil.py Sat Aug 16 08:15:30 2014 -0700 @@ -38,7 +38,7 @@ def parse_uri(routes, uri): - if string.find(uri, '..') >= 0: + if uri.find('..') >= 0: raise UriError(uri) page_num = 1
--- a/tests/mockutil.py Mon Aug 11 22:36:47 2014 -0700 +++ b/tests/mockutil.py Sat Aug 16 08:15:30 2014 -0700 @@ -1,9 +1,190 @@ +import io +import time +import random +import codecs +import os.path +import functools import mock +import yaml from piecrust.app import PieCrust, PieCrustConfiguration +resources_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + '..', 'piecrust', 'resources')) + + def get_mock_app(config=None): app = mock.MagicMock(spec=PieCrust) app.config = PieCrustConfiguration() return app + +def with_mock_fs_app(f): + @functools.wraps(f) + def wrapper(app, *args, **kwargs): + with mock_fs_scope(app): + real_app = app.getApp() + return f(real_app, *args, **kwargs) + return wrapper + + +class mock_fs(object): + def __init__(self, default_spec=True): + self._root = 'root_%d' % random.randrange(1000) + self._fs = {self._root: {}} + if default_spec: + self.withDir('counter') + self.withFile('kitchen/_content/config.yml', + "site:\n title: Mock Website\n") + + def path(self, p): + if p in ['/', '', None]: + return '/%s' % self._root + return '/%s/%s' % (self._root, p.lstrip('/')) + + def getApp(self): + root_dir = self.path('/kitchen') + return PieCrust(root_dir, cache=False) + + def withDir(self, path): + cur = self._fs[self._root] + for b in path.split('/'): + if b not in cur: + cur[b] = {} + cur = cur[b] + return self + + def withFile(self, path, contents): + cur = self._fs[self._root] + bits = path.split('/') + for b in bits[:-1]: + if b not in cur: + cur[b] = {} + cur = cur[b] + cur[bits[-1]] = (contents, {'mtime': time.time()}) + return self + + def withAsset(self, path, contents): + return self.withFile('kitchen/' + path, contents) + + def withAssetDir(self, path): + return self.withDir('kitchen/' + path) + + def withConfig(self, config): + return self.withFile('kitchen/_content/config.yml', + yaml.dump(config)) + + def withThemeConfig(self, config): + return self.withFile('kitchen/_content/theme/_content/theme_config.yml', + yaml.dump(config)) + + def withPage(self, url, config=None, contents=None): + config = config or {} + contents = contents or "A test page." + text = "---\n" + text += yaml.dump(config) + text += "---\n" + text += contents + + name, ext = os.path.splitext(url) + if not ext: + url += '.md' + url = url.lstrip('/') + return self.withAsset('_content/pages/' + url, text) + + def withPageAsset(self, page_url, name, contents=None): + contents = contents or "A test asset." + url_base, ext = os.path.splitext(page_url) + dirname = url_base + '-assets' + return self.withAsset('_content/pages/%s/%s' % (dirname, name), + contents) + + +class mock_fs_scope(object): + def __init__(self, fs): + self._fs = fs + self._root = None + self._patchers = [] + self._originals = {} + if isinstance(fs, mock_fs): + self._fs = fs._fs + self._root = fs._root + + def __enter__(self): + self._startMock() + return self + + def __exit__(self, type, value, traceback): + self._endMock() + + def _startMock(self): + self._createMock('__main__.open', open, self._open, create=True) + self._createMock('codecs.open', codecs.open, self._codecsOpen) + self._createMock('os.listdir', os.listdir, self._listdir) + self._createMock('os.path.isdir', os.path.isdir, self._isdir) + self._createMock('os.path.islink', os.path.islink, self._islink) + self._createMock('os.path.getmtime', os.path.getmtime, self._getmtime) + for p in self._patchers: + p.start() + + def _endMock(self): + for p in self._patchers: + p.stop() + + def _createMock(self, name, orig, func, **kwargs): + self._originals[name] = orig + self._patchers.append(mock.patch(name, func, **kwargs)) + + def _open(self, path, *args, **kwargs): + path = os.path.abspath(path) + if path.startswith(resources_path): + return self._originals['__main__.open'](path, **kwargs) + e = self._getFsEntry(path) + return io.StringIO(e[0]) + + def _codecsOpen(self, path, *args, **kwargs): + path = os.path.abspath(path) + if path.startswith(resources_path): + return self._originals['codecs.open'](path, *args, **kwargs) + e = self._getFsEntry(path) + return io.StringIO(e[0]) + + def _listdir(self, path): + if not path.startswith('/' + self._root): + return self._originals['os.listdir'](path) + e = self._getFsEntry(path) + if not isinstance(e, dict): + raise Exception("'%s' is not a directory." % path) + return list(e.keys()) + + def _isdir(self, path): + if not path.startswith('/' + self._root): + return self._originals['os.path.isdir'](path) + e = self._getFsEntry(path) + return e is not None and isinstance(e, dict) + + def _islink(self, path): + if not path.startswith('/' + self._root): + return self._originals['os.path.islink'](path) + return False + + def _getmtime(self, path): + if not path.startswith('/' + self._root): + return self._originals['os.path.getmtime'](path) + e = self._getFsEntry(path) + if e is None: + raise OSError() + return e[1]['mtime'] + + def _getFsEntry(self, path): + cur = self._fs + bits = path.lstrip('/').split('/') + for p in bits: + try: + cur = cur[p] + except KeyError: + return None + return cur +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_baking_baker.py Sat Aug 16 08:15:30 2014 -0700 @@ -0,0 +1,41 @@ +import pytest +from piecrust.baking.baker import PageBaker +from .mockutil import get_mock_app + + +@pytest.mark.parametrize('uri, page_num, pretty, expected', [ + # Pretty URLs + ('', 1, True, 'index.html'), + ('', 2, True, '2/index.html'), + ('foo', 1, True, 'foo/index.html'), + ('foo', 2, True, 'foo/2/index.html'), + ('foo/bar', 1, True, 'foo/bar/index.html'), + ('foo/bar', 2, True, 'foo/bar/2/index.html'), + ('foo.ext', 1, True, 'foo.ext/index.html'), + ('foo.ext', 2, True, 'foo.ext/2/index.html'), + ('foo.bar.ext', 1, True, 'foo.bar.ext/index.html'), + ('foo.bar.ext', 2, True, 'foo.bar.ext/2/index.html'), + # Ugly URLs + ('', 1, False, 'index.html'), + ('', 2, False, '2.html'), + ('foo', 1, False, 'foo.html'), + ('foo', 2, False, 'foo/2.html'), + ('foo/bar', 1, False, 'foo/bar.html'), + ('foo/bar', 2, False, 'foo/bar/2.html'), + ('foo.ext', 1, False, 'foo.ext'), + ('foo.ext', 2, False, 'foo/2.ext'), + ('foo.bar.ext', 1, False, 'foo.bar.ext'), + ('foo.bar.ext', 2, False, 'foo.bar/2.ext') + ]) +def test_get_output_path(uri, page_num, pretty, expected): + app = get_mock_app() + if pretty: + app.config.set('site/pretty_urls', True) + assert app.config.get('site/pretty_urls') == pretty + + baker = PageBaker(app, '/destination') + sub_uri = baker.getOutputUri(uri, page_num) + path = baker.getOutputPath(sub_uri) + expected = '/destination/' + expected + assert expected == path +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_data_assetor.py Sat Aug 16 08:15:30 2014 -0700 @@ -0,0 +1,55 @@ +import pytest +from mock import MagicMock +from piecrust.data.assetor import Assetor, UnsupportedAssetsError +from .mockutil import mock_fs, mock_fs_scope + + +@pytest.mark.parametrize('fs, expected', [ + (mock_fs().withPage('foo/bar'), {}), + (mock_fs() + .withPage('foo/bar') + .withPageAsset('foo/bar', 'one.txt', 'one'), + {'one': 'one'}), + (mock_fs() + .withPage('foo/bar') + .withPageAsset('foo/bar', 'one.txt', 'one') + .withPageAsset('foo/bar', 'two.txt', 'two'), + {'one': 'one', 'two': 'two'}) + ]) +def test_assets(fs, expected): + with mock_fs_scope(fs): + page = MagicMock() + page.app = fs.getApp() + page.path = fs.path('/kitchen/_content/pages/foo/bar.md') + assetor = Assetor(page, '/foo/bar') + for en in expected.keys(): + assert hasattr(assetor, en) + path = '/foo/bar/%s.txt' % en + assert getattr(assetor, en) == path + assert assetor[en] == path + + +def test_missing_asset(): + with pytest.raises(KeyError): + fs = mock_fs().withPage('foo/bar') + with mock_fs_scope(fs): + page = MagicMock() + page.app = fs.getApp() + page.path = fs.path('/kitchen/_content/pages/foo/bar.md') + assetor = Assetor(page, '/foo/bar') + assetor['this_doesnt_exist'] + + +def test_multiple_assets_with_same_name(): + with pytest.raises(UnsupportedAssetsError): + fs = (mock_fs() + .withPage('foo/bar') + .withPageAsset('foo/bar', 'one.txt', 'one text') + .withPageAsset('foo/bar', 'one.jpg', 'one picture')) + with mock_fs_scope(fs): + page = MagicMock() + page.app = fs.getApp() + page.path = fs.path('/kitchen/_content/pages/foo/bar.md') + assetor = Assetor(page, '/foo/bar') + assetor['one'] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_data_paginator.py Sat Aug 16 08:15:30 2014 -0700 @@ -0,0 +1,127 @@ +import math +import pytest +from piecrust.data.base import IPaginationSource +from piecrust.data.paginator import Paginator + + +class MockSource(list, IPaginationSource): + def __init__(self, count): + for i in range(count): + self.append('item %d' % i) + + def getItemsPerPage(self): + return 5 + + def getSourceIterator(self): + return None + + def getSorterIterator(self, it): + return None + + def getTailIterator(self, it): + return None + + def getPaginationFilter(self, page): + return None + + +@pytest.mark.parametrize('uri, page_num, count', [ + ('', 1, 0), + ('', 1, 4), + ('', 1, 5), + ('', 1, 8), + ('', 1, 14), + ('', 2, 8), + ('', 2, 14), + ('', 3, 14), + ('blog', 1, 0), + ('blog', 1, 4), + ('blog', 1, 5), + ('blog', 1, 8), + ('blog', 1, 14), + ('blog', 2, 8), + ('blog', 2, 14), + ('blog', 3, 14) + ]) +def test_paginator(uri, page_num, count): + source = MockSource(count) + p = Paginator(None, source, uri, page_num) + + if count <= 5: + # All posts fit on the page + assert p.prev_page_number is None + assert p.prev_page is None + assert p.this_page_number == 1 + assert p.this_page == uri + assert p.next_page_number is None + assert p.next_page is None + elif page_num == 1: + # First page in many + assert p.prev_page_number is None + assert p.prev_page is None + assert p.this_page_number == 1 + assert p.this_page == uri + assert p.next_page_number == 2 + np = '2' if uri == '' else (uri + '/2') + assert p.next_page == np + else: + # Page in the middle of it all + assert p.prev_page_number == page_num - 1 + if page_num == 2: + assert p.prev_page == uri + else: + pp = str(page_num - 1) if uri == '' else ( + '%s/%d' % (uri, page_num - 1)) + assert p.prev_page == pp + + assert p.this_page_number == page_num + tp = str(page_num) if uri == '' else ( + '%s/%d' % (uri, page_num)) + assert p.this_page == tp + + if page_num * 5 > count: + assert p.next_page_number is None + assert p.next_page is None + else: + assert p.next_page_number == page_num + 1 + np = str(page_num + 1) if uri == '' else ( + '%s/%d' % (uri, page_num + 1)) + assert p.next_page == np + + assert p.total_post_count == count + page_count = math.ceil(count / 5.0) + assert p.total_page_count == page_count + assert p.all_page_numbers() == list(range(1, page_count + 1)) + + for radius in range(1, 8): + width = radius * 2 + 1 + if page_count == 0: + nums = [] + else: + nums = list(filter( + lambda i: i >= 1 and i <= page_count, + range(page_num - radius, page_num + radius + 1))) + if len(nums) < width: + to_add = width - len(nums) + if nums[0] > 1: + to_add = min(to_add, nums[0] - 1) + nums = list(range(1, to_add + 1)) + nums + else: + to_add = min(to_add, page_count - nums[-1]) + nums = nums + list(range(nums[-1] + 1, nums[-1] + to_add + 1)) + assert nums == p.all_page_numbers(radius) + + itp = count + if count > 5: + if page_num * 5 < count: + itp = 5 + else: + itp = count % 5 + assert p.items_this_page == itp + + indices = list(range(count)) + indices = indices[(page_num - 1) * 5 : (page_num - 1) * 5 + itp] + expected = list(['item %d' % i for i in indices]) + items = list(p.items) + assert items == expected +
--- a/tests/test_pagebaker.py Mon Aug 11 22:36:47 2014 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -import pytest -from piecrust.baking.baker import PageBaker -from .mockutil import get_mock_app - - -@pytest.mark.parametrize('uri, page_num, pretty, expected', [ - # Pretty URLs - ('', 1, True, 'index.html'), - ('', 2, True, '2/index.html'), - ('foo', 1, True, 'foo/index.html'), - ('foo', 2, True, 'foo/2/index.html'), - ('foo/bar', 1, True, 'foo/bar/index.html'), - ('foo/bar', 2, True, 'foo/bar/2/index.html'), - ('foo.ext', 1, True, 'foo.ext/index.html'), - ('foo.ext', 2, True, 'foo.ext/2/index.html'), - ('foo.bar.ext', 1, True, 'foo.bar.ext/index.html'), - ('foo.bar.ext', 2, True, 'foo.bar.ext/2/index.html'), - # Ugly URLs - ('', 1, False, 'index.html'), - ('', 2, False, '2.html'), - ('foo', 1, False, 'foo.html'), - ('foo', 2, False, 'foo/2.html'), - ('foo/bar', 1, False, 'foo/bar.html'), - ('foo/bar', 2, False, 'foo/bar/2.html'), - ('foo.ext', 1, False, 'foo.ext'), - ('foo.ext', 2, False, 'foo/2.ext'), - ('foo.bar.ext', 1, False, 'foo.bar.ext'), - ('foo.bar.ext', 2, False, 'foo.bar/2.ext') - ]) -def test_get_output_path(uri, page_num, pretty, expected): - app = get_mock_app() - if pretty: - app.config.set('site/pretty_urls', True) - assert app.config.get('site/pretty_urls') == pretty - - baker = PageBaker(app, '/destination') - path = baker.getOutputPath(uri, page_num) - expected = '/destination/' + expected - assert expected == path -
--- a/tests/test_server.py Mon Aug 11 22:36:47 2014 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,34 +0,0 @@ -import re -import pytest -import mock -from piecrust.serving import find_routes -from piecrust.sources.base import REALM_USER, REALM_THEME - - -@pytest.mark.parametrize('uri, route_specs, expected', - [ - ('/', - [{'src': 'pages', 'pat': '(?P<path>.*)'}], - [('pages', {'path': ''})]), - ('/', - [{'src': 'pages', 'pat': '(?P<path>.*)'}, - {'src': 'theme', 'pat': '(?P<path>.*)', 'realm': REALM_THEME}], - [('pages', {'path': ''}), ('theme', {'path': ''})]) - ]) -def test_find_routes(uri, route_specs, expected): - routes = [] - for rs in route_specs: - m = mock.Mock() - m.source_name = rs['src'] - m.source_realm = rs.setdefault('realm', REALM_USER) - m.uri_re = re.compile(rs['pat']) - routes.append(m) - matching = find_routes(routes, uri) - - assert len(matching) == len(expected) - for i in range(len(matching)): - route, metadata = matching[i] - exp_source, exp_md = expected[i] - assert route.source_name == exp_source - assert metadata == exp_md -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_serving.py Sat Aug 16 08:15:30 2014 -0700 @@ -0,0 +1,34 @@ +import re +import pytest +import mock +from piecrust.serving import find_routes +from piecrust.sources.base import REALM_USER, REALM_THEME + + +@pytest.mark.parametrize('uri, route_specs, expected', + [ + ('/', + [{'src': 'pages', 'pat': '(?P<path>.*)'}], + [('pages', {'path': ''})]), + ('/', + [{'src': 'pages', 'pat': '(?P<path>.*)'}, + {'src': 'theme', 'pat': '(?P<path>.*)', 'realm': REALM_THEME}], + [('pages', {'path': ''}), ('theme', {'path': ''})]) + ]) +def test_find_routes(uri, route_specs, expected): + routes = [] + for rs in route_specs: + m = mock.Mock() + m.source_name = rs['src'] + m.source_realm = rs.setdefault('realm', REALM_USER) + m.uri_re = re.compile(rs['pat']) + routes.append(m) + matching = find_routes(routes, uri) + + assert len(matching) == len(expected) + for i in range(len(matching)): + route, metadata = matching[i] + exp_source, exp_md = expected[i] + assert route.source_name == exp_source + assert metadata == exp_md +