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