comparison piecrust/sources/autoconfig.py @ 239:f43f19975671

sources: Refactor `autoconfig` source, add `OrderedPageSource`. Also add unit tests.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 15 Feb 2015 22:48:42 -0800
parents 683baa977d97
children f130365568ff
comparison
equal deleted inserted replaced
238:4dce0e61b48c 239:f43f19975671
1 import re
1 import os 2 import os
2 import os.path 3 import os.path
4 import glob
3 import logging 5 import logging
6 from piecrust.configuration import ConfigurationError
7 from piecrust.data.iterators import SettingSortIterator
4 from piecrust.sources.base import ( 8 from piecrust.sources.base import (
5 SimplePageSource, IPreparingSource, SimplePaginationSourceMixin, 9 SimplePageSource, IPreparingSource, SimplePaginationSourceMixin,
6 PageNotFoundError, InvalidFileSystemEndpointError, 10 PageNotFoundError, InvalidFileSystemEndpointError,
7 PageFactory, MODE_CREATING, MODE_PARSING) 11 PageFactory, MODE_CREATING, MODE_PARSING)
8 12
9 13
10 logger = logging.getLogger(__name__) 14 logger = logging.getLogger(__name__)
11 15
12 16
13 class AutoConfigSource(SimplePageSource, 17 class AutoConfigSourceBase(SimplePageSource,
14 SimplePaginationSourceMixin): 18 SimplePaginationSourceMixin):
15 SOURCE_NAME = 'autoconfig' 19 """ Base class for page sources that automatically apply configuration
16 20 settings to their generated pages based on those pages' paths.
21 """
17 def __init__(self, app, name, config): 22 def __init__(self, app, name, config):
18 super(AutoConfigSource, self).__init__(app, name, config) 23 super(AutoConfigSourceBase, self).__init__(app, name, config)
19 self.setting_name = config.get('setting_name', name) 24 self.capture_mode = config.get('capture_mode', 'path')
20 self.collapse_single_values = config.get('collapse_single_values', False) 25 if self.capture_mode not in ['path', 'dirname', 'filename']:
21 self.only_single_values = config.get('only_single_values', False) 26 raise ConfigurationError("Capture mode in source '%s' must be "
27 "one of: path, dirname, filename" %
28 name)
22 29
23 def buildPageFactories(self): 30 def buildPageFactories(self):
24 if not os.path.isdir(self.fs_endpoint_path): 31 if not os.path.isdir(self.fs_endpoint_path):
25 raise InvalidFileSystemEndpointError(self.name, self.fs_endpoint_path) 32 raise InvalidFileSystemEndpointError(self.name,
33 self.fs_endpoint_path)
26 34
27 for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path): 35 for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path):
28 if not filenames: 36 if not filenames:
29 continue 37 continue
30 config = self._extractConfigFragment(dirpath) 38
39 rel_dirpath = os.path.relpath(dirpath, self.fs_endpoint_path)
40
41 # If `capture_mode` is `dirname`, we don't need to recompute it
42 # for each filename, so we do it here.
43 if self.capture_mode == 'dirname':
44 config = self.extractConfigFragment(rel_dirpath)
45
31 for f in filenames: 46 for f in filenames:
32 slug, ext = os.path.splitext(f) 47 if self.capture_mode == 'path':
33 path = os.path.join(dirpath, f) 48 path = os.path.join(rel_dirpath, f)
49 config = self.extractConfigFragment(path)
50 elif self.capture_mode == 'filename':
51 config = self.extractConfigFragment(f)
52
53 fac_path = f
54 if rel_dirpath != '.':
55 fac_path = os.path.join(rel_dirpath, f)
56
57 slug = self.makeSlug(rel_dirpath, f)
58
34 metadata = { 59 metadata = {
35 'slug': slug, 60 'slug': slug,
36 'config': config} 61 'config': config}
37 yield PageFactory(self, path, metadata) 62 yield PageFactory(self, fac_path, metadata)
38 63
39 def _extractConfigFragment(self, path): 64 def makeSlug(self, rel_dirpath, filename):
40 rel_path = os.path.relpath(path, self.fs_endpoint_path) 65 raise NotImplementedError()
66
67 def extractConfigFragment(self, rel_path):
68 raise NotImplementedError()
69
70 def findPagePath(self, metadata, mode):
71 raise NotImplementedError()
72
73
74 class AutoConfigSource(AutoConfigSourceBase):
75 """ Page source that extracts configuration settings from the sub-folders
76 each page resides in. This is ideal for setting tags or categories
77 on pages based on the folders they're in.
78 """
79 SOURCE_NAME = 'autoconfig'
80
81 def __init__(self, app, name, config):
82 config['capture_mode'] = 'dirname'
83 super(AutoConfigSource, self).__init__(app, name, config)
84 self.setting_name = config.get('setting_name', name)
85 self.only_single_values = config.get('only_single_values', False)
86 self.collapse_single_values = config.get('collapse_single_values',
87 False)
88 self.supported_extensions = list(
89 app.config.get('site/auto_formats').keys())
90
91 def makeSlug(self, rel_dirpath, filename):
92 slug, ext = os.path.splitext(filename)
93 if ext.lstrip('.') not in self.supported_extensions:
94 slug += ext
95 return slug
96
97 def extractConfigFragment(self, rel_path):
41 if rel_path == '.': 98 if rel_path == '.':
42 values = [] 99 values = []
43 else: 100 else:
44 values = rel_path.split(os.sep) 101 values = rel_path.split(os.sep)
45 if self.only_single_values and len(values) > 1: 102
46 raise Exception("Only one folder level is allowed for pages " 103 if self.only_single_values:
47 "in source '%s'." % self.name) 104 if len(values) > 1:
48 if self.collapse_single_values and len(values) == 1: 105 raise Exception("Only one folder level is allowed for pages "
49 values = values[0] 106 "in source '%s'." % self.name)
107 elif len(values) == 1:
108 values = values[0]
109 else:
110 values = None
111
112 if self.collapse_single_values:
113 if len(values) == 1:
114 values = values[0]
115 elif len(values) == 0:
116 values = None
117
50 return {self.setting_name: values} 118 return {self.setting_name: values}
51 119
52 def findPagePath(self, metadata, mode): 120 def findPagePath(self, metadata, mode):
121 # Pages from this source are effectively flattened, so we need to
122 # find pages using a brute-force kinda way.
53 for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path): 123 for dirpath, dirnames, filenames in os.walk(self.fs_endpoint_path):
54 for f in filenames: 124 for f in filenames:
55 slug, _ = os.path.splitext(f) 125 slug, _ = os.path.splitext(f)
56 if slug == metadata['slug']: 126 if slug == metadata['slug']:
57 path = os.path.join(dirpath, f) 127 path = os.path.join(dirpath, f)
58 rel_path = os.path.relpath(path, self.fs_endpoint_path) 128 rel_path = os.path.relpath(path, self.fs_endpoint_path)
59 config = self._extractConfigFragment(dirpath) 129 config = self.extractConfigFragment(dirpath)
60 metadata = {'slug': slug, 'config': config} 130 metadata = {'slug': slug, 'config': config}
61 return rel_path, metadata 131 return rel_path, metadata
62 132
133
134 class OrderedPageSource(AutoConfigSourceBase):
135 """ A page source that assigns an "order" to its pages based on a
136 numerical prefix in their filename. Page iterators will automatically
137 sort pages using that order.
138 """
139 SOURCE_NAME = 'ordered'
140
141 re_pattern = re.compile(r'(^|/)(?P<num>\d+)_')
142
143 def __init__(self, app, name, config):
144 config['capture_mode'] = 'filename'
145 super(OrderedPageSource, self).__init__(app, name, config)
146 self.setting_name = config.get('setting_name', 'order')
147 self.default_value = config.get('default_value', 0)
148 self.supported_extensions = list(
149 app.config.get('site/auto_formats').keys())
150
151 def makeSlug(self, rel_dirpath, filename):
152 slug, ext = os.path.splitext(filename)
153 if ext.lstrip('.') not in self.supported_extensions:
154 slug += ext
155 slug = self.re_pattern.sub(r'\1', slug)
156 slug = os.path.join(rel_dirpath, slug).replace('\\', '/')
157 if slug.startswith('./'):
158 slug = slug[2:]
159 return slug
160
161 def extractConfigFragment(self, rel_path):
162 m = self.re_pattern.match(rel_path)
163 if m is not None:
164 val = int(m.group('num'))
165 else:
166 val = self.default_value
167 return {self.setting_name: val}
168
169 def findPagePath(self, metadata, mode):
170 uri_path = metadata.get('slug', '')
171 if uri_path != '':
172 uri_parts = ['*_%s' % p for p in uri_path.split('/')]
173 else:
174 uri_parts = ['*__index']
175 uri_parts.insert(0, self.fs_endpoint_path)
176 path = os.path.join(*uri_parts)
177
178 _, ext = os.path.splitext(uri_path)
179 if ext == '':
180 path += '.*'
181
182 possibles = glob.glob(path)
183
184 if len(possibles) == 0:
185 return None, None
186
187 if len(possibles) > 1:
188 raise Exception("More than one path matching: %s" % uri_path)
189
190 path = possibles[0]
191 fac_path = os.path.relpath(path, self.fs_endpoint_path)
192
193 _, filename = os.path.split(path)
194 config = self.extractConfigFragment(filename)
195 metadata = {'slug': uri_path, 'config': config}
196
197 return fac_path, metadata
198
199 def getSorterIterator(self, it):
200 accessor = self.getSettingAccessor()
201 return SettingSortIterator(it, self.setting_name,
202 value_accessor=accessor)
203
204 def _populateMetadata(self, rel_path, metadata, mode=None):
205 _, filename = os.path.split(rel_path)
206 config = self.extractConfigFragment(filename)
207 metadata['config'] = config
208 slug = metadata['slug']
209 metadata['slug'] = self.re_pattern.sub(r'\1', slug)
210