Mercurial > piecrust2
diff piecrust/appconfig.py @ 683:ec384174b8b2
internal: More work/fixes on how default/theme/user configs are merged.
Change how the code is organized to have better data flow. Add some tests.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 09 Mar 2016 00:23:51 -0800 |
parents | 894d286b348f |
children | 1a6c4c2683fd |
line wrap: on
line diff
--- a/piecrust/appconfig.py Tue Mar 08 01:07:56 2016 -0800 +++ b/piecrust/appconfig.py Wed Mar 09 00:23:51 2016 -0800 @@ -14,7 +14,7 @@ from piecrust.cache import NullCache from piecrust.configuration import ( Configuration, ConfigurationError, ConfigurationLoader, - get_dict_value, set_dict_value, merge_dicts, visit_dict) + try_get_dict_value, set_dict_value, merge_dicts, visit_dict) from piecrust.sources.base import REALM_USER, REALM_THEME @@ -31,6 +31,9 @@ class PieCrustConfiguration(Configuration): def __init__(self, *, path=None, theme_path=None, values=None, cache=None, validate=True, theme_config=False): + if theme_config and theme_path: + raise Exception("Can't be a theme site config and still have a " + "theme applied.") super(PieCrustConfiguration, self).__init__() self._path = path self._theme_path = theme_path @@ -61,33 +64,35 @@ def addVariantValue(self, path, value): def _fixup(config): set_dict_value(config, path, value) + self._post_fixups.append(_fixup) + def setAll(self, values, validate=False): + # Override base class implementation + values = self._combineConfigs({}, values) + if validate: + values = self._validateAll(values) + self._values = values + def _ensureNotLoaded(self): if self._values is not None: raise Exception("The configurations has been loaded.") def _load(self): - paths_and_fixups = [] - if self._theme_path: - paths_and_fixups.append((self._theme_path, self._fixupThemeConfig)) - if self._path: - paths_and_fixups.append((self._path, None)) - paths_and_fixups += [(p, None) for p in self._custom_paths] + # Figure out where to load this configuration from. + paths = [self._theme_path, self._path] + self._custom_paths + paths = list(filter(lambda i: i is not None, paths)) - if not paths_and_fixups: - self._values = self._validateAll({}) - return - - path_times = [os.path.getmtime(p[0]) for p in paths_and_fixups] - + # Build the cache-key. + path_times = [os.path.getmtime(p[0]) for p in paths] cache_key_hash = hashlib.md5( ("version=%s&cache=%d" % ( APP_VERSION, CACHE_VERSION)).encode('utf8')) - for p, _ in paths_and_fixups: + for p in paths: cache_key_hash.update(("&path=%s" % p).encode('utf8')) cache_key = cache_key_hash.hexdigest() + # Check the cache for a valid version. if self._cache.isValid('config.json', path_times): logger.debug("Loading configuration from cache...") config_text = self._cache.read('config.json') @@ -97,34 +102,49 @@ actual_cache_key = self._values.get('__cache_key') if actual_cache_key == cache_key: + # The cached version has the same key! Awesome! self._values['__cache_valid'] = True return logger.debug("Outdated cache key '%s' (expected '%s')." % ( actual_cache_key, cache_key)) - logger.debug("Loading configuration from: %s" % - ', '.join([p[0] for p in paths_and_fixups])) - values = {} + # Nope, load from the paths. try: - for p, f in paths_and_fixups: - with open(p, 'r', encoding='utf-8') as fp: - loaded_values = yaml.load( - fp.read(), - Loader=ConfigurationLoader) - if loaded_values is None: - loaded_values = {} - if f: - f(loaded_values) - merge_dicts(values, loaded_values) + # Theme config. + theme_values = {} + if self._theme_path: + theme_values = self._loadFrom(self._theme_path) + + # Site config. + site_values = {} + if self._path: + site_values = self._loadFrom(self._path) + + # Combine! + logger.debug("Processing loaded configurations...") + values = self._combineConfigs(theme_values, site_values) - for f in self._post_fixups: - f(values) + # Load additional paths. + if self._custom_paths: + logger.debug("Loading %d additional configuration paths." % + len(self._custom_paths)) + for p in self._custom_paths: + loaded = self._loadFrom(p) + if loaded: + merge_dicts(values, loaded) + + # Run final fixups + if self._post_fixups: + logger.debug("Applying %d configuration fixups." % + len(self._post_fixups)) + for f in self._post_fixups: + f(values) self._values = self._validateAll(values) except Exception as ex: raise Exception( "Error loading configuration from: %s" % - ', '.join([p[0] for p in paths_and_fixups])) from ex + ', '.join(paths)) from ex logger.debug("Caching configuration...") self._values['__cache_key'] = cache_key @@ -133,19 +153,59 @@ self._values['__cache_valid'] = False + def _loadFrom(self, path): + logger.debug("Loading configuration from: %s" % path) + with open(path, 'r', encoding='utf-8') as fp: + values = yaml.load( + fp.read(), + Loader=ConfigurationLoader) + if values is None: + values = {} + return values + + def _combineConfigs(self, theme_values, site_values): + # Start with the default configuration. + values = copy.deepcopy(default_configuration) + + if not self.theme_config: + # If the theme config wants the default model, add it. + theme_sitec = theme_values.setdefault( + 'site', collections.OrderedDict()) + gen_default_theme_model = bool(theme_sitec.setdefault( + 'use_default_theme_content', True)) + if gen_default_theme_model: + self._generateDefaultThemeModel(values) + + # Now override with the actual theme config values. + values = merge_dicts(values, theme_values) + + # Make all sources belong to the "theme" realm at this point. + srcc = values['site'].get('sources') + if srcc: + for sn, sc in srcc.items(): + sc['realm'] = REALM_THEME + + # If the site config wants the default model, add it. + site_sitec = site_values.setdefault( + 'site', collections.OrderedDict()) + gen_default_site_model = bool(site_sitec.setdefault( + 'use_default_content', True)) + if gen_default_site_model: + self._generateDefaultSiteModel(values, site_values) + + # And override with the actual site config values. + values = merge_dicts(values, site_values) + + # Set the theme site flag. + if self.theme_config: + values['site']['theme_site'] = True + + return values + def _validateAll(self, values): if values is None: values = {} - # We 'prepend' newer values to default values, so we need to do - # things in the following order so that the final list is made of: - # (1) user values, (2) site values, (3) theme values. - # Still, we need to do a few theme-related things before generating - # the default site content model. - values = self._preValidateThemeConfig(values) - values = self._validateSiteConfig(values) - values = self._validateThemeConfig(values) - # Add a section for our cached information, and start visiting # the configuration tree, calling validation functions as we # find them. @@ -172,76 +232,30 @@ return values - def _fixupThemeConfig(self, values): - # Make all sources belong to the "theme" realm. - sitec = values.get('site') - if sitec: - srcc = sitec.get('sources') - if srcc: - for sn, sc in srcc.items(): - sc['realm'] = REALM_THEME - - def _preValidateThemeConfig(self, values): - if not self._theme_path: - return values + def _generateDefaultThemeModel(self, values): + logger.debug("Generating default theme content model...") + cc = copy.deepcopy(default_theme_content_model_base) + merge_dicts(values, cc) - sitec = values.setdefault('site', collections.OrderedDict()) - gen_default_theme_model = bool(sitec.setdefault( - 'use_default_theme_content', True)) - if gen_default_theme_model: - pmcopy = copy.deepcopy(default_theme_content_pre_model) - values = merge_dicts(pmcopy, values) - return values - - - def _validateThemeConfig(self, values): - if not self._theme_path: - return values + def _generateDefaultSiteModel(self, values, user_overrides): + logger.debug("Generating default content model...") + cc = copy.deepcopy(default_content_model_base) + merge_dicts(values, cc) - # Create the default theme content model if needed. - sitec = values.setdefault('site', collections.OrderedDict()) - gen_default_theme_model = bool(sitec.setdefault( - 'use_default_theme_content', True)) - if gen_default_theme_model: - logger.debug("Generating default theme content model...") - dcmcopy = copy.deepcopy(default_theme_content_model_base) - values = merge_dicts(dcmcopy, values) - return values - - - def _validateSiteConfig(self, values): - # Add the loaded values to the default configuration. - dccopy = copy.deepcopy(default_configuration) - values = merge_dicts(dccopy, values) - - # Set the theme site flag. - sitec = values['site'] - if self.theme_config: - sitec['theme_site'] = True + dcm = get_default_content_model(values, user_overrides) + merge_dicts(values, dcm) - # Create the default content model if needed. - gen_default_model = bool(sitec['use_default_content']) - if gen_default_model: - logger.debug("Generating default content model...") - dcmcopy = copy.deepcopy(default_content_model_base) - values = merge_dicts(dcmcopy, values) - - blogsc = values['site'].get('blogs') - if blogsc is None: - blogsc = ['posts'] - values['site']['blogs'] = blogsc + blogsc = try_get_dict_value(user_overrides, 'site/blogs') + if blogsc is None: + blogsc = ['posts'] + set_dict_value(user_overrides, 'site/blogs', blogsc) - is_only_blog = (len(blogsc) == 1) - for blog_name in blogsc: - blog_cfg = get_default_content_model_for_blog( - blog_name, is_only_blog, values, - theme_site=self.theme_config) - values = merge_dicts(blog_cfg, values) - - dcm = get_default_content_model(values) - values = merge_dicts(dcm, values) - - return values + is_only_blog = (len(blogsc) == 1) + for blog_name in blogsc: + blog_cfg = get_default_content_model_for_blog( + blog_name, is_only_blog, values, user_overrides, + theme_site=self.theme_config) + merge_dicts(values, blog_cfg) class _ConfigCacheWriter(object): @@ -253,6 +267,32 @@ self._cache_dict[name] = val +default_theme_content_model_base = collections.OrderedDict({ + 'site': collections.OrderedDict({ + 'sources': collections.OrderedDict({ + 'theme_pages': { + 'type': 'default', + 'ignore_missing_dir': True, + 'fs_endpoint': 'pages', + 'data_endpoint': 'site.pages', + 'default_layout': 'default', + 'item_name': 'page', + 'realm': REALM_THEME + } + }), + 'routes': [ + { + 'url': '/%path:slug%', + 'source': 'theme_pages', + 'func': 'pcurl(slug)' + } + ], + 'theme_tag_page': 'theme_pages:_tag.%ext%', + 'theme_category_page': 'theme_pages:_category.%ext%' + }) + }) + + default_configuration = collections.OrderedDict({ 'site': collections.OrderedDict({ 'title': "Untitled PieCrust website", @@ -276,6 +316,7 @@ 'enable_debug_info': True, 'show_debug_info': False, 'use_default_content': True, + 'use_default_theme_content': True, 'theme_site': False }), 'baker': collections.OrderedDict({ @@ -299,40 +340,10 @@ }) -default_theme_content_pre_model = collections.OrderedDict({ - 'site': collections.OrderedDict({ - 'theme_tag_page': 'theme_pages:_tag.%ext%', - 'theme_category_page': 'theme_pages:_category.%ext%' - }) - }) - - -default_theme_content_model_base = collections.OrderedDict({ - 'site': collections.OrderedDict({ - 'sources': collections.OrderedDict({ - 'theme_pages': { - 'type': 'default', - 'ignore_missing_dir': True, - 'fs_endpoint': 'pages', - 'data_endpoint': 'site.pages', - 'default_layout': 'default', - 'item_name': 'page', - 'realm': REALM_THEME - } - }), - 'routes': [ - { - 'url': '/%path:slug%', - 'source': 'theme_pages', - 'func': 'pcurl(slug)' - } - ] - }) - }) - - -def get_default_content_model(values): - default_layout = values['site']['default_page_layout'] +def get_default_content_model(values, user_overrides): + default_layout = try_get_dict_value( + user_overrides, 'site/default_page_layout', + values['site']['default_page_layout']) return collections.OrderedDict({ 'site': collections.OrderedDict({ 'sources': collections.OrderedDict({ @@ -365,10 +376,17 @@ def get_default_content_model_for_blog( - blog_name, is_only_blog, values, theme_site=False): - posts_fs = values['site']['posts_fs'] - blog_cfg = values.get(blog_name, {}) + blog_name, is_only_blog, values, user_overrides, theme_site=False): + # Get the global values for various things we're interested in. + defs = {} + names = ['posts_fs', 'posts_per_page', 'date_format', + 'default_post_layout', 'post_url', 'tag_url', 'category_url'] + for n in names: + defs[n] = try_get_dict_value( + user_overrides, 'site/%s' % n, + values['site'][n]) + # More stuff we need. if is_only_blog: url_prefix = '' tax_page_prefix = '' @@ -388,22 +406,24 @@ data_endpoint = blog_name item_name = '%s-post' % blog_name - items_per_page = blog_cfg.get( - 'posts_per_page', values['site']['posts_per_page']) - date_format = blog_cfg.get( - 'date_format', values['site']['date_format']) - default_layout = blog_cfg.get( - 'default_layout', values['site']['default_post_layout']) + # Figure out the settings values for this blog, specifically. + # The value could be set on the blog config itself, globally, or left at + # its default. We already handle the "globally vs. default" with the + # `defs` map that we computed above. + blog_cfg = user_overrides.get(blog_name, {}) + blog_values = {} + for n in names: + blog_values[n] = blog_cfg.get(n, defs[n]) + if n in ['post_url', 'tag_url', 'category_url']: + blog_values[n] = url_prefix + blog_values[n] - post_url = '/' + blog_cfg.get( - 'post_url', - url_prefix + values['site']['post_url']).lstrip('/') - tag_url = '/' + blog_cfg.get( - 'tag_url', - url_prefix + values['site']['tag_url']).lstrip('/') - category_url = '/' + blog_cfg.get( - 'category_url', - url_prefix + values['site']['category_url']).lstrip('/') + posts_fs = blog_values['posts_fs'] + posts_per_page = blog_values['posts_per_page'] + date_format = blog_values['date_format'] + default_layout = blog_values['default_post_layout'] + post_url = '/' + blog_values['post_url'].lstrip('/') + tag_url = '/' + blog_values['tag_url'].lstrip('/') + category_url = '/' + blog_values['category_url'].lstrip('/') tags_taxonomy = 'pages:%s_tag.%%ext%%' % tax_page_prefix category_taxonomy = 'pages:%s_category.%%ext%%' % tax_page_prefix @@ -425,7 +445,7 @@ 'item_name': item_name, 'ignore_missing_dir': True, 'data_type': 'blog', - 'items_per_page': items_per_page, + 'items_per_page': posts_per_page, 'date_format': date_format, 'default_layout': default_layout, 'taxonomy_pages': collections.OrderedDict({