Mercurial > piecrust2
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): |