Mercurial > piecrust2
comparison piecrust/routing.py @ 792:58ebf50235a5
routing: Simplify how routes are defined.
* No more declaring the type of route parameters -- the sources and generators
already know what type each parameter is supposed to be.
* Same for variadic parameters -- we know already.
* Update cache version to force a clear reload of the config.
* Update tests.
TODO: simplify code in the `Route` class to use source or generator transparently.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 07 Sep 2016 08:58:41 -0700 |
parents | 504d6817352d |
children | 4850f8c21b6e |
comparison
equal
deleted
inserted
replaced
791:504d6817352d | 792:58ebf50235a5 |
---|---|
22 pass | 22 pass |
23 | 23 |
24 | 24 |
25 def create_route_metadata(page): | 25 def create_route_metadata(page): |
26 route_metadata = copy.deepcopy(page.source_metadata) | 26 route_metadata = copy.deepcopy(page.source_metadata) |
27 route_metadata.update(page.getRouteMetadata()) | |
28 return route_metadata | 27 return route_metadata |
29 | |
30 | |
31 class IRouteMetadataProvider(object): | |
32 def getRouteMetadata(self): | |
33 raise NotImplementedError() | |
34 | 28 |
35 | 29 |
36 ROUTE_TYPE_SOURCE = 0 | 30 ROUTE_TYPE_SOURCE = 0 |
37 ROUTE_TYPE_GENERATOR = 1 | 31 ROUTE_TYPE_GENERATOR = 1 |
32 | |
33 | |
34 class RouteParameter(object): | |
35 TYPE_STRING = 0 | |
36 TYPE_PATH = 1 | |
37 TYPE_INT2 = 2 | |
38 TYPE_INT4 = 3 | |
39 | |
40 def __init__(self, param_name, param_type=TYPE_STRING, *, variadic=False): | |
41 self.param_name = param_name | |
42 self.param_type = param_type | |
43 self.variadic = variadic | |
38 | 44 |
39 | 45 |
40 class Route(object): | 46 class Route(object): |
41 """ Information about a route for a PieCrust application. | 47 """ Information about a route for a PieCrust application. |
42 Each route defines the "shape" of an URL and how it maps to | 48 Each route defines the "shape" of an URL and how it maps to |
49 self.generator_name = cfg.get('generator') | 55 self.generator_name = cfg.get('generator') |
50 if not self.source_name and not self.generator_name: | 56 if not self.source_name and not self.generator_name: |
51 raise InvalidRouteError( | 57 raise InvalidRouteError( |
52 "Both `source` and `generator` are specified.") | 58 "Both `source` and `generator` are specified.") |
53 | 59 |
60 self.uri_pattern = cfg['url'].lstrip('/') | |
61 | |
62 if self.is_source_route: | |
63 self.supported_params = self.source.getSupportedRouteParameters() | |
64 else: | |
65 self.supported_params = self.generator.getSupportedRouteParameters() | |
66 | |
54 self.pretty_urls = app.config.get('site/pretty_urls') | 67 self.pretty_urls = app.config.get('site/pretty_urls') |
55 self.trailing_slash = app.config.get('site/trailing_slash') | 68 self.trailing_slash = app.config.get('site/trailing_slash') |
56 self.show_debug_info = app.config.get('site/show_debug_info') | 69 self.show_debug_info = app.config.get('site/show_debug_info') |
57 self.pagination_suffix_format = app.config.get( | 70 self.pagination_suffix_format = app.config.get( |
58 '__cache/pagination_suffix_format') | 71 '__cache/pagination_suffix_format') |
59 self.uri_root = app.config.get('site/root') | 72 self.uri_root = app.config.get('site/root') |
60 | 73 |
61 uri = cfg['url'] | 74 self.uri_params = [] |
62 self.uri_pattern = uri.lstrip('/') | |
63 self.uri_format = route_re.sub(self._uriFormatRepl, self.uri_pattern) | 75 self.uri_format = route_re.sub(self._uriFormatRepl, self.uri_pattern) |
64 | 76 |
65 # Get the straight-forward regex for matching this URI pattern. | 77 # Get the straight-forward regex for matching this URI pattern. |
66 p = route_esc_re.sub(self._uriPatternRepl, | 78 p = route_esc_re.sub(self._uriPatternRepl, |
67 re.escape(self.uri_pattern)) + '$' | 79 re.escape(self.uri_pattern)) + '$' |
83 re.escape(uri_pattern_no_path)) + '$' | 95 re.escape(uri_pattern_no_path)) + '$' |
84 self.uri_re_no_path = re.compile(p) | 96 self.uri_re_no_path = re.compile(p) |
85 else: | 97 else: |
86 self.uri_re_no_path = None | 98 self.uri_re_no_path = None |
87 | 99 |
88 # Determine the parameters for the route function. | |
89 self.func_name = self._validateFuncName(cfg.get('func')) | 100 self.func_name = self._validateFuncName(cfg.get('func')) |
90 self.func_parameters = [] | |
91 self.func_has_variadic_parameter = False | 101 self.func_has_variadic_parameter = False |
92 self.param_types = {} | 102 for p in self.uri_params[:-1]: |
93 variadic_param_idx = -1 | 103 param = self.getParameter(p) |
94 for m in route_re.finditer(self.uri_pattern): | 104 if param.variadic: |
95 name = m.group('name') | 105 raise Exception( |
96 self.func_parameters.append(name) | 106 "Only the last route URL parameter can be variadic. " |
97 | 107 "Got: %s" % self.uri_pattern) |
98 qual = m.group('qual') | 108 if len(self.uri_params) > 0: |
99 if not qual: | 109 last_param = self.getParameter(self.uri_params[-1]) |
100 qual = self._getBackwardCompatibleParamType(name) | 110 self.func_has_variadic_parameter = last_param.variadic |
101 if qual: | |
102 self.param_types[name] = qual | |
103 | |
104 if m.group('var'): | |
105 self.func_has_variadic_parameter = True | |
106 variadic_param_idx = len(self.func_parameters) - 1 | |
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) | |
113 | 111 |
114 @property | 112 @property |
115 def route_type(self): | 113 def route_type(self): |
116 if self.source_name: | 114 if self.source_name: |
117 return ROUTE_TYPE_SOURCE | 115 return ROUTE_TYPE_SOURCE |
134 return InvalidRouteError("This is not a source route.") | 132 return InvalidRouteError("This is not a source route.") |
135 for src in self.app.sources: | 133 for src in self.app.sources: |
136 if src.name == self.source_name: | 134 if src.name == self.source_name: |
137 return src | 135 return src |
138 raise Exception("Can't find source '%s' for route '%s'." % ( | 136 raise Exception("Can't find source '%s' for route '%s'." % ( |
139 self.source_name, self.uri)) | 137 self.source_name, self.uri_pattern)) |
140 | 138 |
141 @cached_property | 139 @cached_property |
142 def generator(self): | 140 def generator(self): |
143 if not self.is_generator_route: | 141 if not self.is_generator_route: |
144 return InvalidRouteError("This is not a generator route.") | 142 return InvalidRouteError("This is not a generator route.") |
145 for gen in self.app.generators: | 143 for gen in self.app.generators: |
146 if gen.name == self.generator_name: | 144 if gen.name == self.generator_name: |
147 return gen | 145 return gen |
148 raise Exception("Can't find generator '%s' for route '%s'." % ( | 146 raise Exception("Can't find generator '%s' for route '%s'." % ( |
149 self.generator_name, self.uri)) | 147 self.generator_name, self.uri_pattern)) |
148 | |
149 def hasParameter(self, name): | |
150 return any(lambda p: p.param_name == name, self.supported_params) | |
151 | |
152 def getParameter(self, name): | |
153 for p in self.supported_params: | |
154 if p.param_name == name: | |
155 return p | |
156 raise Exception("No such supported route parameter '%s' in: %s" % | |
157 (name, self.uri_pattern)) | |
158 | |
159 def getParameterType(self, name): | |
160 return self.getParameter(name).param_type | |
150 | 161 |
151 def matchesMetadata(self, route_metadata): | 162 def matchesMetadata(self, route_metadata): |
152 return set(self.func_parameters).issubset(route_metadata.keys()) | 163 return set(self.uri_params).issubset(route_metadata.keys()) |
153 | 164 |
154 def matchUri(self, uri, strict=False): | 165 def matchUri(self, uri, strict=False): |
155 if not uri.startswith(self.uri_root): | 166 if not uri.startswith(self.uri_root): |
156 raise Exception("The given URI is not absolute: %s" % uri) | 167 raise Exception("The given URI is not absolute: %s" % uri) |
157 uri = uri[len(self.uri_root):] | 168 uri = uri[len(self.uri_root):] |
176 # When matching URIs, if the URI is a match but is missing some | 187 # When matching URIs, if the URI is a match but is missing some |
177 # metadata, fill those up with empty strings. This can happen if, | 188 # metadata, fill those up with empty strings. This can happen if, |
178 # say, a route's pattern is `/foo/%slug%`, and we're matching an | 189 # say, a route's pattern is `/foo/%slug%`, and we're matching an |
179 # URL like `/foo`. | 190 # URL like `/foo`. |
180 matched_keys = set(route_metadata.keys()) | 191 matched_keys = set(route_metadata.keys()) |
181 missing_keys = set(self.func_parameters) - matched_keys | 192 missing_keys = set(self.uri_params) - matched_keys |
182 for k in missing_keys: | 193 for k in missing_keys: |
194 if self.getParameterType(k) != RouteParameter.TYPE_PATH: | |
195 return None | |
183 route_metadata[k] = '' | 196 route_metadata[k] = '' |
184 | 197 |
185 for k in route_metadata: | 198 for k in route_metadata: |
186 route_metadata[k] = self._coerceRouteParameter( | 199 route_metadata[k] = self._coerceRouteParameter( |
187 k, route_metadata[k]) | 200 k, route_metadata[k]) |
237 uri += '?!debug' | 250 uri += '?!debug' |
238 | 251 |
239 return uri | 252 return uri |
240 | 253 |
241 def execTemplateFunc(self, *args): | 254 def execTemplateFunc(self, *args): |
242 fixed_param_count = len(self.func_parameters) | 255 fixed_param_count = len(self.uri_params) |
243 if self.func_has_variadic_parameter: | 256 if self.func_has_variadic_parameter: |
244 fixed_param_count -= 1 | 257 fixed_param_count -= 1 |
245 | 258 |
246 if len(args) < fixed_param_count: | 259 if len(args) < fixed_param_count: |
247 raise Exception( | 260 raise Exception( |
256 coerced_args.append(var_arg) | 269 coerced_args.append(var_arg) |
257 else: | 270 else: |
258 coerced_args = args | 271 coerced_args = args |
259 | 272 |
260 metadata = {} | 273 metadata = {} |
261 for arg_name, arg_val in zip(self.func_parameters, coerced_args): | 274 for arg_name, arg_val in zip(self.uri_params, coerced_args): |
262 metadata[arg_name] = self._coerceRouteParameter( | 275 metadata[arg_name] = self._coerceRouteParameter( |
263 arg_name, arg_val) | 276 arg_name, arg_val) |
264 | 277 |
265 if self.is_generator_route: | 278 if self.is_generator_route: |
266 self.generator.onRouteFunctionUsed(self, metadata) | 279 self.generator.onRouteFunctionUsed(self, metadata) |
267 | 280 |
268 return self.getUri(metadata) | 281 return self.getUri(metadata) |
269 | 282 |
270 def _uriFormatRepl(self, m): | 283 def _uriFormatRepl(self, m): |
271 qual = m.group('qual') | 284 if m.group('qual') or m.group('var'): |
285 # Print a warning only if we're not in a worker process. | |
286 print_warning = not self.app.config.has('baker/worker_id') | |
287 if print_warning: | |
288 logger.warning("Route '%s' specified parameter types -- " | |
289 "they're not needed anymore." % | |
290 self.uri_pattern) | |
291 | |
272 name = m.group('name') | 292 name = m.group('name') |
273 | 293 self.uri_params.append(name) |
274 # Backwards compatibility... this will print a warning later. | 294 try: |
275 if qual is None: | 295 param_type = self.getParameterType(name) |
276 if name == 'year': | 296 if param_type == RouteParameter.TYPE_INT4: |
277 qual = 'int4' | 297 return '%%(%s)04d' % name |
278 elif name in ['month', 'day']: | 298 elif param_type == RouteParameter.TYPE_INT2: |
279 qual = 'int2' | 299 return '%%(%s)02d' % name |
280 | 300 return '%%(%s)s' % name |
281 if qual == 'int4': | 301 except: |
282 return '%%(%s)04d' % name | 302 known = [p.name for p in self.supported_params] |
283 elif qual == 'int2': | 303 raise Exception("Unknown route parameter '%s' for route '%s'. " |
284 return '%%(%s)02d' % name | 304 "Must be one of: %s'" % |
285 elif qual and qual != 'path': | 305 (name, self.uri_pattern, known)) |
286 raise Exception("Unknown route parameter type: %s" % qual) | |
287 return '%%(%s)s' % name | |
288 | 306 |
289 def _uriPatternRepl(self, m): | 307 def _uriPatternRepl(self, m): |
290 name = m.group('name') | 308 name = m.group('name') |
291 qual = m.group('qual') | 309 param_type = self.getParameterType(name) |
292 | 310 if param_type == RouteParameter.TYPE_PATH: |
293 # Backwards compatibility... this will print a warning later. | |
294 if qual is None: | |
295 if name == 'year': | |
296 qual = 'int4' | |
297 elif name in ['month', 'day']: | |
298 qual = 'int2' | |
299 | |
300 if qual == 'path' or m.group('var'): | |
301 return r'(?P<%s>[^\?]*)' % name | 311 return r'(?P<%s>[^\?]*)' % name |
302 elif qual == 'int4': | 312 elif param_type == RouteParameter.TYPE_INT4: |
303 return r'(?P<%s>\d{4})' % name | 313 return r'(?P<%s>\d{4})' % name |
304 elif qual == 'int2': | 314 elif param_type == RouteParameter.TYPE_INT2: |
305 return r'(?P<%s>\d{2})' % name | 315 return r'(?P<%s>\d{2})' % name |
306 elif qual and qual != 'path': | |
307 raise Exception("Unknown route parameter type: %s" % qual) | |
308 return r'(?P<%s>[^/\?]+)' % name | 316 return r'(?P<%s>[^/\?]+)' % name |
309 | 317 |
310 def _uriNoPathRepl(self, m): | 318 def _uriNoPathRepl(self, m): |
311 name = m.group('name') | 319 name = m.group('name') |
312 qualifier = m.group('qual') | 320 param_type = self.getParameterType(name) |
313 if qualifier == 'path': | 321 if param_type == RouteParameter.TYPE_PATH: |
314 return '' | 322 return '' |
315 return r'(?P<%s>[^/\?]+)' % name | 323 return r'(?P<%s>[^/\?]+)' % name |
316 | 324 |
317 def _coerceRouteParameter(self, name, val): | 325 def _coerceRouteParameter(self, name, val): |
318 param_type = self.param_types.get(name) | 326 try: |
319 if param_type is None: | 327 param_type = self.getParameterType(name) |
328 except: | |
329 # Unknown parameter... just leave it. | |
320 return val | 330 return val |
321 if param_type in ['int', 'int2', 'int4']: | 331 |
332 if param_type in [RouteParameter.TYPE_INT2, RouteParameter.TYPE_INT4]: | |
322 try: | 333 try: |
323 return int(val) | 334 return int(val) |
324 except ValueError: | 335 except ValueError: |
325 raise Exception( | 336 raise Exception( |
326 "Expected route parameter '%s' to be of type " | 337 "Expected route parameter '%s' to be an integer, " |
327 "'%s', but was: %s" % | 338 "but was: %s" % (name, param_type, val)) |
328 (name, param_type, val)) | 339 return val |
329 if param_type == 'path': | |
330 return val | |
331 raise Exception("Unknown route parameter type: %s" % param_type) | |
332 | |
333 def _getBackwardCompatibleParamType(self, name): | |
334 # Print a warning only if we're not in a worker process. | |
335 print_warning = not self.app.config.has('baker/worker_id') | |
336 | |
337 if name in ['year']: | |
338 if print_warning: | |
339 logger.warning( | |
340 "Route parameter '%%%s%%' has no type qualifier. " | |
341 "You probably meant '%%int4:%s%%' so we'll use that." % | |
342 (name, name)) | |
343 return 'int4' | |
344 if name in ['month', 'day']: | |
345 if print_warning: | |
346 logger.warning( | |
347 "Route parameter '%%%s%%' has no type qualifier. " | |
348 "You probably meant '%%int2:%s%%' so we'll use that." % | |
349 (name, name)) | |
350 return 'int2' | |
351 return None | |
352 | 340 |
353 def _validateFuncName(self, name): | 341 def _validateFuncName(self, name): |
354 if not name: | 342 if not name: |
355 return None | 343 return None |
356 i = name.find('(') | 344 i = name.find('(') |
367 self._routes = [] | 355 self._routes = [] |
368 self._arg_names = None | 356 self._arg_names = None |
369 | 357 |
370 def addFunc(self, route): | 358 def addFunc(self, route): |
371 if self._arg_names is None: | 359 if self._arg_names is None: |
372 self._arg_names = list(route.func_parameters) | 360 self._arg_names = list(route.uri_params) |
373 | 361 |
374 if route.func_parameters != self._arg_names: | 362 if route.uri_params != self._arg_names: |
375 raise Exception("Cannot merge route function with arguments '%s' " | 363 raise Exception("Cannot merge route function with arguments '%s' " |
376 "with route function with arguments '%s'." % | 364 "with route function with arguments '%s'." % |
377 (route.func_parameters, self._arg_names)) | 365 (route.uri_params, self._arg_names)) |
378 self._routes.append(route) | 366 self._routes.append(route) |
379 | 367 |
380 def __call__(self, *args, **kwargs): | 368 def __call__(self, *args, **kwargs): |
381 if len(self._routes) == 1 or len(args) == len(self._arg_names): | 369 if len(self._routes) == 1 or len(args) == len(self._arg_names): |
382 return self._routes[0].execTemplateFunc(*args, **kwargs) | 370 return self._routes[0].execTemplateFunc(*args, **kwargs) |