Mercurial > piecrust2
comparison piecrust/routing.py @ 852:4850f8c21b6e
core: Start of the big refactor for PieCrust 3.0.
* Everything is a `ContentSource`, including assets directories.
* Most content sources are subclasses of the base file-system source.
* A source is processed by a "pipeline", and there are 2 built-in pipelines,
one for assets and one for pages. The asset pipeline is vaguely functional,
but the page pipeline is completely broken right now.
* Rewrite the baking process as just running appropriate pipelines on each
content item. This should allow for better parallelization.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 17 May 2017 00:11:48 -0700 |
parents | 58ebf50235a5 |
children | 08e02c2a2a1a |
comparison
equal
deleted
inserted
replaced
851:2c7e57d80bba | 852:4850f8c21b6e |
---|---|
1 import re | 1 import re |
2 import os.path | 2 import os.path |
3 import copy | |
4 import logging | 3 import logging |
5 import urllib.parse | 4 import urllib.parse |
6 from werkzeug.utils import cached_property | 5 from werkzeug.utils import cached_property |
7 | 6 |
8 | 7 |
9 logger = logging.getLogger(__name__) | 8 logger = logging.getLogger(__name__) |
10 | 9 |
11 | 10 |
12 route_re = re.compile(r'%((?P<qual>[\w\d]+):)?(?P<var>\+)?(?P<name>\w+)%') | 11 route_re = re.compile(r'%((?P<qual>[\w\d]+):)?(?P<var>\+)?(?P<name>\w+)%') |
13 route_esc_re = re.compile(r'\\%((?P<qual>[\w\d]+)\\:)?(?P<var>\\\+)?(?P<name>\w+)\\%') | 12 route_esc_re = re.compile( |
13 r'\\%((?P<qual>[\w\d]+)\\:)?(?P<var>\\\+)?(?P<name>\w+)\\%') | |
14 ugly_url_cleaner = re.compile(r'\.html$') | 14 ugly_url_cleaner = re.compile(r'\.html$') |
15 | 15 |
16 | 16 |
17 class RouteNotFoundError(Exception): | 17 class RouteNotFoundError(Exception): |
18 pass | 18 pass |
19 | 19 |
20 | 20 |
21 class InvalidRouteError(Exception): | 21 class InvalidRouteError(Exception): |
22 pass | 22 pass |
23 | |
24 | |
25 def create_route_metadata(page): | |
26 route_metadata = copy.deepcopy(page.source_metadata) | |
27 return route_metadata | |
28 | |
29 | |
30 ROUTE_TYPE_SOURCE = 0 | |
31 ROUTE_TYPE_GENERATOR = 1 | |
32 | 23 |
33 | 24 |
34 class RouteParameter(object): | 25 class RouteParameter(object): |
35 TYPE_STRING = 0 | 26 TYPE_STRING = 0 |
36 TYPE_PATH = 1 | 27 TYPE_PATH = 1 |
44 | 35 |
45 | 36 |
46 class Route(object): | 37 class Route(object): |
47 """ Information about a route for a PieCrust application. | 38 """ Information about a route for a PieCrust application. |
48 Each route defines the "shape" of an URL and how it maps to | 39 Each route defines the "shape" of an URL and how it maps to |
49 sources and generators. | 40 content sources. |
50 """ | 41 """ |
51 def __init__(self, app, cfg): | 42 def __init__(self, app, cfg): |
52 self.app = app | 43 self.app = app |
53 | 44 |
54 self.source_name = cfg.get('source') | 45 self.source_name = cfg['source'] |
55 self.generator_name = cfg.get('generator') | |
56 if not self.source_name and not self.generator_name: | |
57 raise InvalidRouteError( | |
58 "Both `source` and `generator` are specified.") | |
59 | |
60 self.uri_pattern = cfg['url'].lstrip('/') | 46 self.uri_pattern = cfg['url'].lstrip('/') |
61 | 47 |
62 if self.is_source_route: | 48 self.supported_params = self.source.getSupportedRouteParameters() |
63 self.supported_params = self.source.getSupportedRouteParameters() | |
64 else: | |
65 self.supported_params = self.generator.getSupportedRouteParameters() | |
66 | 49 |
67 self.pretty_urls = app.config.get('site/pretty_urls') | 50 self.pretty_urls = app.config.get('site/pretty_urls') |
68 self.trailing_slash = app.config.get('site/trailing_slash') | 51 self.trailing_slash = app.config.get('site/trailing_slash') |
69 self.show_debug_info = app.config.get('site/show_debug_info') | 52 self.show_debug_info = app.config.get('site/show_debug_info') |
70 self.pagination_suffix_format = app.config.get( | 53 self.pagination_suffix_format = app.config.get( |
71 '__cache/pagination_suffix_format') | 54 '__cache/pagination_suffix_format') |
72 self.uri_root = app.config.get('site/root') | 55 self.uri_root = app.config.get('site/root') |
73 | 56 |
74 self.uri_params = [] | 57 self.uri_params = [] |
75 self.uri_format = route_re.sub(self._uriFormatRepl, self.uri_pattern) | 58 self.uri_format = route_re.sub(self._uriFormatRepl, self.uri_pattern) |
76 | 59 |
85 # lack of a trailing slash). We have to build a special pattern (in | 68 # lack of a trailing slash). We have to build a special pattern (in |
86 # this case without that trailing slash) to match those situations. | 69 # this case without that trailing slash) to match those situations. |
87 # (maybe there's a better way to do it but I can't think of any | 70 # (maybe there's a better way to do it but I can't think of any |
88 # right now) | 71 # right now) |
89 uri_pattern_no_path = ( | 72 uri_pattern_no_path = ( |
90 route_re.sub(self._uriNoPathRepl, self.uri_pattern) | 73 route_re.sub(self._uriNoPathRepl, self.uri_pattern) |
91 .replace('//', '/') | 74 .replace('//', '/') |
92 .rstrip('/')) | 75 .rstrip('/')) |
93 if uri_pattern_no_path != self.uri_pattern: | 76 if uri_pattern_no_path != self.uri_pattern: |
94 p = route_esc_re.sub(self._uriPatternRepl, | 77 p = route_esc_re.sub(self._uriPatternRepl, |
95 re.escape(uri_pattern_no_path)) + '$' | 78 re.escape(uri_pattern_no_path)) + '$' |
96 self.uri_re_no_path = re.compile(p) | 79 self.uri_re_no_path = re.compile(p) |
97 else: | 80 else: |
107 "Got: %s" % self.uri_pattern) | 90 "Got: %s" % self.uri_pattern) |
108 if len(self.uri_params) > 0: | 91 if len(self.uri_params) > 0: |
109 last_param = self.getParameter(self.uri_params[-1]) | 92 last_param = self.getParameter(self.uri_params[-1]) |
110 self.func_has_variadic_parameter = last_param.variadic | 93 self.func_has_variadic_parameter = last_param.variadic |
111 | 94 |
112 @property | |
113 def route_type(self): | |
114 if self.source_name: | |
115 return ROUTE_TYPE_SOURCE | |
116 elif self.generator_name: | |
117 return ROUTE_TYPE_GENERATOR | |
118 else: | |
119 raise InvalidRouteError() | |
120 | |
121 @property | |
122 def is_source_route(self): | |
123 return self.route_type == ROUTE_TYPE_SOURCE | |
124 | |
125 @property | |
126 def is_generator_route(self): | |
127 return self.route_type == ROUTE_TYPE_GENERATOR | |
128 | |
129 @cached_property | 95 @cached_property |
130 def source(self): | 96 def source(self): |
131 if not self.is_source_route: | |
132 return InvalidRouteError("This is not a source route.") | |
133 for src in self.app.sources: | 97 for src in self.app.sources: |
134 if src.name == self.source_name: | 98 if src.name == self.source_name: |
135 return src | 99 return src |
136 raise Exception("Can't find source '%s' for route '%s'." % ( | 100 raise Exception( |
101 "Can't find source '%s' for route '%s'." % ( | |
137 self.source_name, self.uri_pattern)) | 102 self.source_name, self.uri_pattern)) |
138 | |
139 @cached_property | |
140 def generator(self): | |
141 if not self.is_generator_route: | |
142 return InvalidRouteError("This is not a generator route.") | |
143 for gen in self.app.generators: | |
144 if gen.name == self.generator_name: | |
145 return gen | |
146 raise Exception("Can't find generator '%s' for route '%s'." % ( | |
147 self.generator_name, self.uri_pattern)) | |
148 | 103 |
149 def hasParameter(self, name): | 104 def hasParameter(self, name): |
150 return any(lambda p: p.param_name == name, self.supported_params) | 105 return any(lambda p: p.param_name == name, self.supported_params) |
151 | 106 |
152 def getParameter(self, name): | 107 def getParameter(self, name): |
157 (name, self.uri_pattern)) | 112 (name, self.uri_pattern)) |
158 | 113 |
159 def getParameterType(self, name): | 114 def getParameterType(self, name): |
160 return self.getParameter(name).param_type | 115 return self.getParameter(name).param_type |
161 | 116 |
162 def matchesMetadata(self, route_metadata): | 117 def matchesParameters(self, route_params): |
163 return set(self.uri_params).issubset(route_metadata.keys()) | 118 return set(self.uri_params).issubset(route_params.keys()) |
164 | 119 |
165 def matchUri(self, uri, strict=False): | 120 def matchUri(self, uri, strict=False): |
166 if not uri.startswith(self.uri_root): | 121 if not uri.startswith(self.uri_root): |
167 raise Exception("The given URI is not absolute: %s" % uri) | 122 raise Exception("The given URI is not absolute: %s" % uri) |
168 uri = uri[len(self.uri_root):] | 123 uri = uri[len(self.uri_root):] |
170 if not self.pretty_urls: | 125 if not self.pretty_urls: |
171 uri = ugly_url_cleaner.sub('', uri) | 126 uri = ugly_url_cleaner.sub('', uri) |
172 elif self.trailing_slash: | 127 elif self.trailing_slash: |
173 uri = uri.rstrip('/') | 128 uri = uri.rstrip('/') |
174 | 129 |
175 route_metadata = None | 130 route_params = None |
176 m = self.uri_re.match(uri) | 131 m = self.uri_re.match(uri) |
177 if m: | 132 if m: |
178 route_metadata = m.groupdict() | 133 route_params = m.groupdict() |
179 if self.uri_re_no_path: | 134 if self.uri_re_no_path: |
180 m = self.uri_re_no_path.match(uri) | 135 m = self.uri_re_no_path.match(uri) |
181 if m: | 136 if m: |
182 route_metadata = m.groupdict() | 137 route_params = m.groupdict() |
183 if route_metadata is None: | 138 if route_params is None: |
184 return None | 139 return None |
185 | 140 |
186 if not strict: | 141 if not strict: |
187 # When matching URIs, if the URI is a match but is missing some | 142 # When matching URIs, if the URI is a match but is missing some |
188 # metadata, fill those up with empty strings. This can happen if, | 143 # parameters, fill those up with empty strings. This can happen if, |
189 # say, a route's pattern is `/foo/%slug%`, and we're matching an | 144 # say, a route's pattern is `/foo/%slug%`, and we're matching an |
190 # URL like `/foo`. | 145 # URL like `/foo`. |
191 matched_keys = set(route_metadata.keys()) | 146 matched_keys = set(route_params.keys()) |
192 missing_keys = set(self.uri_params) - matched_keys | 147 missing_keys = set(self.uri_params) - matched_keys |
193 for k in missing_keys: | 148 for k in missing_keys: |
194 if self.getParameterType(k) != RouteParameter.TYPE_PATH: | 149 if self.getParameterType(k) != RouteParameter.TYPE_PATH: |
195 return None | 150 return None |
196 route_metadata[k] = '' | 151 route_params[k] = '' |
197 | 152 |
198 for k in route_metadata: | 153 for k in route_params: |
199 route_metadata[k] = self._coerceRouteParameter( | 154 route_params[k] = self._coerceRouteParameter( |
200 k, route_metadata[k]) | 155 k, route_params[k]) |
201 | 156 |
202 return route_metadata | 157 return route_params |
203 | 158 |
204 def getUri(self, route_metadata, *, sub_num=1): | 159 def getUri(self, route_params, *, sub_num=1): |
205 route_metadata = dict(route_metadata) | 160 route_params = dict(route_params) |
206 for k in route_metadata: | 161 for k in route_params: |
207 route_metadata[k] = self._coerceRouteParameter( | 162 route_params[k] = self._coerceRouteParameter( |
208 k, route_metadata[k]) | 163 k, route_params[k]) |
209 | 164 |
210 uri = self.uri_format % route_metadata | 165 uri = self.uri_format % route_params |
211 suffix = None | 166 suffix = None |
212 if sub_num > 1: | 167 if sub_num > 1: |
213 # Note that we know the pagination suffix starts with a slash. | 168 # Note that we know the pagination suffix starts with a slash. |
214 suffix = self.pagination_suffix_format % {'num': sub_num} | 169 suffix = self.pagination_suffix_format % {'num': sub_num} |
215 | 170 |
256 if self.func_has_variadic_parameter: | 211 if self.func_has_variadic_parameter: |
257 fixed_param_count -= 1 | 212 fixed_param_count -= 1 |
258 | 213 |
259 if len(args) < fixed_param_count: | 214 if len(args) < fixed_param_count: |
260 raise Exception( | 215 raise Exception( |
261 "Route function '%s' expected %d arguments, " | 216 "Route function '%s' expected %d arguments, " |
262 "got %d: %s" % | 217 "got %d: %s" % |
263 (self.func_name, fixed_param_count, len(args), args)) | 218 (self.func_name, fixed_param_count, len(args), args)) |
264 | 219 |
265 if self.func_has_variadic_parameter: | 220 if self.func_has_variadic_parameter: |
266 coerced_args = list(args[:fixed_param_count]) | 221 coerced_args = list(args[:fixed_param_count]) |
267 if len(args) > fixed_param_count: | 222 if len(args) > fixed_param_count: |
268 var_arg = tuple(args[fixed_param_count:]) | 223 var_arg = tuple(args[fixed_param_count:]) |
269 coerced_args.append(var_arg) | 224 coerced_args.append(var_arg) |
270 else: | 225 else: |
271 coerced_args = args | 226 coerced_args = args |
272 | 227 |
273 metadata = {} | 228 route_params = {} |
274 for arg_name, arg_val in zip(self.uri_params, coerced_args): | 229 for arg_name, arg_val in zip(self.uri_params, coerced_args): |
275 metadata[arg_name] = self._coerceRouteParameter( | 230 route_params[arg_name] = self._coerceRouteParameter( |
276 arg_name, arg_val) | 231 arg_name, arg_val) |
277 | 232 |
278 if self.is_generator_route: | 233 self.source.onRouteFunctionUsed(self, route_params) |
279 self.generator.onRouteFunctionUsed(self, metadata) | 234 |
280 | 235 return self.getUri(route_params) |
281 return self.getUri(metadata) | |
282 | 236 |
283 def _uriFormatRepl(self, m): | 237 def _uriFormatRepl(self, m): |
284 if m.group('qual') or m.group('var'): | 238 if m.group('qual') or m.group('var'): |
285 # Print a warning only if we're not in a worker process. | 239 # Print a warning only if we're not in a worker process. |
286 print_warning = not self.app.config.has('baker/worker_id') | 240 print_warning = not self.app.config.has('baker/worker_id') |
348 "Route function names shouldn't contain the list of arguments " | 302 "Route function names shouldn't contain the list of arguments " |
349 "anymore -- just specify '%s'." % name) | 303 "anymore -- just specify '%s'." % name) |
350 return name | 304 return name |
351 | 305 |
352 | 306 |
353 class CompositeRouteFunction(object): | 307 class RouteFunction: |
354 def __init__(self): | 308 def __init__(self, route): |
355 self._routes = [] | 309 self._route = route |
356 self._arg_names = None | |
357 | |
358 def addFunc(self, route): | |
359 if self._arg_names is None: | |
360 self._arg_names = list(route.uri_params) | |
361 | |
362 if route.uri_params != self._arg_names: | |
363 raise Exception("Cannot merge route function with arguments '%s' " | |
364 "with route function with arguments '%s'." % | |
365 (route.uri_params, self._arg_names)) | |
366 self._routes.append(route) | |
367 | 310 |
368 def __call__(self, *args, **kwargs): | 311 def __call__(self, *args, **kwargs): |
369 if len(self._routes) == 1 or len(args) == len(self._arg_names): | 312 return self._route.execTemplateFunc(*args, **kwargs) |
370 return self._routes[0].execTemplateFunc(*args, **kwargs) | |
371 | |
372 if len(args) == len(self._arg_names) + 1: | |
373 f_args = args[:-1] | |
374 for r in self._routes: | |
375 if r.source_name == args[-1]: | |
376 return r.execTemplateFunc(*f_args, **kwargs) | |
377 raise Exception("No such source: %s" % args[-1]) | |
378 | |
379 raise Exception("Incorrect number of arguments for route function. " | |
380 "Expected '%s', got '%s'" % (self._arg_names, args)) | |
381 |