Mercurial > piecrust2
comparison piecrust/data/pagedata.py @ 440:32c7c2d219d2
performance: Refactor how data is managed to reduce copying.
* Make use of `collections.abc.Mapping` to better identify things that are
supposed to look like dictionaries.
* Instead of handling "overlay" of data in a dict tree in each different data
object, make all objects `Mapping`s and handle merging at a higher level
with the new `MergedMapping` object.
* Since this new object is read-only, remove the need for deep-copying of
app and page configurations.
* Split data classes into separate modules.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 28 Jun 2015 08:22:39 -0700 |
parents | |
children | 785dea918ad8 |
comparison
equal
deleted
inserted
replaced
439:c0700c6d9545 | 440:32c7c2d219d2 |
---|---|
1 import time | |
2 import collections.abc | |
3 | |
4 | |
5 class LazyPageConfigLoaderHasNoValue(Exception): | |
6 """ An exception that can be returned when a loader for `LazyPageConfig` | |
7 can't return any value. | |
8 """ | |
9 pass | |
10 | |
11 | |
12 class LazyPageConfigData(collections.abc.Mapping): | |
13 """ An object that represents the configuration header of a page, | |
14 but also allows for additional data. It's meant to be exposed | |
15 to the templating system. | |
16 """ | |
17 debug_render = [] | |
18 debug_render_invoke = [] | |
19 debug_render_dynamic = ['_debugRenderKeys'] | |
20 debug_render_invoke_dynamic = ['_debugRenderKeys'] | |
21 | |
22 def __init__(self, page): | |
23 self._page = page | |
24 self._values = {} | |
25 self._loaders = {} | |
26 self._is_loaded = False | |
27 | |
28 def __getattr__(self, name): | |
29 try: | |
30 return self._getValue(name) | |
31 except LazyPageConfigLoaderHasNoValue as ex: | |
32 raise AttributeError("No such attribute: %s" % name) from ex | |
33 | |
34 def __getitem__(self, name): | |
35 try: | |
36 return self._getValue(name) | |
37 except LazyPageConfigLoaderHasNoValue as ex: | |
38 raise KeyError("No such key: %s" % name) from ex | |
39 | |
40 def __iter__(self): | |
41 keys = list(self._page.config.keys()) | |
42 keys += list(self._values.keys()) | |
43 keys += list(self._loaders.keys()) | |
44 return iter(keys) | |
45 | |
46 def __len__(self): | |
47 return len(self._page.config) + len(self._values) + len(self._loaders) | |
48 | |
49 def _getValue(self, name): | |
50 # First try the page configuration itself. | |
51 try: | |
52 return self._page.config[name] | |
53 except KeyError: | |
54 pass | |
55 | |
56 # Then try loaded values. | |
57 self._ensureLoaded() | |
58 try: | |
59 return self._values[name] | |
60 except KeyError: | |
61 pass | |
62 | |
63 # Try a loader for a new value. | |
64 loader = self._loaders.get(name) | |
65 if loader is not None: | |
66 try: | |
67 self._values[name] = loader(self, name) | |
68 except LazyPageConfigLoaderHasNoValue: | |
69 raise | |
70 except Exception as ex: | |
71 raise Exception( | |
72 "Error while loading attribute '%s' for: %s" % | |
73 (name, self._page.rel_path)) from ex | |
74 | |
75 # Forget this loader now that it served its purpose. | |
76 try: | |
77 del self._loaders[name] | |
78 except KeyError: | |
79 pass | |
80 return self._values[name] | |
81 | |
82 # Try the wildcard loader if it exists. | |
83 loader = self._loaders.get('*') | |
84 if loader is not None: | |
85 try: | |
86 self._values[name] = loader(self, name) | |
87 except LazyPageConfigLoaderHasNoValue: | |
88 raise | |
89 except Exception as ex: | |
90 raise Exception( | |
91 "Error while loading attribute '%s' for: %s" % | |
92 (name, self._page.rel_path)) from ex | |
93 # We always keep the wildcard loader in the loaders list. | |
94 return self._values[name] | |
95 | |
96 raise LazyPageConfigLoaderHasNoValue() | |
97 | |
98 def _setValue(self, name, value): | |
99 self._values[name] = value | |
100 | |
101 def _unmapLoader(self, attr_name): | |
102 try: | |
103 del self._loaders[attr_name] | |
104 except KeyError: | |
105 pass | |
106 | |
107 def _mapLoader(self, attr_name, loader, override_existing=False): | |
108 assert loader is not None | |
109 | |
110 if not override_existing and attr_name in self._loaders: | |
111 raise Exception( | |
112 "A loader has already been mapped for: %s" % attr_name) | |
113 self._loaders[attr_name] = loader | |
114 | |
115 def _mapValue(self, attr_name, value, override_existing=False): | |
116 loader = lambda _, __: value | |
117 self._mapLoader(attr_name, loader, override_existing=override_existing) | |
118 | |
119 def _ensureLoaded(self): | |
120 if self._is_loaded: | |
121 return | |
122 | |
123 self._is_loaded = True | |
124 try: | |
125 self._load() | |
126 except Exception as ex: | |
127 raise Exception( | |
128 "Error while loading data for: %s" % | |
129 self._page.rel_path) from ex | |
130 | |
131 def _load(self): | |
132 pass | |
133 | |
134 def _debugRenderKeys(self): | |
135 self._ensureLoaded() | |
136 keys = set(self._values.keys()) | |
137 if self._loaders: | |
138 keys |= set(self._loaders.keys()) | |
139 return list(keys) | |
140 | |
141 | |
142 class PageData(LazyPageConfigData): | |
143 """ Template data for a page. | |
144 """ | |
145 def __init__(self, page, ctx): | |
146 super(PageData, self).__init__(page) | |
147 self._ctx = ctx | |
148 | |
149 def _load(self): | |
150 page = self._page | |
151 for k, v in page.source_metadata.items(): | |
152 self._setValue(k, v) | |
153 self._setValue('url', self._ctx.uri) | |
154 self._setValue('timestamp', time.mktime(page.datetime.timetuple())) | |
155 date_format = page.app.config.get('site/date_format') | |
156 if date_format: | |
157 self._setValue('date', page.datetime.strftime(date_format)) | |
158 |