comparison piecrust/appconfig.py @ 681:894d286b348f

internal: Refactor config loading some more. * Remove fixup code in the app to make the app config class more standalone. * Remove support for old-style variants... maybe bring it back later. * Try and fix various bugs introduced by subtle config value overriding order changes.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 08 Mar 2016 01:07:34 -0800
parents 3df808b133f8
children ec384174b8b2
comparison
equal deleted inserted replaced
680:c2ea75e37540 681:894d286b348f
26 super(VariantNotFoundError, self).__init__( 26 super(VariantNotFoundError, self).__init__(
27 message or ("No such configuration variant: %s" % 27 message or ("No such configuration variant: %s" %
28 variant_name)) 28 variant_name))
29 29
30 30
31 def _make_variant_fixup(variant_name, raise_if_not_found):
32 def _variant_fixup(index, config):
33 if index != -1:
34 return
35 try:
36 try:
37 v = get_dict_value(config, 'variants/%s' % variant_name)
38 except KeyError:
39 raise VariantNotFoundError(variant_name)
40 if not isinstance(v, dict):
41 raise VariantNotFoundError(
42 variant_name,
43 "Configuration variant '%s' is not an array. "
44 "Check your configuration file." % variant_name)
45 merge_dicts(config, v)
46 except VariantNotFoundError:
47 if raise_if_not_found:
48 raise
49
50 return _variant_fixup
51
52
53 class PieCrustConfiguration(Configuration): 31 class PieCrustConfiguration(Configuration):
54 def __init__(self, paths=None, cache=None, values=None, validate=True, 32 def __init__(self, *, path=None, theme_path=None, values=None,
55 theme_config=False): 33 cache=None, validate=True, theme_config=False):
56 super(PieCrustConfiguration, self).__init__() 34 super(PieCrustConfiguration, self).__init__()
57 self._paths = paths 35 self._path = path
36 self._theme_path = theme_path
58 self._cache = cache or NullCache() 37 self._cache = cache or NullCache()
59 self._fixups = [] 38 self._custom_paths = []
39 self._post_fixups = []
60 self.theme_config = theme_config 40 self.theme_config = theme_config
61 # Set the values after we set the rest, since our validation needs 41 # Set the values after we set the rest, since our validation needs
62 # our attributes. 42 # our attributes.
63 if values: 43 if values:
64 self.setAll(values, validate=validate) 44 self.setAll(values, validate=validate)
65 45
66 def addFixup(self, f): 46 def addPath(self, p):
67 self._ensureNotLoaded() 47 self._ensureNotLoaded()
68 self._fixups.append(f) 48 self._custom_paths.append(p)
69
70 def addPath(self, p, first=False):
71 self._ensureNotLoaded()
72 if not first:
73 self._paths.append(p)
74 else:
75 self._paths.insert(0, p)
76 49
77 def addVariant(self, variant_path, raise_if_not_found=True): 50 def addVariant(self, variant_path, raise_if_not_found=True):
78 self._ensureNotLoaded() 51 self._ensureNotLoaded()
79 if os.path.isfile(variant_path): 52 if os.path.isfile(variant_path):
80 self.addPath(variant_path) 53 self.addPath(variant_path)
81 else: 54 elif raise_if_not_found:
82 name, _ = os.path.splitext(os.path.basename(variant_path)) 55 logger.error(
83 fixup = _make_variant_fixup(name, raise_if_not_found) 56 "Configuration variants should now be `.yml` files "
84 self.addFixup(fixup) 57 "located in the `configs/` directory of your website.")
85 58 raise VariantNotFoundError(variant_path)
86 logger.warning( 59
87 "Configuration variants should now be `.yml` files located "
88 "in the `configs/` directory of your website.")
89 logger.warning(
90 "Variants defined in the site configuration will be "
91 "deprecated in a future version of PieCrust.")
92 60
93 def addVariantValue(self, path, value): 61 def addVariantValue(self, path, value):
94 def _fixup(index, config): 62 def _fixup(config):
95 set_dict_value(config, path, value) 63 set_dict_value(config, path, value)
96 self.addFixup(_fixup) 64 self._post_fixups.append(_fixup)
97 65
98 def _ensureNotLoaded(self): 66 def _ensureNotLoaded(self):
99 if self._values is not None: 67 if self._values is not None:
100 raise Exception("The configurations has been loaded.") 68 raise Exception("The configurations has been loaded.")
101 69
102 def _load(self): 70 def _load(self):
103 if self._paths is None: 71 paths_and_fixups = []
72 if self._theme_path:
73 paths_and_fixups.append((self._theme_path, self._fixupThemeConfig))
74 if self._path:
75 paths_and_fixups.append((self._path, None))
76 paths_and_fixups += [(p, None) for p in self._custom_paths]
77
78 if not paths_and_fixups:
104 self._values = self._validateAll({}) 79 self._values = self._validateAll({})
105 return 80 return
106 81
107 path_times = [os.path.getmtime(p) for p in self._paths] 82 path_times = [os.path.getmtime(p[0]) for p in paths_and_fixups]
108 83
109 cache_key_hash = hashlib.md5( 84 cache_key_hash = hashlib.md5(
110 ("version=%s&cache=%d" % ( 85 ("version=%s&cache=%d" % (
111 APP_VERSION, CACHE_VERSION)).encode('utf8')) 86 APP_VERSION, CACHE_VERSION)).encode('utf8'))
112 for p in self._paths: 87 for p, _ in paths_and_fixups:
113 cache_key_hash.update(("&path=%s" % p).encode('utf8')) 88 cache_key_hash.update(("&path=%s" % p).encode('utf8'))
114 cache_key = cache_key_hash.hexdigest() 89 cache_key = cache_key_hash.hexdigest()
115 90
116 if self._cache.isValid('config.json', path_times): 91 if self._cache.isValid('config.json', path_times):
117 logger.debug("Loading configuration from cache...") 92 logger.debug("Loading configuration from cache...")
125 self._values['__cache_valid'] = True 100 self._values['__cache_valid'] = True
126 return 101 return
127 logger.debug("Outdated cache key '%s' (expected '%s')." % ( 102 logger.debug("Outdated cache key '%s' (expected '%s')." % (
128 actual_cache_key, cache_key)) 103 actual_cache_key, cache_key))
129 104
130 logger.debug("Loading configuration from: %s" % self._paths) 105 logger.debug("Loading configuration from: %s" %
106 ', '.join([p[0] for p in paths_and_fixups]))
131 values = {} 107 values = {}
132 try: 108 try:
133 for i, p in enumerate(self._paths): 109 for p, f in paths_and_fixups:
134 with open(p, 'r', encoding='utf-8') as fp: 110 with open(p, 'r', encoding='utf-8') as fp:
135 loaded_values = yaml.load( 111 loaded_values = yaml.load(
136 fp.read(), 112 fp.read(),
137 Loader=ConfigurationLoader) 113 Loader=ConfigurationLoader)
138 if loaded_values is None: 114 if loaded_values is None:
139 loaded_values = {} 115 loaded_values = {}
140 for fixup in self._fixups: 116 if f:
141 fixup(i, loaded_values) 117 f(loaded_values)
142 merge_dicts(values, loaded_values) 118 merge_dicts(values, loaded_values)
143 119
144 for fixup in self._fixups: 120 for f in self._post_fixups:
145 fixup(-1, values) 121 f(values)
146 122
147 self._values = self._validateAll(values) 123 self._values = self._validateAll(values)
148 except Exception as ex: 124 except Exception as ex:
149 raise Exception("Error loading configuration from: %s" % 125 raise Exception(
150 ', '.join(self._paths)) from ex 126 "Error loading configuration from: %s" %
127 ', '.join([p[0] for p in paths_and_fixups])) from ex
151 128
152 logger.debug("Caching configuration...") 129 logger.debug("Caching configuration...")
153 self._values['__cache_key'] = cache_key 130 self._values['__cache_key'] = cache_key
154 config_text = json.dumps(self._values) 131 config_text = json.dumps(self._values)
155 self._cache.write('config.json', config_text) 132 self._cache.write('config.json', config_text)
158 135
159 def _validateAll(self, values): 136 def _validateAll(self, values):
160 if values is None: 137 if values is None:
161 values = {} 138 values = {}
162 139
163 # Add the loaded values to the default configuration. 140 # We 'prepend' newer values to default values, so we need to do
164 values = merge_dicts(copy.deepcopy(default_configuration), values) 141 # things in the following order so that the final list is made of:
165 142 # (1) user values, (2) site values, (3) theme values.
166 # Set the theme site flag. 143 # Still, we need to do a few theme-related things before generating
167 if self.theme_config: 144 # the default site content model.
168 values['site']['theme_site'] = True 145 values = self._preValidateThemeConfig(values)
169 146 values = self._validateSiteConfig(values)
170 # Figure out if we need to generate the configuration for the 147 values = self._validateThemeConfig(values)
171 # default content model. 148
172 sitec = values.setdefault('site', {}) 149 # Add a section for our cached information, and start visiting
173 gen_default_model = bool(sitec.get('use_default_content')) 150 # the configuration tree, calling validation functions as we
174 if gen_default_model: 151 # find them.
175 logger.debug("Generating default content model...")
176 values = self._generateDefaultContentModel(values)
177
178 # Add a section for our cached information.
179 cachec = collections.OrderedDict() 152 cachec = collections.OrderedDict()
180 values['__cache'] = cachec 153 values['__cache'] = cachec
181 cache_writer = _ConfigCacheWriter(cachec) 154 cache_writer = _ConfigCacheWriter(cachec)
182 globs = globals() 155 globs = globals()
183 156
197 170
198 visit_dict(values, _visitor) 171 visit_dict(values, _visitor)
199 172
200 return values 173 return values
201 174
202 def _generateDefaultContentModel(self, values): 175 def _fixupThemeConfig(self, values):
203 dcmcopy = copy.deepcopy(default_content_model_base) 176 # Make all sources belong to the "theme" realm.
204 values = merge_dicts(dcmcopy, values) 177 sitec = values.get('site')
205 178 if sitec:
206 blogsc = values['site'].get('blogs') 179 srcc = sitec.get('sources')
207 if blogsc is None: 180 if srcc:
208 blogsc = ['posts'] 181 for sn, sc in srcc.items():
209 values['site']['blogs'] = blogsc 182 sc['realm'] = REALM_THEME
210 183
211 is_only_blog = (len(blogsc) == 1) 184 def _preValidateThemeConfig(self, values):
212 for blog_name in blogsc: 185 if not self._theme_path:
213 blog_cfg = get_default_content_model_for_blog( 186 return values
214 blog_name, is_only_blog, values, 187
215 theme_site=self.theme_config) 188 sitec = values.setdefault('site', collections.OrderedDict())
216 values = merge_dicts(blog_cfg, values) 189 gen_default_theme_model = bool(sitec.setdefault(
217 190 'use_default_theme_content', True))
218 dcm = get_default_content_model(values) 191 if gen_default_theme_model:
219 values = merge_dicts(dcm, values) 192 pmcopy = copy.deepcopy(default_theme_content_pre_model)
193 values = merge_dicts(pmcopy, values)
194 return values
195
196
197 def _validateThemeConfig(self, values):
198 if not self._theme_path:
199 return values
200
201 # Create the default theme content model if needed.
202 sitec = values.setdefault('site', collections.OrderedDict())
203 gen_default_theme_model = bool(sitec.setdefault(
204 'use_default_theme_content', True))
205 if gen_default_theme_model:
206 logger.debug("Generating default theme content model...")
207 dcmcopy = copy.deepcopy(default_theme_content_model_base)
208 values = merge_dicts(dcmcopy, values)
209 return values
210
211
212 def _validateSiteConfig(self, values):
213 # Add the loaded values to the default configuration.
214 dccopy = copy.deepcopy(default_configuration)
215 values = merge_dicts(dccopy, values)
216
217 # Set the theme site flag.
218 sitec = values['site']
219 if self.theme_config:
220 sitec['theme_site'] = True
221
222 # Create the default content model if needed.
223 gen_default_model = bool(sitec['use_default_content'])
224 if gen_default_model:
225 logger.debug("Generating default content model...")
226 dcmcopy = copy.deepcopy(default_content_model_base)
227 values = merge_dicts(dcmcopy, values)
228
229 blogsc = values['site'].get('blogs')
230 if blogsc is None:
231 blogsc = ['posts']
232 values['site']['blogs'] = blogsc
233
234 is_only_blog = (len(blogsc) == 1)
235 for blog_name in blogsc:
236 blog_cfg = get_default_content_model_for_blog(
237 blog_name, is_only_blog, values,
238 theme_site=self.theme_config)
239 values = merge_dicts(blog_cfg, values)
240
241 dcm = get_default_content_model(values)
242 values = merge_dicts(dcm, values)
220 243
221 return values 244 return values
222 245
223 246
224 class _ConfigCacheWriter(object): 247 class _ConfigCacheWriter(object):
270 'default_post_layout': 'post', 293 'default_post_layout': 'post',
271 'post_url': '%year%/%month%/%day%/%slug%', 294 'post_url': '%year%/%month%/%day%/%slug%',
272 'tag_url': 'tag/%tag%', 295 'tag_url': 'tag/%tag%',
273 'category_url': '%category%', 296 'category_url': '%category%',
274 'posts_per_page': 5 297 'posts_per_page': 5
298 })
299 })
300
301
302 default_theme_content_pre_model = collections.OrderedDict({
303 'site': collections.OrderedDict({
304 'theme_tag_page': 'theme_pages:_tag.%ext%',
305 'theme_category_page': 'theme_pages:_category.%ext%'
306 })
307 })
308
309
310 default_theme_content_model_base = collections.OrderedDict({
311 'site': collections.OrderedDict({
312 'sources': collections.OrderedDict({
313 'theme_pages': {
314 'type': 'default',
315 'ignore_missing_dir': True,
316 'fs_endpoint': 'pages',
317 'data_endpoint': 'site.pages',
318 'default_layout': 'default',
319 'item_name': 'page',
320 'realm': REALM_THEME
321 }
322 }),
323 'routes': [
324 {
325 'url': '/%path:slug%',
326 'source': 'theme_pages',
327 'func': 'pcurl(slug)'
328 }
329 ]
275 }) 330 })
276 }) 331 })
277 332
278 333
279 def get_default_content_model(values): 334 def get_default_content_model(values):