comparison piecrust/sources/base.py @ 3:f485ba500df3

Gigantic change to basically make PieCrust 2 vaguely functional. - Serving works, with debug window. - Baking works, multi-threading, with dependency handling. - Various things not implemented yet.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 10 Aug 2014 23:43:16 -0700
parents
children 474c9882decf
comparison
equal deleted inserted replaced
2:40fa08b261b9 3:f485ba500df3
1 import re
2 import os
3 import os.path
4 import logging
5 from werkzeug.utils import cached_property
6 from piecrust import CONTENT_DIR
7 from piecrust.configuration import ConfigurationError
8 from piecrust.page import Page
9
10
11 REALM_USER = 0
12 REALM_THEME = 1
13 REALM_NAMES = {
14 REALM_USER: 'User',
15 REALM_THEME: 'Theme'}
16
17
18 MODE_PARSING = 0
19 MODE_CREATING = 1
20
21
22 logger = logging.getLogger(__name__)
23
24
25 page_ref_pattern = re.compile(r'(?P<src>[\w]+)\:(?P<path>.*?)(;|$)')
26
27
28 class PageNotFoundError(Exception):
29 pass
30
31
32 class InvalidFileSystemEndpointError(Exception):
33 def __init__(self, source_name, fs_endpoint):
34 super(InvalidFileSystemEndpointError, self).__init__(
35 "Invalid file-system endpoint for source '%s': %s" %
36 (source_name, fs_endpoint))
37
38
39 class PageFactory(object):
40 """ A class responsible for creating a page.
41 """
42 def __init__(self, source, rel_path, metadata):
43 self.source = source
44 self.rel_path = rel_path
45 self.metadata = metadata
46
47 @property
48 def ref_spec(self):
49 return '%s:%s' % (self.source.name, self.rel_path)
50
51 @cached_property
52 def path(self):
53 return self.source.resolveRef(self.rel_path)
54
55 def buildPage(self):
56 repo = self.source.app.env.page_repository
57 if repo is not None:
58 cache_key = '%s:%s' % (self.source.name, self.rel_path)
59 return repo.get(cache_key, self._doBuildPage)
60 return self._doBuildPage()
61
62 def _doBuildPage(self):
63 logger.debug("Building page: %s" % self.path)
64 page = Page(self.source, self.metadata, self.rel_path)
65 # Load it right away, especially when using the page repository,
66 # because we'll be inside a critical scope.
67 page._load()
68 return page
69
70
71 class CachedPageFactory(object):
72 """ A `PageFactory` (in appearance) that already has a page built.
73 """
74 def __init__(self, page):
75 self._page = page
76
77 @property
78 def rel_path(self):
79 return self._page.rel_path
80
81 @property
82 def metadata(self):
83 return self._page.source_metadata
84
85 @property
86 def ref_spec(self):
87 return self._page.ref_spec
88
89 @property
90 def path(self):
91 return self._page.path
92
93 def buildPage(self):
94 return self._page
95
96
97 class PageRef(object):
98 """ A reference to a page, with support for looking a page in different
99 realms.
100 """
101 def __init__(self, app, page_ref):
102 self.app = app
103 self._page_ref = page_ref
104 self._paths = None
105 self._first_valid_path_index = -2
106 self._exts = app.config.get('site/auto_formats').keys()
107
108 @property
109 def exists(self):
110 try:
111 self._checkPaths()
112 return True
113 except PageNotFoundError:
114 return False
115
116 @property
117 def source_name(self):
118 self._checkPaths()
119 return self._paths[self._first_valid_path_index][0]
120
121 @property
122 def source(self):
123 return self.app.getSource(self.source_name)
124
125 @property
126 def rel_path(self):
127 self._checkPaths()
128 return self._paths[self._first_valid_path_index][1]
129
130 @property
131 def path(self):
132 self._checkPaths()
133 return self._paths[self._first_valid_path_index][2]
134
135 @property
136 def possible_rel_paths(self):
137 self._load()
138 return [p[1] for p in self._paths]
139
140 @property
141 def possible_paths(self):
142 self._load()
143 return [p[2] for p in self._paths]
144
145 def _load(self):
146 if self._paths is not None:
147 return
148
149 it = list(page_ref_pattern.finditer(self._page_ref))
150 if len(it) == 0:
151 raise Exception("Invalid page ref: %s" % self._page_ref)
152
153 self._paths = []
154 for m in it:
155 source_name = m.group('src')
156 source = self.app.getSource(source_name)
157 if source is None:
158 raise Exception("No such source: %s" % source_name)
159 rel_path = m.group('path')
160 path = source.resolveRef(rel_path)
161 if '%ext%' in rel_path:
162 for e in self._exts:
163 self._paths.append((source_name,
164 rel_path.replace('%ext%', e),
165 path.replace('%ext%', e)))
166 else:
167 self._paths.append((source_name, rel_path, path))
168
169 def _checkPaths(self):
170 if self._first_valid_path_index >= 0:
171 return
172 if self._first_valid_path_index == -1:
173 raise PageNotFoundError("No valid paths were found for page reference:" %
174 self._page_ref)
175
176 self._load()
177 for i, path_info in enumerate(self._paths):
178 if os.path.isfile(path_info[2]):
179 self._first_valid_path_index = i
180 break
181
182
183 class PageSource(object):
184 """ A source for pages, e.g. a directory with one file per page.
185 """
186 def __init__(self, app, name, config):
187 self.app = app
188 self.name = name
189 self.config = config
190 self._factories = None
191 self._provider_type = None
192
193 def __getattr__(self, name):
194 try:
195 return self.config[name]
196 except KeyError:
197 raise AttributeError()
198
199 @property
200 def is_theme_source(self):
201 return self.realm == REALM_THEME
202
203 @property
204 def root_dir(self):
205 if self.is_theme_source:
206 return self.app.theme_dir
207 return self.app.root_dir
208
209 def getPageFactories(self):
210 if self._factories is None:
211 self._factories = list(self.buildPageFactories())
212 return self._factories
213
214 def buildPageFactories(self):
215 raise NotImplementedError()
216
217 def resolveRef(self, ref_path):
218 raise NotImplementedError()
219
220 def findPagePath(self, metadata, mode):
221 raise NotImplementedError()
222
223 def buildDataProvider(self, page, user_data):
224 if self._provider_type is None:
225 cls = next((pt for pt in self.app.plugin_loader.getDataProviders()
226 if pt.PROVIDER_NAME == self.data_type),
227 None)
228 if cls is None:
229 raise ConfigurationError("Unknown data provider type: %s" %
230 self.data_type)
231 self._provider_type = cls
232
233 return self._provider_type(self, page, user_data)
234
235 def getTaxonomyPageRef(self, tax_name):
236 tax_pages = self.config.get('taxonomy_pages')
237 if tax_pages is None:
238 return None
239 return tax_pages.get(tax_name)
240
241
242 class IPreparingSource:
243 def setupPrepareParser(self, parser, app):
244 raise NotImplementedError()
245
246 def buildMetadata(self, args):
247 raise NotImplementedError()
248
249
250 class ArraySource(PageSource):
251 def __init__(self, app, inner_source, name='array', config=None):
252 super(ArraySource, self).__init__(app, name, config or {})
253 self.inner_source = inner_source
254
255 @property
256 def page_count(self):
257 return len(self.inner_source)
258
259 def getPageFactories(self):
260 for p in self.inner_source:
261 yield CachedPageFactory(p)
262
263
264 class SimplePageSource(PageSource):
265 def __init__(self, app, name, config):
266 super(SimplePageSource, self).__init__(app, name, config)
267 self.fs_endpoint = config.get('fs_endpoint', name)
268 self.fs_endpoint_path = os.path.join(self.root_dir, CONTENT_DIR, self.fs_endpoint)
269 self.supported_extensions = app.config.get('site/auto_formats').keys()
270
271 def buildPageFactories(self):
272 logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path)
273 if not os.path.isdir(self.fs_endpoint_path):
274 raise InvalidFileSystemEndpointError(self.name, self.fs_endpoint_path)
275
276 for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path):
277 rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path)
278 dirnames[:] = filter(self._filterPageDirname, dirnames)
279 for f in filter(self._filterPageFilename, filenames):
280 slug, ext = os.path.splitext(os.path.join(rel_dirpath, f))
281 if slug.startswith('./') or slug.startswith('.\\'):
282 slug = slug[2:]
283 if slug == '_index':
284 slug = ''
285 metadata = {'path': slug}
286 fac_path = f
287 if rel_dirpath != '.':
288 fac_path = os.path.join(rel_dirpath, f)
289 yield PageFactory(self, fac_path, metadata)
290
291 def resolveRef(self, ref_path):
292 return os.path.join(self.fs_endpoint_path, ref_path)
293
294 def findPagePath(self, metadata, mode):
295 uri_path = metadata['path']
296 if uri_path == '':
297 uri_path = '_index'
298 path = os.path.join(self.fs_endpoint_path, uri_path)
299 _, ext = os.path.splitext(path)
300
301 if mode == MODE_CREATING:
302 if ext == '':
303 return '%s.*' % path
304 return path, metadata
305
306 if ext == '':
307 paths_to_check = ['%s.%s' % (path, e)
308 for e in self.supported_extensions]
309 else:
310 paths_to_check = [path]
311 for path in paths_to_check:
312 if os.path.isfile(path):
313 return path, metadata
314
315 return None, None
316
317 def _filterPageDirname(self, d):
318 return not d.endswith('-assets')
319
320 def _filterPageFilename(self, f):
321 name, ext = os.path.splitext(f)
322 return (f[0] != '.' and
323 f[-1] != '~' and
324 ext.lstrip('.') in self.supported_extensions and
325 f not in ['Thumbs.db'])
326
327
328 class DefaultPageSource(SimplePageSource, IPreparingSource):
329 SOURCE_NAME = 'default'
330
331 def __init__(self, app, name, config):
332 super(DefaultPageSource, self).__init__(app, name, config)
333
334 def setupPrepareParser(self, parser, app):
335 parser.add_argument('uri', help='The URI for the new page.')
336
337 def buildMetadata(self, args):
338 return {'path': args.uri}
339