Mercurial > piecrust2
comparison piecrust/baking/single.py @ 150:91dcbb5fe1e8
Split baking code in smaller files.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 30 Nov 2014 21:46:42 -0800 |
parents | |
children | 55087da9a72e |
comparison
equal
deleted
inserted
replaced
149:ea4a17831242 | 150:91dcbb5fe1e8 |
---|---|
1 import os.path | |
2 import shutil | |
3 import codecs | |
4 import logging | |
5 import urllib.error | |
6 import urllib.parse | |
7 import urllib.request | |
8 from piecrust.baking.records import FLAG_OVERRIDEN, FLAG_SOURCE_MODIFIED | |
9 from piecrust.data.filters import (PaginationFilter, HasFilterClause, | |
10 IsFilterClause, AndBooleanClause) | |
11 from piecrust.rendering import (PageRenderingContext, render_page, | |
12 PASS_FORMATTING, PASS_RENDERING) | |
13 from piecrust.sources.base import (PageFactory, | |
14 REALM_NAMES, REALM_USER, REALM_THEME) | |
15 | |
16 | |
17 logger = logging.getLogger(__name__) | |
18 | |
19 | |
20 class BakingError(Exception): | |
21 pass | |
22 | |
23 | |
24 class PageBaker(object): | |
25 def __init__(self, app, out_dir, force=False, record=None, | |
26 copy_assets=True): | |
27 self.app = app | |
28 self.out_dir = out_dir | |
29 self.force = force | |
30 self.record = record | |
31 self.copy_assets = copy_assets | |
32 self.site_root = app.config.get('site/root') | |
33 self.pretty_urls = app.config.get('site/pretty_urls') | |
34 self.pagination_suffix = app.config.get('site/pagination_suffix') | |
35 | |
36 def getOutputUri(self, uri, num): | |
37 suffix = self.pagination_suffix.replace('%num%', str(num)) | |
38 if self.pretty_urls: | |
39 # Output will be: | |
40 # - `uri/name` | |
41 # - `uri/name/2` | |
42 # - `uri/name.ext` | |
43 # - `uri/name.ext/2` | |
44 if num <= 1: | |
45 return uri | |
46 return uri + suffix | |
47 else: | |
48 # Output will be: | |
49 # - `uri/name.html` | |
50 # - `uri/name/2.html` | |
51 # - `uri/name.ext` | |
52 # - `uri/name/2.ext` | |
53 if uri == '/': | |
54 if num <= 1: | |
55 return '/' | |
56 return '/' + suffix.lstrip('/') | |
57 else: | |
58 if num <= 1: | |
59 return uri | |
60 #TODO: watch out for tags with dots in them. | |
61 base_uri, ext = os.path.splitext(uri) | |
62 return base_uri + suffix + ext | |
63 | |
64 def getOutputPath(self, uri): | |
65 bake_path = [self.out_dir] | |
66 decoded_uri = urllib.parse.unquote(uri.lstrip('/')) | |
67 if self.pretty_urls: | |
68 bake_path.append(decoded_uri) | |
69 bake_path.append('index.html') | |
70 else: | |
71 name, ext = os.path.splitext(decoded_uri) | |
72 if decoded_uri == '': | |
73 bake_path.append('index.html') | |
74 elif ext: | |
75 bake_path.append(decoded_uri) | |
76 else: | |
77 bake_path.append(decoded_uri + '.html') | |
78 | |
79 return os.path.normpath(os.path.join(*bake_path)) | |
80 | |
81 def bake(self, factory, route, record_entry, | |
82 taxonomy_name=None, taxonomy_term=None): | |
83 custom_data = None | |
84 pagination_filter = None | |
85 route_metadata = dict(factory.metadata) | |
86 if taxonomy_name and taxonomy_term: | |
87 # Must bake a taxonomy listing page... we'll have to add a | |
88 # pagination filter for only get matching posts, and the output | |
89 # URL will be a bit different. | |
90 tax = self.app.getTaxonomy(taxonomy_name) | |
91 pagination_filter = PaginationFilter() | |
92 if tax.is_multiple: | |
93 if isinstance(taxonomy_term, tuple): | |
94 abc = AndBooleanClause() | |
95 for t in taxonomy_term: | |
96 abc.addClause(HasFilterClause(taxonomy_name, t)) | |
97 pagination_filter.addClause(abc) | |
98 slugified_term = '/'.join(taxonomy_term) | |
99 else: | |
100 pagination_filter.addClause( | |
101 HasFilterClause(taxonomy_name, taxonomy_term)) | |
102 slugified_term = taxonomy_term | |
103 else: | |
104 pagination_filter.addClause( | |
105 IsFilterClause(taxonomy_name, taxonomy_term)) | |
106 slugified_term = taxonomy_term | |
107 custom_data = {tax.term_name: taxonomy_term} | |
108 route_metadata.update({tax.term_name: slugified_term}) | |
109 | |
110 # Generate the URL using the route. | |
111 page = factory.buildPage() | |
112 uri = route.getUri(route_metadata, page) | |
113 | |
114 override = self.record.getOverrideEntry(factory, uri) | |
115 if override is not None: | |
116 override_source = self.app.getSource(override.source_name) | |
117 if override_source.realm == factory.source.realm: | |
118 raise BakingError( | |
119 "Page '%s' maps to URL '%s' but is overriden by page" | |
120 "'%s:%s'." % (factory.ref_spec, uri, | |
121 override.source_name, override.rel_path)) | |
122 logger.debug("'%s' [%s] is overriden by '%s:%s'. Skipping" % | |
123 (factory.ref_spec, uri, override.source_name, | |
124 override.rel_path)) | |
125 record_entry.flags |= FLAG_OVERRIDEN | |
126 return | |
127 | |
128 cur_sub = 1 | |
129 has_more_subs = True | |
130 force_this = self.force | |
131 invalidate_formatting = False | |
132 record_entry.config = page.config.get().copy() | |
133 prev_record_entry = self.record.getPreviousEntry( | |
134 factory.source.name, factory.rel_path, | |
135 taxonomy_name, taxonomy_term) | |
136 | |
137 logger.debug("Baking '%s'..." % uri) | |
138 | |
139 # If the current page is known to use pages from other sources, | |
140 # see if any of those got baked, or are going to be baked for some | |
141 # reason. If so, we need to bake this one too. | |
142 # (this happens for instance with the main page of a blog). | |
143 if prev_record_entry and prev_record_entry.was_baked_successfully: | |
144 invalidated_render_passes = set() | |
145 used_src_names = list(prev_record_entry.used_source_names) | |
146 for src_name, rdr_pass in used_src_names: | |
147 entries = self.record.getCurrentEntries(src_name) | |
148 for e in entries: | |
149 if e.was_baked or e.flags & FLAG_SOURCE_MODIFIED: | |
150 invalidated_render_passes.add(rdr_pass) | |
151 break | |
152 if len(invalidated_render_passes) > 0: | |
153 logger.debug("'%s' is known to use sources %s, at least one " | |
154 "of which got baked. Will force bake this page. " | |
155 % (uri, used_src_names)) | |
156 force_this = True | |
157 if PASS_FORMATTING in invalidated_render_passes: | |
158 logger.debug("Will invalidate cached formatting for '%s' " | |
159 "since sources were using during that pass." | |
160 % uri) | |
161 invalidate_formatting = True | |
162 | |
163 while has_more_subs: | |
164 sub_uri = self.getOutputUri(uri, cur_sub) | |
165 out_path = self.getOutputPath(sub_uri) | |
166 | |
167 # Check for up-to-date outputs. | |
168 do_bake = True | |
169 if not force_this: | |
170 try: | |
171 in_path_time = record_entry.path_mtime | |
172 out_path_time = os.path.getmtime(out_path) | |
173 if out_path_time > in_path_time: | |
174 do_bake = False | |
175 except OSError: | |
176 # File doesn't exist, we'll need to bake. | |
177 pass | |
178 | |
179 # If this page didn't bake because it's already up-to-date. | |
180 # Keep trying for as many subs as we know this page has. | |
181 if not do_bake: | |
182 if (prev_record_entry is not None and | |
183 prev_record_entry.num_subs < cur_sub): | |
184 logger.debug("") | |
185 cur_sub += 1 | |
186 has_more_subs = True | |
187 logger.debug(" %s is up to date, skipping to next " | |
188 "sub-page." % out_path) | |
189 continue | |
190 | |
191 # We don't know how many subs to expect... just skip. | |
192 logger.debug(" %s is up to date, skipping bake." % out_path) | |
193 break | |
194 | |
195 # All good, proceed. | |
196 try: | |
197 if invalidate_formatting: | |
198 cache_key = '%s:%s' % (uri, cur_sub) | |
199 self.app.env.rendered_segments_repository.invalidate( | |
200 cache_key) | |
201 | |
202 logger.debug(" p%d -> %s" % (cur_sub, out_path)) | |
203 ctx, rp = self._bakeSingle(page, sub_uri, cur_sub, out_path, | |
204 pagination_filter, custom_data) | |
205 except Exception as ex: | |
206 if self.app.debug: | |
207 logger.exception(ex) | |
208 page_rel_path = os.path.relpath(page.path, self.app.root_dir) | |
209 raise BakingError("%s: error baking '%s'." % | |
210 (page_rel_path, uri)) from ex | |
211 | |
212 # Copy page assets. | |
213 if (cur_sub == 1 and self.copy_assets and | |
214 ctx.used_assets is not None): | |
215 if self.pretty_urls: | |
216 out_assets_dir = os.path.dirname(out_path) | |
217 else: | |
218 out_assets_dir, out_name = os.path.split(out_path) | |
219 if sub_uri != self.site_root: | |
220 out_name_noext, _ = os.path.splitext(out_name) | |
221 out_assets_dir += out_name_noext | |
222 | |
223 logger.debug("Copying page assets to: %s" % out_assets_dir) | |
224 if not os.path.isdir(out_assets_dir): | |
225 os.makedirs(out_assets_dir, 0o755) | |
226 for ap in ctx.used_assets: | |
227 dest_ap = os.path.join(out_assets_dir, os.path.basename(ap)) | |
228 logger.debug(" %s -> %s" % (ap, dest_ap)) | |
229 shutil.copy(ap, dest_ap) | |
230 | |
231 # Record what we did and figure out if we have more work. | |
232 record_entry.out_uris.append(sub_uri) | |
233 record_entry.out_paths.append(out_path) | |
234 record_entry.used_source_names |= ctx.used_source_names | |
235 record_entry.used_taxonomy_terms |= ctx.used_taxonomy_terms | |
236 | |
237 has_more_subs = False | |
238 if (ctx.used_pagination is not None and | |
239 ctx.used_pagination.has_more): | |
240 cur_sub += 1 | |
241 has_more_subs = True | |
242 | |
243 def _bakeSingle(self, page, sub_uri, num, out_path, | |
244 pagination_filter=None, custom_data=None): | |
245 ctx = PageRenderingContext(page, sub_uri) | |
246 ctx.page_num = num | |
247 if pagination_filter: | |
248 ctx.pagination_filter = pagination_filter | |
249 if custom_data: | |
250 ctx.custom_data = custom_data | |
251 | |
252 rp = render_page(ctx) | |
253 | |
254 out_dir = os.path.dirname(out_path) | |
255 if not os.path.isdir(out_dir): | |
256 os.makedirs(out_dir, 0o755) | |
257 | |
258 with codecs.open(out_path, 'w', 'utf8') as fp: | |
259 fp.write(rp.content) | |
260 | |
261 return ctx, rp | |
262 |