Mercurial > piecrust2
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 |