comparison piecrust/routing.py @ 723:606f6d57b5df

routing: Cleanup URL routing and improve page matching. * Add new types of route parameters for integers (int4, int2, int). * Remove hard-coded hacks around converting year/month/day values. * Make the blog post routes use the new typed parameters. * Fix problems with matching routes with integer parameters when they can get confused with a sub-page number.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 29 May 2016 20:19:28 -0700
parents ab5c6a8ae90a
children 8c3c2b949b82
comparison
equal deleted inserted replaced
722:f0a3af3fbea2 723:606f6d57b5df
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>path):)?(?P<name>\w+)%') 12 route_re = re.compile(r'%((?P<qual>[\w\d]+):)?(?P<name>\w+)%')
13 route_esc_re = re.compile(r'\\%((?P<qual>path)\\:)?(?P<name>\w+)\\%') 13 route_esc_re = re.compile(r'\\%((?P<qual>[\w\d]+)\\:)?(?P<name>\w+)\\%')
14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<args>.*)\)\s*$') 14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<args>.*)\)\s*$')
15 template_func_arg_re = re.compile(r'(?P<arg>\+?\w+)') 15 template_func_arg_re = re.compile(r'(?P<arg>\+?\w+)')
16 ugly_url_cleaner = re.compile(r'\.html$') 16 ugly_url_cleaner = re.compile(r'\.html$')
17 17
18 18
25 25
26 26
27 def create_route_metadata(page): 27 def create_route_metadata(page):
28 route_metadata = copy.deepcopy(page.source_metadata) 28 route_metadata = copy.deepcopy(page.source_metadata)
29 route_metadata.update(page.getRouteMetadata()) 29 route_metadata.update(page.getRouteMetadata())
30
31 # TODO: fix this hard-coded shit
32 for key in ['year', 'month', 'day']:
33 if key in route_metadata and isinstance(route_metadata[key], str):
34 route_metadata[key] = int(route_metadata[key])
35
36 return route_metadata 30 return route_metadata
37 31
38 32
39 class IRouteMetadataProvider(object): 33 class IRouteMetadataProvider(object):
40 def getRouteMetadata(self): 34 def getRouteMetadata(self):
72 66
73 # Get the straight-forward regex for matching this URI pattern. 67 # Get the straight-forward regex for matching this URI pattern.
74 p = route_esc_re.sub(self._uriPatternRepl, 68 p = route_esc_re.sub(self._uriPatternRepl,
75 re.escape(self.uri_pattern)) + '$' 69 re.escape(self.uri_pattern)) + '$'
76 self.uri_re = re.compile(p) 70 self.uri_re = re.compile(p)
71
72 # Get the types of the route parameters.
73 self.param_types = {}
74 for m in route_re.finditer(self.uri_pattern):
75 qual = m.group('qual')
76 if qual:
77 self.param_types[str(m.group('name'))] = qual
77 78
78 # If the URI pattern has a 'path'-type component, we'll need to match 79 # If the URI pattern has a 'path'-type component, we'll need to match
79 # the versions for which that component is empty. So for instance if 80 # the versions for which that component is empty. So for instance if
80 # we have `/foo/%path:bar%`, we may need to match `/foo` (note the 81 # we have `/foo/%path:bar%`, we may need to match `/foo` (note the
81 # lack of a trailing slash). We have to build a special pattern (in 82 # lack of a trailing slash). We have to build a special pattern (in
172 matched_keys = set(route_metadata.keys()) 173 matched_keys = set(route_metadata.keys())
173 missing_keys = self.required_route_metadata - matched_keys 174 missing_keys = self.required_route_metadata - matched_keys
174 for k in missing_keys: 175 for k in missing_keys:
175 route_metadata[k] = '' 176 route_metadata[k] = ''
176 177
177 # TODO: fix this hard-coded shit 178 for k in route_metadata:
178 for key in ['year', 'month', 'day']: 179 route_metadata[k] = self._coerceRouteParameter(
179 if key in route_metadata and isinstance(route_metadata[key], str): 180 k, route_metadata[k])
180 try:
181 route_metadata[key] = int(route_metadata[key])
182 except ValueError:
183 pass
184 181
185 return route_metadata 182 return route_metadata
186 183
187 def getUri(self, route_metadata, *, sub_num=1): 184 def getUri(self, route_metadata, *, sub_num=1):
185 route_metadata = dict(route_metadata)
186 for k in route_metadata:
187 route_metadata[k] = self._coerceRouteParameter(
188 k, route_metadata[k])
189
188 uri = self.uri_format % route_metadata 190 uri = self.uri_format % route_metadata
189 suffix = None 191 suffix = None
190 if sub_num > 1: 192 if sub_num > 1:
191 # Note that we know the pagination suffix starts with a slash. 193 # Note that we know the pagination suffix starts with a slash.
192 suffix = self.pagination_suffix_format % {'num': sub_num} 194 suffix = self.pagination_suffix_format % {'num': sub_num}
228 uri += '?!debug' 230 uri += '?!debug'
229 231
230 return uri 232 return uri
231 233
232 def _uriFormatRepl(self, m): 234 def _uriFormatRepl(self, m):
235 qual = m.group('qual')
233 name = m.group('name') 236 name = m.group('name')
234 #TODO: fix this hard-coded shit 237 if qual == 'int4':
235 if name == 'year': 238 return '%%(%s)04d' % name
236 return '%(year)04d' 239 elif qual == 'int2':
237 if name == 'month': 240 return '%%(%s)02d' % name
238 return '%(month)02d' 241 return '%%(%s)s' % name
239 if name == 'day':
240 return '%(day)02d'
241 return '%(' + name + ')s'
242 242
243 def _uriPatternRepl(self, m): 243 def _uriPatternRepl(self, m):
244 name = m.group('name') 244 name = m.group('name')
245 qualifier = m.group('qual') 245 qual = m.group('qual')
246 if qualifier == 'path': 246 if qual == 'path':
247 return r'(?P<%s>[^\?]*)' % name 247 return r'(?P<%s>[^\?]*)' % name
248 elif qual == 'int4':
249 return r'(?P<%s>\d{4})' % name
250 elif qual == 'int2':
251 return r'(?P<%s>\d{2})' % name
248 return r'(?P<%s>[^/\?]+)' % name 252 return r'(?P<%s>[^/\?]+)' % name
249 253
250 def _uriNoPathRepl(self, m): 254 def _uriNoPathRepl(self, m):
251 name = m.group('name') 255 name = m.group('name')
252 qualifier = m.group('qual') 256 qualifier = m.group('qual')
253 if qualifier == 'path': 257 if qualifier == 'path':
254 return '' 258 return ''
255 return r'(?P<%s>[^/\?]+)' % name 259 return r'(?P<%s>[^/\?]+)' % name
260
261 def _coerceRouteParameter(self, name, val):
262 param_type = self.param_types.get(name)
263 if param_type is None:
264 return val
265 if param_type in ['int', 'int2', 'int4']:
266 try:
267 return int(val)
268 except ValueError:
269 raise Exception(
270 "Expected route parameter '%s' to be of type "
271 "'%s', but was: %s" %
272 (k, param_type, route_metadata[k]))
273 if param_type == 'path':
274 return val
275 raise Exception("Unknown route parameter type: %s" % param_type)
256 276
257 def _createTemplateFunc(self, func_def): 277 def _createTemplateFunc(self, func_def):
258 if func_def is None: 278 if func_def is None:
259 return 279 return
260 280
297 non_var_args = list(self.template_func_args) 317 non_var_args = list(self.template_func_args)
298 if is_variable: 318 if is_variable:
299 del non_var_args[-1] 319 del non_var_args[-1]
300 320
301 for arg_name, arg_val in zip(non_var_args, args): 321 for arg_name, arg_val in zip(non_var_args, args):
302 #TODO: fix this hard-coded shit. 322 metadata[arg_name] = self._coerceRouteParameter(
303 if arg_name in ['year', 'month', 'day']: 323 arg_name, arg_val)
304 arg_val = int(arg_val)
305 metadata[arg_name] = arg_val
306 324
307 if is_variable: 325 if is_variable:
308 metadata[self.template_func_vararg] = [] 326 metadata[self.template_func_vararg] = []
309 for i in range(len(non_var_args), len(args)): 327 for i in range(len(non_var_args), len(args)):
310 metadata[self.template_func_vararg].append(args[i]) 328 metadata[self.template_func_vararg].append(args[i])