comparison piecrust/appconfig.py @ 666:81d9c3a3a0b5

internal: Get rid of the whole "sub cache" business. * Compute cache keys up front, so the cache directory is only chosen once. * Buffer up config variants to apply before loading the config. Makes it possible to cache variant-resulting configs, too. * Make a factory class to reuse the logic that creates the `PieCrust` object correctly for multi-process workers and such. * Add a test.
author Ludovic Chabant <ludovic@chabant.com>
date Thu, 03 Mar 2016 08:22:41 -0800
parents 3ceeca7bb71c
children 3df808b133f8
comparison
equal deleted inserted replaced
665:5dc13c816045 666:81d9c3a3a0b5
12 DEFAULT_FORMAT, DEFAULT_TEMPLATE_ENGINE, DEFAULT_POSTS_FS, 12 DEFAULT_FORMAT, DEFAULT_TEMPLATE_ENGINE, DEFAULT_POSTS_FS,
13 DEFAULT_DATE_FORMAT, DEFAULT_THEME_SOURCE) 13 DEFAULT_DATE_FORMAT, DEFAULT_THEME_SOURCE)
14 from piecrust.cache import NullCache 14 from piecrust.cache import NullCache
15 from piecrust.configuration import ( 15 from piecrust.configuration import (
16 Configuration, ConfigurationError, ConfigurationLoader, 16 Configuration, ConfigurationError, ConfigurationLoader,
17 merge_dicts, visit_dict) 17 get_dict_value, set_dict_value, merge_dicts, visit_dict)
18 from piecrust.sources.base import REALM_USER, REALM_THEME 18 from piecrust.sources.base import REALM_USER, REALM_THEME
19 19
20 20
21 logger = logging.getLogger(__name__) 21 logger = logging.getLogger(__name__)
22 22
23 23
24 class VariantNotFoundError(Exception): 24 class VariantNotFoundError(Exception):
25 def __init__(self, variant_path, message=None): 25 def __init__(self, variant_name, message=None):
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_path)) 28 variant_name))
29
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
29 51
30 52
31 class PieCrustConfiguration(Configuration): 53 class PieCrustConfiguration(Configuration):
32 def __init__(self, paths=None, cache=None, values=None, validate=True, 54 def __init__(self, paths=None, cache=None, values=None, validate=True,
33 theme_config=False): 55 theme_config=False):
34 super(PieCrustConfiguration, self).__init__() 56 super(PieCrustConfiguration, self).__init__()
35 self.paths = paths 57 self._paths = paths
36 self.cache = cache or NullCache() 58 self._cache = cache or NullCache()
37 self.fixups = [] 59 self._fixups = []
38 self.theme_config = theme_config 60 self.theme_config = theme_config
39 # Set the values after we set the rest, since our validation needs 61 # Set the values after we set the rest, since our validation needs
40 # our attributes. 62 # our attributes.
41 if values: 63 if values:
42 self.setAll(values, validate=validate) 64 self.setAll(values, validate=validate)
43 65
44 def applyVariant(self, variant_path, raise_if_not_found=True): 66 def addFixup(self, f):
45 variant = self.get(variant_path) 67 self._ensureNotLoaded()
46 if variant is None: 68 self._fixups.append(f)
47 if raise_if_not_found: 69
48 raise VariantNotFoundError(variant_path) 70 def addPath(self, p, first=False):
49 return 71 self._ensureNotLoaded()
50 if not isinstance(variant, dict): 72 if not first:
51 raise VariantNotFoundError( 73 self._paths.append(p)
52 variant_path, 74 else:
53 "Configuration variant '%s' is not an array. " 75 self._paths.insert(0, p)
54 "Check your configuration file." % variant_path) 76
55 self.merge(variant) 77 def addVariant(self, variant_path, raise_if_not_found=True):
78 self._ensureNotLoaded()
79 if os.path.isfile(variant_path):
80 self.addPath(variant_path)
81 else:
82 name, _ = os.path.splitext(os.path.basename(variant_path))
83 fixup = _make_variant_fixup(name, raise_if_not_found)
84 self.addFixup(fixup)
85
86 logger.warning(
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
93 def addVariantValue(self, path, value):
94 def _fixup(index, config):
95 set_dict_value(config, path, value)
96 self.addFixup(_fixup)
97
98 def _ensureNotLoaded(self):
99 if self._values is not None:
100 raise Exception("The configurations has been loaded.")
56 101
57 def _load(self): 102 def _load(self):
58 if self.paths is None: 103 if self._paths is None:
59 self._values = self._validateAll({}) 104 self._values = self._validateAll({})
60 return 105 return
61 106
62 path_times = [os.path.getmtime(p) for p in self.paths] 107 path_times = [os.path.getmtime(p) for p in self._paths]
63 108
64 cache_key_hash = hashlib.md5( 109 cache_key_hash = hashlib.md5(
65 ("version=%s&cache=%d" % ( 110 ("version=%s&cache=%d" % (
66 APP_VERSION, CACHE_VERSION)).encode('utf8')) 111 APP_VERSION, CACHE_VERSION)).encode('utf8'))
67 for p in self.paths: 112 for p in self._paths:
68 cache_key_hash.update(("&path=%s" % p).encode('utf8')) 113 cache_key_hash.update(("&path=%s" % p).encode('utf8'))
69 cache_key = cache_key_hash.hexdigest() 114 cache_key = cache_key_hash.hexdigest()
70 115
71 if self.cache.isValid('config.json', path_times): 116 if self._cache.isValid('config.json', path_times):
72 logger.debug("Loading configuration from cache...") 117 logger.debug("Loading configuration from cache...")
73 config_text = self.cache.read('config.json') 118 config_text = self._cache.read('config.json')
74 self._values = json.loads( 119 self._values = json.loads(
75 config_text, 120 config_text,
76 object_pairs_hook=collections.OrderedDict) 121 object_pairs_hook=collections.OrderedDict)
77 122
78 actual_cache_key = self._values.get('__cache_key') 123 actual_cache_key = self._values.get('__cache_key')
80 self._values['__cache_valid'] = True 125 self._values['__cache_valid'] = True
81 return 126 return
82 logger.debug("Outdated cache key '%s' (expected '%s')." % ( 127 logger.debug("Outdated cache key '%s' (expected '%s')." % (
83 actual_cache_key, cache_key)) 128 actual_cache_key, cache_key))
84 129
85 logger.debug("Loading configuration from: %s" % self.paths) 130 logger.debug("Loading configuration from: %s" % self._paths)
86 values = {} 131 values = {}
87 try: 132 try:
88 for i, p in enumerate(self.paths): 133 for i, p in enumerate(self._paths):
89 with open(p, 'r', encoding='utf-8') as fp: 134 with open(p, 'r', encoding='utf-8') as fp:
90 loaded_values = yaml.load( 135 loaded_values = yaml.load(
91 fp.read(), 136 fp.read(),
92 Loader=ConfigurationLoader) 137 Loader=ConfigurationLoader)
93 if loaded_values is None: 138 if loaded_values is None:
94 loaded_values = {} 139 loaded_values = {}
95 for fixup in self.fixups: 140 for fixup in self._fixups:
96 fixup(i, loaded_values) 141 fixup(i, loaded_values)
97 merge_dicts(values, loaded_values) 142 merge_dicts(values, loaded_values)
98 143
99 for fixup in self.fixups: 144 for fixup in self._fixups:
100 fixup(len(self.paths), values) 145 fixup(-1, values)
101 146
102 self._values = self._validateAll(values) 147 self._values = self._validateAll(values)
103 except Exception as ex: 148 except Exception as ex:
104 raise Exception("Error loading configuration from: %s" % 149 raise Exception("Error loading configuration from: %s" %
105 ', '.join(self.paths)) from ex 150 ', '.join(self._paths)) from ex
106 151
107 logger.debug("Caching configuration...") 152 logger.debug("Caching configuration...")
108 self._values['__cache_key'] = cache_key 153 self._values['__cache_key'] = cache_key
109 config_text = json.dumps(self._values) 154 config_text = json.dumps(self._values)
110 self.cache.write('config.json', config_text) 155 self._cache.write('config.json', config_text)
111 156
112 self._values['__cache_valid'] = False 157 self._values['__cache_valid'] = False
113 158
114 def _validateAll(self, values): 159 def _validateAll(self, values):
115 if values is None: 160 if values is None: