Mercurial > piecrust2
comparison piecrust/data/base.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 | 5be275137056 |
children | d446029c9478 |
comparison
equal
deleted
inserted
replaced
439:c0700c6d9545 | 440:32c7c2d219d2 |
---|---|
1 import copy | 1 import collections.abc |
2 import time | |
3 import logging | |
4 from piecrust.data.assetor import Assetor | |
5 from piecrust.routing import create_route_metadata | |
6 from piecrust.uriutil import split_uri | |
7 | 2 |
8 | 3 |
9 logger = logging.getLogger(__name__) | 4 class MergedMapping(collections.abc.Mapping): |
10 | 5 """ Provides a dictionary-like object that's really the aggregation of |
11 | 6 multiple dictionary-like objects. |
12 class LazyPageConfigLoaderHasNoValue(Exception): | |
13 """ An exception that can be returned when a loader for `LazyPageConfig` | |
14 can't return any value. | |
15 """ | 7 """ |
16 pass | 8 def __init__(self, dicts, path=''): |
17 | 9 self._dicts = dicts |
18 | 10 self._path = path |
19 class LazyPageConfigData(object): | |
20 """ An object that represents the configuration header of a page, | |
21 but also allows for additional data. It's meant to be exposed | |
22 to the templating system. | |
23 """ | |
24 debug_render = [] | |
25 debug_render_invoke = [] | |
26 debug_render_dynamic = ['_debugRenderKeys'] | |
27 debug_render_invoke_dynamic = ['_debugRenderKeys'] | |
28 | |
29 def __init__(self, page): | |
30 self._page = page | |
31 self._values = None | |
32 self._loaders = None | |
33 | |
34 @property | |
35 def page(self): | |
36 return self._page | |
37 | |
38 def get(self, name): | |
39 try: | |
40 return self._getValue(name) | |
41 except LazyPageConfigLoaderHasNoValue: | |
42 return None | |
43 | 11 |
44 def __getattr__(self, name): | 12 def __getattr__(self, name): |
45 try: | 13 try: |
46 return self._getValue(name) | 14 return self[name] |
47 except LazyPageConfigLoaderHasNoValue as ex: | 15 except KeyError: |
48 raise AttributeError("No such attribute: %s" % name) from ex | 16 raise AttributeError("No such attribute: %s" % self._subp(name)) |
49 | 17 |
50 def __getitem__(self, name): | 18 def __getitem__(self, name): |
51 try: | 19 values = [] |
52 return self._getValue(name) | 20 for d in self._dicts: |
53 except LazyPageConfigLoaderHasNoValue as ex: | 21 try: |
54 raise KeyError("No such key: %s" % name) from ex | 22 val = d[name] |
23 except KeyError: | |
24 continue | |
25 values.append(val) | |
55 | 26 |
56 def _getValue(self, name): | 27 if len(values) == 0: |
57 self._load() | 28 raise KeyError("No such item: %s" % self._subp(name)) |
29 if len(values) == 1: | |
30 return values[0] | |
58 | 31 |
59 if name in self._values: | 32 for val in values: |
60 return self._values[name] | 33 if not isinstance(val, (dict, collections.abc.Mapping)): |
34 raise Exception( | |
35 "Template data for '%s' contains an incompatible mix " | |
36 "of data: %s" % ( | |
37 self._subp(name), | |
38 ', '.join([str(type(v)) for v in values]))) | |
61 | 39 |
62 if self._loaders: | 40 return MergedMapping(values, self._subp(name)) |
63 loader = self._loaders.get(name) | |
64 if loader is not None: | |
65 try: | |
66 self._values[name] = loader(self, name) | |
67 except LazyPageConfigLoaderHasNoValue: | |
68 raise | |
69 except Exception as ex: | |
70 raise Exception( | |
71 "Error while loading attribute '%s' for: %s" % | |
72 (name, self._page.rel_path)) from ex | |
73 | 41 |
74 # We need to double-check `_loaders` here because | 42 def __iter__(self): |
75 # the loader could have removed all loaders, which | 43 keys = set() |
76 # would set this back to `None`. | 44 for d in self._dicts: |
77 if self._loaders is not None: | 45 keys |= set(d.keys()) |
78 del self._loaders[name] | 46 return iter(keys) |
79 if len(self._loaders) == 0: | |
80 self._loaders = None | |
81 | 47 |
82 else: | 48 def __len__(self): |
83 loader = self._loaders.get('*') | 49 keys = set() |
84 if loader is not None: | 50 for d in self._dicts: |
85 try: | 51 keys |= set(d.keys()) |
86 self._values[name] = loader(self, name) | 52 return len(keys) |
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 | 53 |
95 if name not in self._values: | 54 def _subp(self, name): |
96 raise LazyPageConfigLoaderHasNoValue() | 55 return '%s/%s' % (self._path, name) |
97 return self._values[name] | |
98 | 56 |
99 def _setValue(self, name, value): | 57 def _prependMapping(self, d): |
100 if self._values is None: | 58 self._dicts.insert(0, d) |
101 raise Exception("Can't call _setValue before this data has been " | |
102 "loaded") | |
103 self._values[name] = value | |
104 | 59 |
105 def mapLoader(self, attr_name, loader, override_existing=False): | 60 def _appendMapping(self, d): |
106 if loader is None: | 61 self._dicts.append(d) |
107 if self._loaders is None or attr_name not in self._loaders: | |
108 return | |
109 del self._loaders[attr_name] | |
110 if len(self._loaders) == 0: | |
111 self._loaders = None | |
112 return | |
113 | 62 |
114 if self._loaders is None: | |
115 self._loaders = {} | |
116 if not override_existing and attr_name in self._loaders: | |
117 raise Exception( | |
118 "A loader has already been mapped for: %s" % attr_name) | |
119 self._loaders[attr_name] = loader | |
120 | |
121 def mapValue(self, attr_name, value, override_existing=False): | |
122 loader = lambda _, __: value | |
123 self.mapLoader(attr_name, loader, override_existing=override_existing) | |
124 | |
125 def _load(self): | |
126 if self._values is not None: | |
127 return | |
128 self._values = self._page.config.getDeepcopy(self._page.app.debug) | |
129 try: | |
130 self._loadCustom() | |
131 except Exception as ex: | |
132 raise Exception( | |
133 "Error while loading data for: %s" % | |
134 self._page.rel_path) from ex | |
135 | |
136 def _loadCustom(self): | |
137 pass | |
138 | |
139 def _debugRenderKeys(self): | |
140 self._load() | |
141 keys = set(self._values.keys()) | |
142 if self._loaders: | |
143 keys |= set(self._loaders.keys()) | |
144 return list(keys) | |
145 | |
146 | |
147 class PaginationData(LazyPageConfigData): | |
148 def __init__(self, page): | |
149 super(PaginationData, self).__init__(page) | |
150 self._route = None | |
151 self._route_metadata = None | |
152 | |
153 def _get_uri(self): | |
154 page = self._page | |
155 if self._route is None: | |
156 # TODO: this is not quite correct, as we're missing parts of the | |
157 # route metadata if the current page is a taxonomy page. | |
158 route_metadata = create_route_metadata(page) | |
159 self._route = page.app.getRoute(page.source.name, route_metadata) | |
160 self._route_metadata = route_metadata | |
161 if self._route is None: | |
162 raise Exception("Can't get route for page: %s" % page.path) | |
163 return self._route.getUri(self._route_metadata) | |
164 | |
165 def _loadCustom(self): | |
166 page_url = self._get_uri() | |
167 _, slug = split_uri(self.page.app, page_url) | |
168 self._setValue('url', page_url) | |
169 self._setValue('slug', slug) | |
170 self._setValue( | |
171 'timestamp', | |
172 time.mktime(self.page.datetime.timetuple())) | |
173 date_format = self.page.app.config.get('site/date_format') | |
174 if date_format: | |
175 self._setValue('date', self.page.datetime.strftime(date_format)) | |
176 self._setValue('mtime', self.page.path_mtime) | |
177 | |
178 assetor = Assetor(self.page, page_url) | |
179 self._setValue('assets', assetor) | |
180 | |
181 segment_names = self.page.config.get('segments') | |
182 for name in segment_names: | |
183 self.mapLoader(name, self._load_rendered_segment) | |
184 | |
185 def _load_rendered_segment(self, data, name): | |
186 do_render = True | |
187 eis = self._page.app.env.exec_info_stack | |
188 if eis is not None and eis.hasPage(self._page): | |
189 # This is the pagination data for the page that is currently | |
190 # being rendered! Inception! But this is possible... so just | |
191 # prevent infinite recursion. | |
192 do_render = False | |
193 | |
194 assert self is data | |
195 | |
196 if do_render: | |
197 uri = self._get_uri() | |
198 try: | |
199 from piecrust.rendering import ( | |
200 QualifiedPage, PageRenderingContext, | |
201 render_page_segments) | |
202 qp = QualifiedPage(self._page, self._route, | |
203 self._route_metadata) | |
204 ctx = PageRenderingContext(qp) | |
205 render_result = render_page_segments(ctx) | |
206 segs = render_result.segments | |
207 except Exception as e: | |
208 raise Exception( | |
209 "Error rendering segments for '%s'" % uri) from e | |
210 else: | |
211 segs = {} | |
212 for name in self.page.config.get('segments'): | |
213 segs[name] = "<unavailable: current page>" | |
214 | |
215 for k, v in segs.items(): | |
216 self.mapLoader(k, None) | |
217 self._setValue(k, v) | |
218 | |
219 if 'content.abstract' in segs: | |
220 self._setValue('content', segs['content.abstract']) | |
221 self._setValue('has_more', True) | |
222 if name == 'content': | |
223 return segs['content.abstract'] | |
224 | |
225 return segs[name] | |
226 |