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