comparison piecrust/sources/base.py @ 242:f130365568ff

internal: Code reorganization to put less stuff in `sources.base`. Interfaces that sources can implement are in `sources.interfaces`. The default page source is in `sources.default`. The `SimplePageSource` is gone since most subclasses only wanted to do *some* stuff the same, but *lots* of stuff slightly different. I may have to revisit the code to extract exactly the code that's in common.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 18 Feb 2015 18:35:03 -0800
parents 4dce0e61b48c
children 88bffd469b04
comparison
equal deleted inserted replaced
241:85a6c7ba5e3b 242:f130365568ff
1 import re
2 import os
3 import os.path
4 import logging 1 import logging
5 from werkzeug.utils import cached_property 2 from werkzeug.utils import cached_property
6 from piecrust.configuration import ConfigurationError 3 from piecrust.configuration import ConfigurationError
7 from piecrust.data.base import IPaginationSource, PaginationData
8 from piecrust.data.filters import PaginationFilter
9 from piecrust.page import Page 4 from piecrust.page import Page
10 5
11 6
12 REALM_USER = 0 7 REALM_USER = 0
13 REALM_THEME = 1 8 REALM_THEME = 1
21 16
22 17
23 logger = logging.getLogger(__name__) 18 logger = logging.getLogger(__name__)
24 19
25 20
26 page_ref_pattern = re.compile(r'(?P<src>[\w]+)\:(?P<path>.*?)(;|$)')
27
28
29 def build_pages(app, factories): 21 def build_pages(app, factories):
30 with app.env.page_repository.startBatchGet(): 22 with app.env.page_repository.startBatchGet():
31 for f in factories: 23 for f in factories:
32 yield f.buildPage() 24 yield f.buildPage()
33
34
35 class PageNotFoundError(Exception):
36 pass
37 25
38 26
39 class InvalidFileSystemEndpointError(Exception): 27 class InvalidFileSystemEndpointError(Exception):
40 def __init__(self, source_name, fs_endpoint): 28 def __init__(self, source_name, fs_endpoint):
41 super(InvalidFileSystemEndpointError, self).__init__( 29 super(InvalidFileSystemEndpointError, self).__init__(
71 page = Page(self.source, self.metadata, self.rel_path) 59 page = Page(self.source, self.metadata, self.rel_path)
72 # Load it right away, especially when using the page repository, 60 # Load it right away, especially when using the page repository,
73 # because we'll be inside a critical scope. 61 # because we'll be inside a critical scope.
74 page._load() 62 page._load()
75 return page 63 return page
76
77
78 class CachedPageFactory(object):
79 """ A `PageFactory` (in appearance) that already has a page built.
80 """
81 def __init__(self, page):
82 self._page = page
83
84 @property
85 def rel_path(self):
86 return self._page.rel_path
87
88 @property
89 def metadata(self):
90 return self._page.source_metadata
91
92 @property
93 def ref_spec(self):
94 return self._page.ref_spec
95
96 @property
97 def path(self):
98 return self._page.path
99
100 def buildPage(self):
101 return self._page
102
103
104 class PageRef(object):
105 """ A reference to a page, with support for looking a page in different
106 realms.
107 """
108 def __init__(self, app, page_ref):
109 self.app = app
110 self._page_ref = page_ref
111 self._paths = None
112 self._first_valid_path_index = -2
113 self._exts = list(app.config.get('site/auto_formats').keys())
114
115 @property
116 def exists(self):
117 try:
118 self._checkPaths()
119 return True
120 except PageNotFoundError:
121 return False
122
123 @property
124 def source_name(self):
125 self._checkPaths()
126 return self._paths[self._first_valid_path_index][0]
127
128 @property
129 def source(self):
130 return self.app.getSource(self.source_name)
131
132 @property
133 def rel_path(self):
134 self._checkPaths()
135 return self._paths[self._first_valid_path_index][1]
136
137 @property
138 def path(self):
139 self._checkPaths()
140 return self._paths[self._first_valid_path_index][2]
141
142 @property
143 def possible_rel_paths(self):
144 self._load()
145 return [p[1] for p in self._paths]
146
147 @property
148 def possible_paths(self):
149 self._load()
150 return [p[2] for p in self._paths]
151
152 def _load(self):
153 if self._paths is not None:
154 return
155
156 it = list(page_ref_pattern.finditer(self._page_ref))
157 if len(it) == 0:
158 raise Exception("Invalid page ref: %s" % self._page_ref)
159
160 self._paths = []
161 for m in it:
162 source_name = m.group('src')
163 source = self.app.getSource(source_name)
164 if source is None:
165 raise Exception("No such source: %s" % source_name)
166 rel_path = m.group('path')
167 path = source.resolveRef(rel_path)
168 if '%ext%' in rel_path:
169 for e in self._exts:
170 self._paths.append((source_name,
171 rel_path.replace('%ext%', e),
172 path.replace('%ext%', e)))
173 else:
174 self._paths.append((source_name, rel_path, path))
175
176 def _checkPaths(self):
177 if self._first_valid_path_index >= 0:
178 return
179 if self._first_valid_path_index == -1:
180 raise PageNotFoundError(
181 "No valid paths were found for page reference: %s" %
182 self._page_ref)
183
184 self._load()
185 self._first_valid_path_index = -1
186 for i, path_info in enumerate(self._paths):
187 if os.path.isfile(path_info[2]):
188 self._first_valid_path_index = i
189 break
190 64
191 65
192 class PageSource(object): 66 class PageSource(object):
193 """ A source for pages, e.g. a directory with one file per page. 67 """ A source for pages, e.g. a directory with one file per page.
194 """ 68 """
233 raise NotImplementedError() 107 raise NotImplementedError()
234 108
235 def buildDataProvider(self, page, user_data): 109 def buildDataProvider(self, page, user_data):
236 if self._provider_type is None: 110 if self._provider_type is None:
237 cls = next((pt for pt in self.app.plugin_loader.getDataProviders() 111 cls = next((pt for pt in self.app.plugin_loader.getDataProviders()
238 if pt.PROVIDER_NAME == self.data_type), 112 if pt.PROVIDER_NAME == self.data_type),
239 None) 113 None)
240 if cls is None: 114 if cls is None:
241 raise ConfigurationError("Unknown data provider type: %s" % 115 raise ConfigurationError(
242 self.data_type) 116 "Unknown data provider type: %s" % self.data_type)
243 self._provider_type = cls 117 self._provider_type = cls
244 118
245 return self._provider_type(self, page, user_data) 119 return self._provider_type(self, page, user_data)
246 120
247 def getTaxonomyPageRef(self, tax_name): 121 def getTaxonomyPageRef(self, tax_name):
248 tax_pages = self.config.get('taxonomy_pages') 122 tax_pages = self.config.get('taxonomy_pages')
249 if tax_pages is None: 123 if tax_pages is None:
250 return None 124 return None
251 return tax_pages.get(tax_name) 125 return tax_pages.get(tax_name)
252 126
253
254 class IPreparingSource:
255 def setupPrepareParser(self, parser, app):
256 raise NotImplementedError()
257
258 def buildMetadata(self, args):
259 raise NotImplementedError()
260
261
262 class IListableSource:
263 def listPath(self, rel_path):
264 raise NotImplementedError()
265
266 def getDirpath(self, rel_path):
267 raise NotImplementedError()
268
269 def getBasename(self, rel_path):
270 raise NotImplementedError()
271
272
273 class SimplePaginationSourceMixin(IPaginationSource):
274 def getItemsPerPage(self):
275 return self.config['items_per_page']
276
277 def getSourceIterator(self):
278 return SourceFactoryIterator(self)
279
280 def getSorterIterator(self, it):
281 return DateSortIterator(it)
282
283 def getTailIterator(self, it):
284 return PaginationDataBuilderIterator(it)
285
286 def getPaginationFilter(self, page):
287 conf = (page.config.get('items_filters') or
288 page.app.config.get('site/items_filters'))
289 if conf == 'none' or conf == 'nil' or conf == '':
290 conf = None
291 if conf is not None:
292 f = PaginationFilter()
293 f.addClausesFromConfig(conf)
294 return f
295 return None
296
297 def getSettingAccessor(self):
298 return lambda i, n: i.config.get(n)
299
300
301 class ArraySource(PageSource, SimplePaginationSourceMixin):
302 def __init__(self, app, inner_source, name='array', config=None):
303 super(ArraySource, self).__init__(app, name, config or {})
304 self.inner_source = inner_source
305
306 @property
307 def page_count(self):
308 return len(self.inner_source)
309
310 def getPageFactories(self):
311 for p in self.inner_source:
312 yield CachedPageFactory(p)
313
314
315 class SimplePageSource(PageSource, IListableSource, IPreparingSource,
316 SimplePaginationSourceMixin):
317 def __init__(self, app, name, config):
318 super(SimplePageSource, self).__init__(app, name, config)
319 self.fs_endpoint = config.get('fs_endpoint', name)
320 self.fs_endpoint_path = os.path.join(self.root_dir, self.fs_endpoint)
321 self.supported_extensions = list(app.config.get('site/auto_formats').keys())
322 self.default_auto_format = app.config.get('site/default_auto_format')
323
324 def buildPageFactories(self):
325 logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path)
326 if not os.path.isdir(self.fs_endpoint_path):
327 if self.ignore_missing_dir:
328 return
329 raise InvalidFileSystemEndpointError(self.name, self.fs_endpoint_path)
330
331 for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path):
332 rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path)
333 dirnames[:] = list(filter(self._filterPageDirname, dirnames))
334 for f in filter(self._filterPageFilename, filenames):
335 fac_path = f
336 if rel_dirpath != '.':
337 fac_path = os.path.join(rel_dirpath, f)
338 slug = self._makeSlug(fac_path)
339 metadata = {'slug': slug}
340 fac_path = fac_path.replace('\\', '/')
341 self._populateMetadata(fac_path, metadata)
342 yield PageFactory(self, fac_path, metadata)
343
344 def resolveRef(self, ref_path):
345 return os.path.normpath(
346 os.path.join(self.fs_endpoint_path, ref_path.lstrip("\\/")))
347
348 def findPagePath(self, metadata, mode):
349 uri_path = metadata.get('slug', '')
350 if not uri_path:
351 uri_path = '_index'
352 path = os.path.join(self.fs_endpoint_path, uri_path)
353 _, ext = os.path.splitext(path)
354
355 if mode == MODE_CREATING:
356 if ext == '':
357 path = '%s.%s' % (path, self.default_auto_format)
358 rel_path = os.path.relpath(path, self.fs_endpoint_path)
359 rel_path = rel_path.replace('\\', '/')
360 self._populateMetadata(rel_path, metadata, mode)
361 return rel_path, metadata
362
363 if ext == '':
364 paths_to_check = [
365 '%s.%s' % (path, e)
366 for e in self.supported_extensions]
367 else:
368 paths_to_check = [path]
369 for path in paths_to_check:
370 if os.path.isfile(path):
371 rel_path = os.path.relpath(path, self.fs_endpoint_path)
372 rel_path = rel_path.replace('\\', '/')
373 self._populateMetadata(rel_path, metadata, mode)
374 return rel_path, metadata
375
376 return None, None
377
378 def listPath(self, rel_path):
379 rel_path = rel_path.lstrip('\\/')
380 path = os.path.join(self.fs_endpoint_path, rel_path)
381 names = sorted(os.listdir(path))
382 items = []
383 for name in names:
384 if os.path.isdir(os.path.join(path, name)):
385 if self._filterPageDirname(name):
386 rel_subdir = os.path.join(rel_path, name)
387 items.append((True, name, rel_subdir))
388 else:
389 if self._filterPageFilename(name):
390 slug = self._makeSlug(os.path.join(rel_path, name))
391 metadata = {'slug': slug}
392
393 fac_path = name
394 if rel_path != '.':
395 fac_path = os.path.join(rel_path, name)
396 fac_path = fac_path.replace('\\', '/')
397
398 self._populateMetadata(fac_path, metadata)
399 fac = PageFactory(self, fac_path, metadata)
400
401 name, _ = os.path.splitext(name)
402 items.append((False, name, fac))
403 return items
404
405 def getDirpath(self, rel_path):
406 return os.path.dirname(rel_path)
407
408 def getBasename(self, rel_path):
409 filename = os.path.basename(rel_path)
410 name, _ = os.path.splitext(filename)
411 return name
412
413 def setupPrepareParser(self, parser, app):
414 parser.add_argument('uri', help='The URI for the new page.')
415
416 def buildMetadata(self, args):
417 return {'slug': args.uri}
418
419 def _makeSlug(self, rel_path):
420 slug, ext = os.path.splitext(rel_path)
421 slug = slug.replace('\\', '/')
422 if ext.lstrip('.') not in self.supported_extensions:
423 slug += ext
424 if slug.startswith('./'):
425 slug = slug[2:]
426 if slug == '_index':
427 slug = ''
428 return slug
429
430 def _filterPageDirname(self, d):
431 return not d.endswith('-assets')
432
433 def _filterPageFilename(self, f):
434 return (f[0] != '.' and # .DS_store and other crap
435 f[-1] != '~' and # Vim temp files and what-not
436 f not in ['Thumbs.db']) # Windows bullshit
437
438 def _populateMetadata(self, rel_path, metadata, mode=None):
439 pass
440
441
442 class DefaultPageSource(SimplePageSource):
443 SOURCE_NAME = 'default'
444
445 def __init__(self, app, name, config):
446 super(DefaultPageSource, self).__init__(app, name, config)
447
448
449 class SourceFactoryIterator(object):
450 def __init__(self, source):
451 self.source = source
452 self.it = None # This is to permit recursive traversal of the
453 # iterator chain. It acts as the end.
454
455 def __iter__(self):
456 return self.source.getPages()
457
458
459 class DateSortIterator(object):
460 def __init__(self, it, reverse=True):
461 self.it = it
462 self.reverse = reverse
463
464 def __iter__(self):
465 return iter(sorted(self.it,
466 key=lambda x: x.datetime, reverse=self.reverse))
467
468
469 class PaginationDataBuilderIterator(object):
470 def __init__(self, it):
471 self.it = it
472
473 def __iter__(self):
474 for page in self.it:
475 if page is None:
476 yield None
477 else:
478 yield PaginationData(page)
479