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