Mercurial > piecrust2
comparison piecrust/routing.py @ 787:f6f9a284a5f3
routing: Simplify how route functions are declared and handled.
* Site config now only has to declare the function name.
* Simply code for running route functions.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Thu, 01 Sep 2016 23:00:58 -0700 |
parents | 3f01f63b7247 |
children | 4cbe057a8b6a |
comparison
equal
deleted
inserted
replaced
786:97c1dc568810 | 787:f6f9a284a5f3 |
---|---|
7 | 7 |
8 | 8 |
9 logger = logging.getLogger(__name__) | 9 logger = logging.getLogger(__name__) |
10 | 10 |
11 | 11 |
12 route_re = re.compile(r'%((?P<qual>[\w\d]+):)?(?P<name>\w+)%') | 12 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<name>\w+)\\%') | 13 route_esc_re = re.compile(r'\\%((?P<qual>[\w\d]+)\\:)?(?P<var>\\\+)?(?P<name>\w+)\\%') |
14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<args>.*)\)\s*$') | |
15 template_func_arg_re = re.compile(r'((?P<qual>[\w\d]+):)?(?P<arg>\+?\w+)') | |
16 ugly_url_cleaner = re.compile(r'\.html$') | 14 ugly_url_cleaner = re.compile(r'\.html$') |
17 | 15 |
18 | 16 |
19 class RouteNotFoundError(Exception): | 17 class RouteNotFoundError(Exception): |
20 pass | 18 pass |
92 re.escape(uri_pattern_no_path)) + '$' | 90 re.escape(uri_pattern_no_path)) + '$' |
93 self.uri_re_no_path = re.compile(p) | 91 self.uri_re_no_path = re.compile(p) |
94 else: | 92 else: |
95 self.uri_re_no_path = None | 93 self.uri_re_no_path = None |
96 | 94 |
97 self.required_route_metadata = set() | 95 # Determine the parameters for the route function. |
96 self.func_name = self._validateFuncName(cfg.get('func')) | |
97 self.func_parameters = [] | |
98 self.func_has_variadic_parameter = False | |
99 variadic_param_idx = -1 | |
98 for m in route_re.finditer(self.uri_pattern): | 100 for m in route_re.finditer(self.uri_pattern): |
99 self.required_route_metadata.add(m.group('name')) | 101 name = m.group('name') |
100 | 102 if m.group('var'): |
101 self.template_func = None | 103 self.func_has_variadic_parameter = True |
102 self.template_func_name = None | 104 variadic_param_idx = len(self.func_parameters) |
103 self.template_func_args = [] | 105 |
104 self.template_func_vararg = None | 106 self.func_parameters.append(name) |
105 self._createTemplateFunc(cfg.get('func')) | 107 |
108 if (variadic_param_idx >= 0 and | |
109 variadic_param_idx != len(self.func_parameters) - 1): | |
110 raise Exception( | |
111 "Only the last route URL parameter can be variadic. " | |
112 "Got: %s" % self.uri_pattern) | |
106 | 113 |
107 @property | 114 @property |
108 def route_type(self): | 115 def route_type(self): |
109 if self.source_name: | 116 if self.source_name: |
110 return ROUTE_TYPE_SOURCE | 117 return ROUTE_TYPE_SOURCE |
140 return gen | 147 return gen |
141 raise Exception("Can't find generator '%s' for route '%s'." % ( | 148 raise Exception("Can't find generator '%s' for route '%s'." % ( |
142 self.generator_name, self.uri)) | 149 self.generator_name, self.uri)) |
143 | 150 |
144 def matchesMetadata(self, route_metadata): | 151 def matchesMetadata(self, route_metadata): |
145 return self.required_route_metadata.issubset(route_metadata.keys()) | 152 return set(self.func_parameters).issubset(route_metadata.keys()) |
146 | 153 |
147 def matchUri(self, uri, strict=False): | 154 def matchUri(self, uri, strict=False): |
148 if not uri.startswith(self.uri_root): | 155 if not uri.startswith(self.uri_root): |
149 raise Exception("The given URI is not absolute: %s" % uri) | 156 raise Exception("The given URI is not absolute: %s" % uri) |
150 uri = uri[len(self.uri_root):] | 157 uri = uri[len(self.uri_root):] |
169 # When matching URIs, if the URI is a match but is missing some | 176 # When matching URIs, if the URI is a match but is missing some |
170 # metadata, fill those up with empty strings. This can happen if, | 177 # metadata, fill those up with empty strings. This can happen if, |
171 # say, a route's pattern is `/foo/%slug%`, and we're matching an | 178 # say, a route's pattern is `/foo/%slug%`, and we're matching an |
172 # URL like `/foo`. | 179 # URL like `/foo`. |
173 matched_keys = set(route_metadata.keys()) | 180 matched_keys = set(route_metadata.keys()) |
174 missing_keys = self.required_route_metadata - matched_keys | 181 missing_keys = set(self.func_parameters) - matched_keys |
175 for k in missing_keys: | 182 for k in missing_keys: |
176 route_metadata[k] = '' | 183 route_metadata[k] = '' |
177 | 184 |
178 for k in route_metadata: | 185 for k in route_metadata: |
179 route_metadata[k] = self._coerceRouteParameter( | 186 route_metadata[k] = self._coerceRouteParameter( |
229 if self.show_debug_info: | 236 if self.show_debug_info: |
230 uri += '?!debug' | 237 uri += '?!debug' |
231 | 238 |
232 return uri | 239 return uri |
233 | 240 |
241 def execTemplateFunc(self, *args): | |
242 fixed_param_count = len(self.func_parameters) | |
243 if self.func_has_variadic_parameter: | |
244 fixed_param_count -= 1 | |
245 | |
246 if len(args) < fixed_param_count: | |
247 raise Exception( | |
248 "Route function '%s' expected %d arguments, " | |
249 "got %d: %s" % | |
250 (self.func_name, fixed_param_count, len(args), args)) | |
251 | |
252 if self.func_has_variadic_parameter: | |
253 coerced_args = list(args[:fixed_param_count]) | |
254 if len(args) > fixed_param_count: | |
255 var_arg = tuple(args[fixed_param_count:]) | |
256 coerced_args.append(var_arg) | |
257 else: | |
258 coerced_args = args | |
259 | |
260 metadata = {} | |
261 for arg_name, arg_val in zip(self.func_parameters, coerced_args): | |
262 metadata[arg_name] = self._coerceRouteParameter( | |
263 arg_name, arg_val) | |
264 | |
265 if self.is_generator_route: | |
266 self.generator.onRouteFunctionUsed(self, metadata) | |
267 | |
268 return self.getUri(metadata) | |
269 | |
234 def _uriFormatRepl(self, m): | 270 def _uriFormatRepl(self, m): |
235 qual = m.group('qual') | 271 qual = m.group('qual') |
236 name = m.group('name') | 272 name = m.group('name') |
237 if qual == 'int4': | 273 if qual == 'int4': |
238 return '%%(%s)04d' % name | 274 return '%%(%s)04d' % name |
241 return '%%(%s)s' % name | 277 return '%%(%s)s' % name |
242 | 278 |
243 def _uriPatternRepl(self, m): | 279 def _uriPatternRepl(self, m): |
244 name = m.group('name') | 280 name = m.group('name') |
245 qual = m.group('qual') | 281 qual = m.group('qual') |
246 if qual == 'path': | 282 if qual == 'path' or m.group('var'): |
247 return r'(?P<%s>[^\?]*)' % name | 283 return r'(?P<%s>[^\?]*)' % name |
248 elif qual == 'int4': | 284 elif qual == 'int4': |
249 return r'(?P<%s>\d{4})' % name | 285 return r'(?P<%s>\d{4})' % name |
250 elif qual == 'int2': | 286 elif qual == 'int2': |
251 return r'(?P<%s>\d{2})' % name | 287 return r'(?P<%s>\d{2})' % name |
267 return int(val) | 303 return int(val) |
268 except ValueError: | 304 except ValueError: |
269 raise Exception( | 305 raise Exception( |
270 "Expected route parameter '%s' to be of type " | 306 "Expected route parameter '%s' to be of type " |
271 "'%s', but was: %s" % | 307 "'%s', but was: %s" % |
272 (k, param_type, route_metadata[k])) | 308 (name, param_type, val)) |
273 if param_type == 'path': | 309 if param_type == 'path': |
274 return val | 310 return val |
275 raise Exception("Unknown route parameter type: %s" % param_type) | 311 raise Exception("Unknown route parameter type: %s" % param_type) |
276 | 312 |
277 def _createTemplateFunc(self, func_def): | 313 def _validateFuncName(self, name): |
278 if func_def is None: | 314 if not name: |
279 return | 315 return None |
280 | 316 i = name.find('(') |
281 m = template_func_re.match(func_def) | 317 if i >= 0: |
282 if m is None: | 318 name = name[:i] |
283 raise Exception("Template function definition for route '%s' " | 319 logger.warning( |
284 "has invalid syntax: %s" % | 320 "Route function names shouldn't contain the list of arguments " |
285 (self.uri_pattern, func_def)) | 321 "anymore -- just specify '%s'." % name) |
286 | 322 return name |
287 self.template_func_name = m.group('name') | |
288 self.template_func_args = [] | |
289 arg_list = m.group('args') | |
290 if arg_list: | |
291 self.template_func_args = [] | |
292 for m2 in template_func_arg_re.finditer(arg_list): | |
293 self.template_func_args.append(m2.group('arg')) | |
294 for i in range(len(self.template_func_args) - 1): | |
295 if self.template_func_args[i][0] == '+': | |
296 raise Exception("Only the last route parameter can be a " | |
297 "variable argument (prefixed with `+`)") | |
298 | |
299 if (self.template_func_args and | |
300 self.template_func_args[-1][0] == '+'): | |
301 self.template_func_vararg = self.template_func_args[-1][1:] | |
302 | |
303 def template_func(*args): | |
304 is_variable = (self.template_func_vararg is not None) | |
305 if not is_variable and len(args) != len(self.template_func_args): | |
306 raise Exception( | |
307 "Route function '%s' expected %d arguments, " | |
308 "got %d: %s" % | |
309 (func_def, len(self.template_func_args), | |
310 len(args), args)) | |
311 elif is_variable and len(args) < len(self.template_func_args): | |
312 raise Exception( | |
313 "Route function '%s' expected at least %d arguments, " | |
314 "got %d: %s" % | |
315 (func_def, len(self.template_func_args), | |
316 len(args), args)) | |
317 | |
318 metadata = {} | |
319 non_var_args = list(self.template_func_args) | |
320 if is_variable: | |
321 del non_var_args[-1] | |
322 | |
323 for arg_name, arg_val in zip(non_var_args, args): | |
324 metadata[arg_name] = self._coerceRouteParameter( | |
325 arg_name, arg_val) | |
326 | |
327 if is_variable: | |
328 metadata[self.template_func_vararg] = [] | |
329 for i in range(len(non_var_args), len(args)): | |
330 metadata[self.template_func_vararg].append(args[i]) | |
331 | |
332 if self.is_generator_route: | |
333 self.generator.onRouteFunctionUsed(self, metadata) | |
334 | |
335 return self.getUri(metadata) | |
336 | |
337 self.template_func = template_func | |
338 | 323 |
339 | 324 |
340 class CompositeRouteFunction(object): | 325 class CompositeRouteFunction(object): |
341 def __init__(self): | 326 def __init__(self): |
342 self._funcs = [] | 327 self._routes = [] |
343 self._arg_names = None | 328 self._arg_names = None |
344 | 329 |
345 def addFunc(self, route): | 330 def addFunc(self, route): |
346 if self._arg_names is None: | 331 if self._arg_names is None: |
347 self._arg_names = sorted(route.template_func_args) | 332 self._arg_names = list(route.func_parameters) |
348 | 333 |
349 if sorted(route.template_func_args) != self._arg_names: | 334 if route.func_parameters != self._arg_names: |
350 raise Exception("Cannot merge route function with arguments '%s' " | 335 raise Exception("Cannot merge route function with arguments '%s' " |
351 "with route function with arguments '%s'." % | 336 "with route function with arguments '%s'." % |
352 (route.template_func_args, self._arg_names)) | 337 (route.func_parameters, self._arg_names)) |
353 self._funcs.append((route, route.template_func)) | 338 self._routes.append(route) |
354 | 339 |
355 def __call__(self, *args, **kwargs): | 340 def __call__(self, *args, **kwargs): |
356 if len(self._funcs) == 1 or len(args) == len(self._arg_names): | 341 if len(self._routes) == 1 or len(args) == len(self._arg_names): |
357 f = self._funcs[0][1] | 342 return self._routes[0].execTemplateFunc(*args, **kwargs) |
358 return f(*args, **kwargs) | |
359 | 343 |
360 if len(args) == len(self._arg_names) + 1: | 344 if len(args) == len(self._arg_names) + 1: |
361 f_args = args[:-1] | 345 f_args = args[:-1] |
362 for r, f in self._funcs: | 346 for r in self._routes: |
363 if r.source_name == args[-1]: | 347 if r.source_name == args[-1]: |
364 return f(*f_args, **kwargs) | 348 return r.execTemplateFunc(*f_args, **kwargs) |
365 raise Exception("No such source: %s" % args[-1]) | 349 raise Exception("No such source: %s" % args[-1]) |
366 | 350 |
367 raise Exception("Incorrect number of arguments for route function. " | 351 raise Exception("Incorrect number of arguments for route function. " |
368 "Expected '%s', got '%s'" % (self._arg_names, args)) | 352 "Expected '%s', got '%s'" % (self._arg_names, args)) |
369 | 353 |