comparison piecrust/routing.py @ 711:ab5c6a8ae90a

bake: Replace hard-coded taxonomy support with "generator" system. * Taxonomies are now implemented one or more `TaxonomyGenerator`s. * A `BlogArchivesGenerator` stub is there but non-functional.
author Ludovic Chabant <ludovic@chabant.com>
date Thu, 26 May 2016 19:52:47 -0700
parents 2f780b191541
children 606f6d57b5df
comparison
equal deleted inserted replaced
710:e85f29b28b84 711:ab5c6a8ae90a
1 import re 1 import re
2 import os.path 2 import os.path
3 import copy 3 import copy
4 import logging 4 import logging
5 import urllib.parse 5 import urllib.parse
6 import unidecode 6 from werkzeug.utils import cached_property
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>path):)?(?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>path)\\:)?(?P<name>\w+)\\%')
14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<first_arg>\w+)' 14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<args>.*)\)\s*$')
15 r'(?P<other_args>.*)\)\s*$') 15 template_func_arg_re = re.compile(r'(?P<arg>\+?\w+)')
16 template_func_arg_re = re.compile(r',\s*(?P<arg>\w+)')
17 ugly_url_cleaner = re.compile(r'\.html$') 16 ugly_url_cleaner = re.compile(r'\.html$')
18 17
19 18
20 class RouteNotFoundError(Exception): 19 class RouteNotFoundError(Exception):
20 pass
21
22
23 class InvalidRouteError(Exception):
21 pass 24 pass
22 25
23 26
24 def create_route_metadata(page): 27 def create_route_metadata(page):
25 route_metadata = copy.deepcopy(page.source_metadata) 28 route_metadata = copy.deepcopy(page.source_metadata)
36 class IRouteMetadataProvider(object): 39 class IRouteMetadataProvider(object):
37 def getRouteMetadata(self): 40 def getRouteMetadata(self):
38 raise NotImplementedError() 41 raise NotImplementedError()
39 42
40 43
41 SLUGIFY_ENCODE = 1 44 ROUTE_TYPE_SOURCE = 0
42 SLUGIFY_TRANSLITERATE = 2 45 ROUTE_TYPE_GENERATOR = 1
43 SLUGIFY_LOWERCASE = 4
44 SLUGIFY_DOT_TO_DASH = 8
45 SLUGIFY_SPACE_TO_DASH = 16
46
47
48 re_first_dot_to_dash = re.compile(r'^\.+')
49 re_dot_to_dash = re.compile(r'\.+')
50 re_space_to_dash = re.compile(r'\s+')
51
52
53 def _parse_slugify_mode(value):
54 mapping = {
55 'encode': SLUGIFY_ENCODE,
56 'transliterate': SLUGIFY_TRANSLITERATE,
57 'lowercase': SLUGIFY_LOWERCASE,
58 'dot_to_dash': SLUGIFY_DOT_TO_DASH,
59 'space_to_dash': SLUGIFY_SPACE_TO_DASH}
60 mode = 0
61 for v in value.split(','):
62 f = mapping.get(v.strip())
63 if f is None:
64 if v == 'iconv':
65 raise Exception("'iconv' is not supported as a slugify mode "
66 "in PieCrust2. Use 'transliterate'.")
67 raise Exception("Unknown slugify flag: %s" % v)
68 mode |= f
69 return mode
70 46
71 47
72 class Route(object): 48 class Route(object):
73 """ Information about a route for a PieCrust application. 49 """ Information about a route for a PieCrust application.
74 Each route defines the "shape" of an URL and how it maps to 50 Each route defines the "shape" of an URL and how it maps to
75 sources and taxonomies. 51 sources and generators.
76 """ 52 """
77 def __init__(self, app, cfg): 53 def __init__(self, app, cfg):
78 self.app = app 54 self.app = app
79 55
80 self.source_name = cfg['source'] 56 self.source_name = cfg.get('source')
81 self.taxonomy_name = cfg.get('taxonomy') 57 self.generator_name = cfg.get('generator')
82 self.taxonomy_term_sep = cfg.get('term_separator', '/') 58 if not self.source_name and not self.generator_name:
83 59 raise InvalidRouteError(
84 sm = cfg.get('slugify_mode') 60 "Both `source` and `generator` are specified.")
85 if not sm:
86 sm = app.config.get('site/slugify_mode', 'encode')
87 self.slugify_mode = _parse_slugify_mode(sm)
88 61
89 self.pretty_urls = app.config.get('site/pretty_urls') 62 self.pretty_urls = app.config.get('site/pretty_urls')
90 self.trailing_slash = app.config.get('site/trailing_slash') 63 self.trailing_slash = app.config.get('site/trailing_slash')
91 self.show_debug_info = app.config.get('site/show_debug_info') 64 self.show_debug_info = app.config.get('site/show_debug_info')
92 self.pagination_suffix_format = app.config.get( 65 self.pagination_suffix_format = app.config.get(
125 self.required_route_metadata.add(m.group('name')) 98 self.required_route_metadata.add(m.group('name'))
126 99
127 self.template_func = None 100 self.template_func = None
128 self.template_func_name = None 101 self.template_func_name = None
129 self.template_func_args = [] 102 self.template_func_args = []
103 self.template_func_vararg = None
130 self._createTemplateFunc(cfg.get('func')) 104 self._createTemplateFunc(cfg.get('func'))
131 105
132 @property 106 @property
133 def is_taxonomy_route(self): 107 def route_type(self):
134 return self.taxonomy_name is not None 108 if self.source_name:
109 return ROUTE_TYPE_SOURCE
110 elif self.generator_name:
111 return ROUTE_TYPE_GENERATOR
112 else:
113 raise InvalidRouteError()
135 114
136 @property 115 @property
116 def is_source_route(self):
117 return self.route_type == ROUTE_TYPE_SOURCE
118
119 @property
120 def is_generator_route(self):
121 return self.route_type == ROUTE_TYPE_GENERATOR
122
123 @cached_property
137 def source(self): 124 def source(self):
125 if not self.is_source_route:
126 return InvalidRouteError("This is not a source route.")
138 for src in self.app.sources: 127 for src in self.app.sources:
139 if src.name == self.source_name: 128 if src.name == self.source_name:
140 return src 129 return src
141 raise Exception("Can't find source '%s' for route '%'." % ( 130 raise Exception("Can't find source '%s' for route '%s'." % (
142 self.source_name, self.uri)) 131 self.source_name, self.uri))
143 132
144 @property 133 @cached_property
145 def source_realm(self): 134 def generator(self):
146 return self.source.realm 135 if not self.is_generator_route:
136 return InvalidRouteError("This is not a generator route.")
137 for gen in self.app.generators:
138 if gen.name == self.generator_name:
139 return gen
140 raise Exception("Can't find generator '%s' for route '%s'." % (
141 self.generator_name, self.uri))
147 142
148 def matchesMetadata(self, route_metadata): 143 def matchesMetadata(self, route_metadata):
149 return self.required_route_metadata.issubset(route_metadata.keys()) 144 return self.required_route_metadata.issubset(route_metadata.keys())
150 145
151 def matchUri(self, uri, strict=False): 146 def matchUri(self, uri, strict=False):
232 if self.show_debug_info: 227 if self.show_debug_info:
233 uri += '?!debug' 228 uri += '?!debug'
234 229
235 return uri 230 return uri
236 231
237 def getTaxonomyTerms(self, route_metadata):
238 if not self.is_taxonomy_route:
239 raise Exception("This route isn't a taxonomy route.")
240
241 tax = self.app.getTaxonomy(self.taxonomy_name)
242 all_values = route_metadata.get(tax.term_name)
243 if all_values is None:
244 raise Exception("'%s' values couldn't be found in route metadata" %
245 tax.term_name)
246
247 if self.taxonomy_term_sep in all_values:
248 return tuple(all_values.split(self.taxonomy_term_sep))
249 return all_values
250
251 def slugifyTaxonomyTerm(self, term):
252 if isinstance(term, tuple):
253 return self.taxonomy_term_sep.join(
254 map(self._slugifyOne, term))
255 return self._slugifyOne(term)
256
257 def _slugifyOne(self, term):
258 if self.slugify_mode & SLUGIFY_TRANSLITERATE:
259 term = unidecode.unidecode(term)
260 if self.slugify_mode & SLUGIFY_LOWERCASE:
261 term = term.lower()
262 if self.slugify_mode & SLUGIFY_DOT_TO_DASH:
263 term = re_first_dot_to_dash.sub('', term)
264 term = re_dot_to_dash.sub('-', term)
265 if self.slugify_mode & SLUGIFY_SPACE_TO_DASH:
266 term = re_space_to_dash.sub('-', term)
267 return term
268
269 def _uriFormatRepl(self, m): 232 def _uriFormatRepl(self, m):
270 name = m.group('name') 233 name = m.group('name')
271 #TODO: fix this hard-coded shit 234 #TODO: fix this hard-coded shit
272 if name == 'year': 235 if name == 'year':
273 return '%(year)04d' 236 return '%(year)04d'
278 return '%(' + name + ')s' 241 return '%(' + name + ')s'
279 242
280 def _uriPatternRepl(self, m): 243 def _uriPatternRepl(self, m):
281 name = m.group('name') 244 name = m.group('name')
282 qualifier = m.group('qual') 245 qualifier = m.group('qual')
283 if qualifier == 'path' or self.taxonomy_name: 246 if qualifier == 'path':
284 return r'(?P<%s>[^\?]*)' % name 247 return r'(?P<%s>[^\?]*)' % name
285 return r'(?P<%s>[^/\?]+)' % name 248 return r'(?P<%s>[^/\?]+)' % name
286 249
287 def _uriNoPathRepl(self, m): 250 def _uriNoPathRepl(self, m):
288 name = m.group('name') 251 name = m.group('name')
300 raise Exception("Template function definition for route '%s' " 263 raise Exception("Template function definition for route '%s' "
301 "has invalid syntax: %s" % 264 "has invalid syntax: %s" %
302 (self.uri_pattern, func_def)) 265 (self.uri_pattern, func_def))
303 266
304 self.template_func_name = m.group('name') 267 self.template_func_name = m.group('name')
305 self.template_func_args.append(m.group('first_arg')) 268 self.template_func_args = []
306 arg_list = m.group('other_args') 269 arg_list = m.group('args')
307 if arg_list: 270 if arg_list:
308 self.template_func_args += template_func_arg_re.findall(arg_list) 271 self.template_func_args = template_func_arg_re.findall(arg_list)
309 272 for i in range(len(self.template_func_args) - 1):
310 if self.taxonomy_name: 273 if self.template_func_args[i][0] == '+':
311 # This will be a taxonomy route function... this means we can 274 raise Exception("Only the last route parameter can be a "
312 # have a variable number of parameters, but only one parameter 275 "variable argument (prefixed with `+`)")
313 # definition, which is the value. 276
314 if len(self.template_func_args) != 1: 277 if (self.template_func_args and
315 raise Exception("Route '%s' is a taxonomy route and must have " 278 self.template_func_args[-1][0] == '+'):
316 "only one argument, which is the term value." % 279 self.template_func_vararg = self.template_func_args[-1][1:]
317 self.uri_pattern) 280
318 281 def template_func(*args):
319 def template_func(*args): 282 is_variable = (self.template_func_vararg is not None)
320 if len(args) == 0: 283 if not is_variable and len(args) != len(self.template_func_args):
321 raise Exception( 284 raise Exception(
322 "Route function '%s' expected at least one " 285 "Route function '%s' expected %d arguments, "
323 "argument." % func_def) 286 "got %d." %
324 287 (func_def, len(self.template_func_args),
325 # Term combinations can be passed as an array, or as multiple 288 len(args)))
326 # arguments. 289 elif is_variable and len(args) < len(self.template_func_args):
327 values = args 290 raise Exception(
328 if len(args) == 1 and isinstance(args[0], list): 291 "Route function '%s' expected at least %d arguments, "
329 values = args[0] 292 "got %d." %
330 293 (func_def, len(self.template_func_args),
331 # We need to register this use of a taxonomy term. 294 len(args)))
332 if len(values) == 1: 295
333 registered_values = str(values[0]) 296 metadata = {}
334 else: 297 non_var_args = list(self.template_func_args)
335 registered_values = tuple([str(v) for v in values]) 298 if is_variable:
336 eis = self.app.env.exec_info_stack 299 del non_var_args[-1]
337 cpi = eis.current_page_info.render_ctx.current_pass_info 300
338 if cpi: 301 for arg_name, arg_val in zip(non_var_args, args):
339 cpi.used_taxonomy_terms.add( 302 #TODO: fix this hard-coded shit.
340 (self.source_name, self.taxonomy_name, 303 if arg_name in ['year', 'month', 'day']:
341 registered_values)) 304 arg_val = int(arg_val)
342 305 metadata[arg_name] = arg_val
343 str_values = self.slugifyTaxonomyTerm(registered_values) 306
344 term_name = self.template_func_args[0] 307 if is_variable:
345 metadata = {term_name: str_values} 308 metadata[self.template_func_vararg] = []
346 309 for i in range(len(non_var_args), len(args)):
347 return self.getUri(metadata) 310 metadata[self.template_func_vararg].append(args[i])
348 311
349 else: 312 if self.is_generator_route:
350 # Normal route function. 313 self.generator.onRouteFunctionUsed(self, metadata)
351 def template_func(*args): 314
352 if len(args) != len(self.template_func_args): 315 return self.getUri(metadata)
353 raise Exception(
354 "Route function '%s' expected %d arguments, "
355 "got %d." %
356 (func_def, len(self.template_func_args),
357 len(args)))
358 metadata = {}
359 for arg_name, arg_val in zip(self.template_func_args, args):
360 #TODO: fix this hard-coded shit.
361 if arg_name in ['year', 'month', 'day']:
362 arg_val = int(arg_val)
363 metadata[arg_name] = arg_val
364 return self.getUri(metadata)
365 316
366 self.template_func = template_func 317 self.template_func = template_func
367 318
368 319
369 class CompositeRouteFunction(object): 320 class CompositeRouteFunction(object):