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