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