Mercurial > piecrust2
diff piecrust/app.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 | aaa8fb7c8918 |
children | 474c9882decf |
line wrap: on
line diff
--- a/piecrust/app.py Wed Dec 25 22:16:46 2013 -0800 +++ b/piecrust/app.py Sun Aug 10 23:43:16 2014 -0700 @@ -1,3 +1,4 @@ +import re import json import os.path import types @@ -5,30 +6,28 @@ import hashlib import logging import yaml -from cache import SimpleCache -from decorators import lazy_property -from plugins.base import PluginLoader -from environment import StandardEnvironment -from configuration import Configuration, merge_dicts - - -APP_VERSION = '2.0.0alpha' -CACHE_VERSION = '2.0' - -CACHE_DIR = '_cache' -TEMPLATES_DIR = '_content/templates' -PAGES_DIR = '_content/pages' -POSTS_DIR = '_content/posts' -PLUGINS_DIR = '_content/plugins' -THEME_DIR = '_content/theme' - -CONFIG_PATH = '_content/config.yml' -THEME_CONFIG_PATH = '_content/theme_config.yml' +from werkzeug.utils import cached_property +from piecrust import (APP_VERSION, + CACHE_DIR, TEMPLATES_DIR, + PLUGINS_DIR, THEME_DIR, + CONFIG_PATH, THEME_CONFIG_PATH, + DEFAULT_FORMAT, DEFAULT_TEMPLATE_ENGINE, DEFAULT_POSTS_FS, + DEFAULT_DATE_FORMAT, DEFAULT_PLUGIN_SOURCE, DEFAULT_THEME_SOURCE) +from piecrust.cache import ExtensibleCache, NullCache, NullExtensibleCache +from piecrust.plugins.base import PluginLoader +from piecrust.environment import StandardEnvironment +from piecrust.configuration import Configuration, ConfigurationError, merge_dicts +from piecrust.routing import Route +from piecrust.sources.base import REALM_USER, REALM_THEME +from piecrust.taxonomies import Taxonomy logger = logging.getLogger(__name__) +CACHE_VERSION = 10 + + class VariantNotFoundError(Exception): def __init__(self, variant_path, message=None): super(VariantNotFoundError, self).__init__( @@ -36,10 +35,10 @@ class PieCrustConfiguration(Configuration): - def __init__(self, paths=None, cache_dir=False): - super(PieCrustConfiguration, self).__init__() + def __init__(self, paths=None, cache=None, values=None, validate=True): + super(PieCrustConfiguration, self).__init__(values, validate) self.paths = paths - self.cache_dir = cache_dir + self.cache = cache or NullCache() self.fixups = [] def applyVariant(self, variant_path, raise_if_not_found=True): @@ -58,29 +57,29 @@ if self.paths is None: self._values = self._validateAll({}) return - - path_times = filter(self.paths, - lambda p: os.path.getmtime(p)) - cache_key = hashlib.md5("version=%s&cache=%s" % ( - APP_VERSION, CACHE_VERSION)) - - cache = None - if self.cache_dir: - cache = SimpleCache(self.cache_dir) + + path_times = map(lambda p: os.path.getmtime(p), self.paths) + cache_key = hashlib.md5("version=%s&cache=%d" % ( + APP_VERSION, CACHE_VERSION)).hexdigest() - if cache is not None: - if cache.isValid('config.json', path_times): - config_text = cache.read('config.json') - self._values = json.loads(config_text) - - actual_cache_key = self._values.get('__cache_key') - if actual_cache_key == cache_key: - return + if self.cache.isValid('config.json', path_times): + logger.debug("Loading configuration from cache...") + config_text = self.cache.read('config.json') + self._values = json.loads(config_text) + + actual_cache_key = self._values.get('__cache_key') + if actual_cache_key == cache_key: + return + logger.debug("Outdated cache key '%s' (expected '%s')." % ( + actual_cache_key, cache_key)) values = {} + logger.debug("Loading configuration from: %s" % self.paths) for i, p in enumerate(self.paths): with codecs.open(p, 'r', 'utf-8') as fp: loaded_values = yaml.load(fp.read()) + if loaded_values is None: + loaded_values = {} for fixup in self.fixups: fixup(i, loaded_values) merge_dicts(values, loaded_values) @@ -90,51 +89,307 @@ self._values = self._validateAll(values) - if cache is not None: - self._values['__cache_key'] = cache_key - config_text = json.dumps(self._values) - cache.write('config.json', config_text) + logger.debug("Caching configuration...") + self._values['__cache_key'] = cache_key + config_text = json.dumps(self._values) + self.cache.write('config.json', config_text) + + def _validateAll(self, values): + # Put all the defaults in the `site` section. + default_sitec = { + 'title': "Untitled PieCrust website", + 'root': '/', + 'default_format': DEFAULT_FORMAT, + 'default_template_engine': DEFAULT_TEMPLATE_ENGINE, + 'enable_gzip': True, + 'pretty_urls': False, + 'slugify': 'transliterate|lowercase', + 'timezone': False, + 'locale': False, + 'date_format': DEFAULT_DATE_FORMAT, + 'auto_formats': { + 'html': '', + 'md': 'markdown', + 'textile': 'textile'}, + 'default_auto_format': 'md', + 'pagination_suffix': '/%num%', + 'plugins_sources': [DEFAULT_PLUGIN_SOURCE], + 'themes_sources': [DEFAULT_THEME_SOURCE], + 'cache_time': 28800, + 'display_errors': True, + 'enable_debug_info': True + } + sitec = values.get('site') + if sitec is None: + sitec = {} + for key, val in default_sitec.iteritems(): + sitec.setdefault(key, val) + values['site'] = sitec + + # Add a section for our cached information. + cachec = {} + values['__cache'] = cachec + + # Cache auto-format regexes. + if not isinstance(sitec['auto_formats'], dict): + raise ConfigurationError("The 'site/auto_formats' setting must be a dictionary.") + cachec['auto_formats_re'] = r"\.(%s)$" % ( + '|'.join( + map(lambda i: re.escape(i), sitec['auto_formats'].keys()))) + if sitec['default_auto_format'] not in sitec['auto_formats']: + raise ConfigurationError("Default auto-format '%s' is not declared." % sitec['default_auto_format']) + + # Cache pagination suffix regex. + pgn_suffix = re.escape(sitec['pagination_suffix']) + pgn_suffix = pgn_suffix.replace("\\%num\\%", "(?P<num>\\d+)") + '$' + cachec['pagination_suffix_re'] = pgn_suffix + + # Make sure plugins and theme sources are lists. + if not isinstance(sitec['plugins_sources'], list): + sitec['plugins_sources'] = [sitec['plugins_sources']] + if not isinstance(sitec['themes_sources'], list): + sitec['themes_sources'] = [sitec['themes_sources']] + + # Setup values for posts/items. + ipp = sitec.get('posts_per_page') + if ipp is not None: + sitec.setdefault('items_per_page', ipp) + pf = sitec.get('posts_filters') + if pf is not None: + sitec.setdefault('items_filters', pf) + + # Figure out if we need to validate sources/routes, or auto-generate + # them from simple blog settings. + if 'sources' not in sitec: + posts_fs = sitec.setdefault('posts_fs', DEFAULT_POSTS_FS) + blogsc = sitec.setdefault('blogs', ['posts']) + + g_post_url = sitec.get('post_url', '%year%/%month%/%slug%') + g_tag_url = sitec.get('tag_url', 'tag/%tag%') + g_category_url = sitec.get('category_url', '%category%') + g_posts_per_page = sitec.get('items_per_page', 5) + g_posts_filters = sitec.get('items_filters') + g_date_format = sitec.get('date_format', DEFAULT_DATE_FORMAT) + + sourcesc = {} + sourcesc['pages'] = { + 'type': 'default', + 'data_endpoint': 'site/pages', + 'item_name': 'page'} + sitec['sources'] = sourcesc + + routesc = [] + sitec['routes'] = routesc + + taxonomiesc = {} + taxonomiesc['tags'] = { + 'multiple': True, + 'term': 'tag'} + taxonomiesc['categories'] = { + 'term': 'category'} + sitec['taxonomies'] = taxonomiesc + + for blog_name in blogsc: + blogc = values.get(blog_name, {}) + url_prefix = blog_name + '/' + endpoint = 'posts/%s' % blog_name + item_name = '%s-post' % blog_name + items_per_page = blogc.get('posts_per_page', g_posts_per_page) + items_filters = blogc.get('posts_filters', g_posts_filters) + date_format = blogc.get('date_format', g_date_format) + if len(blogsc) == 1: + url_prefix = '' + endpoint = 'posts' + item_name = 'post' + sourcesc[blog_name] = { + 'type': 'posts/%s' % posts_fs, + 'fs_endpoint': endpoint, + 'data_type': 'blog', + 'item_name': item_name, + 'items_per_page': items_per_page, + 'items_filters': items_filters, + 'date_format': date_format, + 'default_layout': 'post'} + tax_page_prefix = '' + if len(blogsc) > 1: + tax_page_prefix = blog_name + '/' + sourcesc[blog_name]['taxonomy_pages'] = { + 'tags': ('pages:%s_tag.%%ext%%;' + 'theme_pages:_tag.%%ext%%' % + tax_page_prefix), + 'categories': ('pages:%s_category.%%ext%%;' + 'theme_pages:_category.%%ext%%' % + tax_page_prefix)} + + post_url = blogc.get('post_url', url_prefix + g_post_url) + post_url = '/' + post_url.lstrip('/') + tag_url = blogc.get('tag_url', url_prefix + g_tag_url) + tag_url = '/' + tag_url.lstrip('/') + category_url = blogc.get('category_url', url_prefix + g_category_url) + category_url = '/' + category_url.lstrip('/') + routesc.append({'url': post_url, 'source': blog_name, + 'func': 'pcposturl(year,month,day,slug)'}) + routesc.append({'url': tag_url, 'source': blog_name, + 'taxonomy': 'tags', + 'func': 'pctagurl(tag)'}) + routesc.append({'url': category_url, 'source': blog_name, + 'taxonomy': 'categories', + 'func': 'pccaturl(category)'}) + + routesc.append({'url': '/%path:path%', 'source': 'pages', + 'func': 'pcurl(path)'}) + + # Validate sources/routes. + sourcesc = sitec.get('sources') + routesc = sitec.get('routes') + if not sourcesc: + raise ConfigurationError("There are no sources defined.") + if not routesc: + raise ConfigurationError("There are no routes defined.") + if not isinstance(sourcesc, dict): + raise ConfigurationError("The 'site/sources' setting must be a dictionary.") + if not isinstance(routesc, list): + raise ConfigurationError("The 'site/routes' setting must be a list.") + + # Add the theme page source if no sources were defined in the theme + # configuration itself. + has_any_theme_source = False + for sn, sc in sourcesc.iteritems(): + if sc.get('realm') == REALM_THEME: + has_any_theme_source = True + break + if not has_any_theme_source: + sitec['sources']['theme_pages'] = { + 'theme_source': True, + 'fs_endpoint': 'pages', + 'data_endpoint': 'site/pages', + 'item_name': 'page', + 'realm': REALM_THEME} + sitec['routes'].append({ + 'url': '/%path:path%', + 'source': 'theme_pages', + 'func': 'pcurl(path)'}) + + # Sources have the `default` scanner by default, duh. Also, a bunch + # of other default values for other configuration stuff. + for sn, sc in sourcesc.iteritems(): + if not isinstance(sc, dict): + raise ConfigurationError("All sources in 'site/sources' must be dictionaries.") + sc.setdefault('type', 'default') + sc.setdefault('fs_endpoint', sn) + sc.setdefault('data_endpoint', sn) + sc.setdefault('data_type', 'iterator') + sc.setdefault('item_name', sn) + sc.setdefault('items_per_page', 5) + sc.setdefault('date_format', DEFAULT_DATE_FORMAT) + sc.setdefault('realm', REALM_USER) + + # Check routes are referencing correct routes, have default + # values, etc. + for rc in routesc: + if not isinstance(rc, dict): + raise ConfigurationError("All routes in 'site/routes' must be dictionaries.") + rc_url = rc.get('url') + if not rc_url: + raise ConfigurationError("All routes in 'site/routes' must have an 'url'.") + if rc_url[0] != '/': + raise ConfigurationError("Route URLs must start with '/'.") + if rc.get('source') is None: + raise ConfigurationError("Routes must specify a source.") + if rc['source'] not in sourcesc.keys(): + raise ConfigurationError("Route is referencing unknown source: %s" % + rc['source']) + rc.setdefault('taxonomy', None) + rc.setdefault('page_suffix', '/%num%') + + # Validate taxonomies. + sitec.setdefault('taxonomies', {}) + taxonomiesc = sitec.get('taxonomies') + for tn, tc in taxonomiesc.iteritems(): + tc.setdefault('multiple', False) + tc.setdefault('term', tn) + tc.setdefault('page', '_%s.%%ext%%' % tc['term']) + + # Validate endpoints, and make sure the theme has a default source. + reserved_endpoints = set(['piecrust', 'site', 'page', 'route', + 'assets', 'pagination', 'siblings', + 'family']) + for name, src in sitec['sources'].iteritems(): + endpoint = src['data_endpoint'] + if endpoint in reserved_endpoints: + raise ConfigurationError( + "Source '%s' is using a reserved endpoint name: %s" % + (name, endpoint)) + + + # Done validating! + return values class PieCrust(object): - def __init__(self, root, cache=True, debug=False, env=None): - self.root = root + def __init__(self, root_dir, cache=True, debug=False, theme_site=False, + env=None): + self.root_dir = root_dir self.debug = debug - self.cache = cache + self.theme_site = theme_site self.plugin_loader = PluginLoader(self) + + if cache: + self.cache = ExtensibleCache(self.cache_dir) + else: + self.cache = NullExtensibleCache() + self.env = env if self.env is None: self.env = StandardEnvironment() self.env.initialize(self) - @lazy_property + @cached_property def config(self): - logger.debug("Loading site configuration...") + logger.debug("Creating site configuration...") paths = [] if self.theme_dir: paths.append(os.path.join(self.theme_dir, THEME_CONFIG_PATH)) - paths.append(os.path.join(self.root, CONFIG_PATH)) + paths.append(os.path.join(self.root_dir, CONFIG_PATH)) - config = PieCrustConfiguration(paths, self.cache_dir) + config_cache = self.cache.getCache('app') + config = PieCrustConfiguration(paths, config_cache) if self.theme_dir: # We'll need to patch the templates directories to be relative # to the site's root, and not the theme root. def _fixupThemeTemplatesDir(index, config): - if index == 0: - sitec = config.get('site') - if sitec: - tplc = sitec.get('templates_dirs') - if tplc: - if isinstance(tplc, types.StringTypes): - tplc = [tplc] - sitec['templates_dirs'] = filter(tplc, - lambda p: os.path.join(self.theme_dir, p)) + if index != 0: + return + sitec = config.get('site') + if sitec is None: + return + tplc = sitec.get('templates_dirs') + if tplc is None: + return + if isinstance(tplc, types.StringTypes): + tplc = [tplc] + sitec['templates_dirs'] = filter(tplc, + lambda p: os.path.join(self.theme_dir, p)) + config.fixups.append(_fixupThemeTemplatesDir) - config.fixups.append(_fixupThemeTemplatesDir) + # We'll also need to flag all page sources as coming from + # the theme. + def _fixupThemeSources(index, config): + if index != 0: + return + sitec = config.get('site') + if sitec is None: + sitec = {} + config['site'] = sitec + srcc = sitec.get('sources') + if srcc is not None: + for sn, sc in srcc.iteritems(): + sc['realm'] = REALM_THEME + config.fixups.append(_fixupThemeSources) return config - @lazy_property + @cached_property def templates_dirs(self): templates_dirs = self._get_configurable_dirs(TEMPLATES_DIR, 'site/templates_dirs') @@ -147,34 +402,88 @@ return templates_dirs - @lazy_property - def pages_dir(self): - return self._get_dir(PAGES_DIR) - - @lazy_property - def posts_dir(self): - return self._get_dir(POSTS_DIR) - - @lazy_property + @cached_property def plugins_dirs(self): return self._get_configurable_dirs(PLUGINS_DIR, 'site/plugins_dirs') - @lazy_property + @cached_property def theme_dir(self): - return self._get_dir(THEME_DIR) + td = self._get_dir(THEME_DIR) + if td is not None: + return td + return os.path.join(os.path.dirname(__file__), 'resources', 'theme') + + @cached_property + def cache_dir(self): + return os.path.join(self.root_dir, CACHE_DIR) + + @cached_property + def sources(self): + defs = {} + for cls in self.plugin_loader.getSources(): + defs[cls.SOURCE_NAME] = cls + + sources = [] + for n, s in self.config.get('site/sources').iteritems(): + cls = defs.get(s['type']) + if cls is None: + raise ConfigurationError("No such page source type: %s" % s['type']) + src = cls(self, n, s) + sources.append(src) + return sources + + @cached_property + def routes(self): + routes = [] + for r in self.config.get('site/routes'): + rte = Route(self, r) + routes.append(rte) + return routes - @lazy_property - def cache_dir(self): - if self.cache: - return os.path.join(self.root, CACHE_DIR) - return False + @cached_property + def taxonomies(self): + taxonomies = [] + for tn, tc in self.config.get('site/taxonomies').iteritems(): + tax = Taxonomy(self, tn, tc) + taxonomies.append(tax) + return taxonomies + + def getSource(self, source_name): + for source in self.sources: + if source.name == source_name: + return source + return None + + 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 is None: + yield route + + def getRoute(self, source_name, source_metadata): + for route in self.getRoutes(source_name, True): + if route.isMatch(source_metadata): + return route + return None + + def getTaxonomyRoute(self, tax_name, source_name): + for route in self.routes: + if route.taxonomy == tax_name and route.source_name == source_name: + return route + return None + + def getTaxonomy(self, tax_name): + for tax in self.taxonomies: + if tax.name == tax_name: + return tax + return None def _get_dir(self, default_rel_dir): - abs_dir = os.path.join(self.root, default_rel_dir) + abs_dir = os.path.join(self.root_dir, default_rel_dir) if os.path.isdir(abs_dir): return abs_dir - return False + return None def _get_configurable_dirs(self, default_rel_dir, conf_name): dirs = [] @@ -182,11 +491,14 @@ # Add custom directories from the configuration. conf_dirs = self.config.get(conf_name) if conf_dirs is not None: - dirs += filter(conf_dirs, - lambda p: os.path.join(self.root, p)) + if isinstance(conf_dirs, types.StringTypes): + dirs.append(os.path.join(self.root_dir, conf_dirs)) + else: + dirs += filter(lambda p: os.path.join(self.root_dir, p), + conf_dirs) # Add the default directory if it exists. - default_dir = os.path.join(self.root, default_rel_dir) + default_dir = os.path.join(self.root_dir, default_rel_dir) if os.path.isdir(default_dir): dirs.append(default_dir)