Michael Merickel
2011-08-21 7ecc829f5a78e2a04a64cc9a44d46a151b171554
commit | author | age
50cedf 1 import itertools
05c023 2 import re
50cedf 3 from bisect import insort
25cbe1 4 from urllib import unquote
74409d 5
b60bdb 6 from pyramid.interfaces import IRoute
534cb4 7 from pyramid.interfaces import IRouteGroup
MM 8 from pyramid.interfaces import IRoutesMapper
8392d7 9
b60bdb 10 from pyramid.encode import url_quote
CM 11 from pyramid.exceptions import URLDecodeError
12 from pyramid.traversal import traversal_path
13 from pyramid.traversal import quote_path_segment
1aa864 14 from pyramid.util import join_elements
534cb4 15
MM 16 from zope.interface import implements
74409d 17
dfc2b6 18 _marker = object()
277df8 19
05c023 20 class Route(object):
74409d 21     implements(IRoute)
70f1cd 22     def __init__(self, name, pattern, factory=None, predicates=(),
4018ad 23                  pregenerator=None):
74409d 24         self.pattern = pattern
CM 25         self.path = pattern # indefinite b/w compat, not in interface
671416 26         self.match, self.generate, self.args = _compile_route(pattern)
05c023 27         self.name = name
CM 28         self.factory = factory
65476e 29         self.predicates = predicates
70f1cd 30         self.pregenerator = pregenerator
05c023 31
1aa864 32     def gen(self, request, elements, kw):
MM 33         if self.pregenerator is not None:
34             elements, kw = self.pregenerator(request, elements, kw)
35
36         path = self.generate(kw)
37
38         if elements:
39             suffix = join_elements(elements)
40             if not path.endswith('/'):
41                 suffix = '/' + suffix
42         else:
43             suffix = ''
44
45         return path + suffix, kw
46
50cedf 47 class RouteGroup(object):
534cb4 48     implements(IRouteGroup)
50cedf 49     def __init__(self, name):
MM 50         self.name = name
51         self.counter = itertools.count(1)
52         self.routes = []
53         self.sorted_routes = []
54
7ecc82 55     def _find_best_match(self, request, elements, kw):
50cedf 56         """ Compare the provided keys to the required args of a route.
MM 57
58         The selected route is the first one with all required keys satisfied.
59         """
60         default_keys = frozenset(kw.keys())
61         for entry in self.sorted_routes:
62             args, route = entry[2:4]
63             if route.pregenerator is not None:
64                 e, k = route.pregenerator(request, elements[:], dict(kw))
65                 keys = frozenset(k.keys())
66             else:
67                 e, k, keys = elements, kw, default_keys
68             if args.issubset(keys):
69                 return route, e, k
70         raise KeyError('Cannot find matching route in group "%s" using '
71                        'provided keys "%s"' % (self.name, sorted(keys)))
72
73     def gen(self, request, elements, kw):
7ecc82 74         route, elements, kw = self._find_best_match(request, elements, kw)
50cedf 75
MM 76         path = route.generate(kw)
77
78         if elements:
79             suffix = join_elements(elements)
80             if not path.endswith('/'):
81                 suffix = '/' + suffix
82         else:
83             suffix = ''
84
85         return path + suffix, kw
86
87     def add(self, route):
88         self.routes.append(route)
89
90         args = frozenset(route.args)
534cb4 91         # -len(args) sorts routes in descending order by the number of args
7ecc82 92         # -next(self.counter) sorts routes in reverse order of adding
MM 93         entry = (-len(args), -next(self.counter), args, route)
50cedf 94         insort(self.sorted_routes, entry)
MM 95
cbfafb 96 class RoutesMapper(object):
74409d 97     implements(IRoutesMapper)
cbfafb 98     def __init__(self):
05c023 99         self.routelist = []
CM 100         self.routes = {}
534cb4 101         self.groups = {}
62267e 102
CM 103     def has_routes(self):
05c023 104         return bool(self.routelist)
62267e 105
750ce4 106     def get_routes(self):
CM 107         return self.routelist
108
534cb4 109     def get_groups(self):
MM 110         return self.groups
111
74409d 112     def get_route(self, name):
CM 113         return self.routes.get(name)
534cb4 114
MM 115     def get_group(self, name):
116         return self.groups.get(name)
74409d 117
70f1cd 118     def connect(self, name, pattern, factory=None, predicates=(),
e725cf 119                 pregenerator=None, static=False):
4018ad 120         route = Route(name, pattern, factory, predicates, pregenerator)
534cb4 121         group = self.get_group(name)
MM 122         if group is not None:
44c2bb 123             group.add(route)
MM 124         else:
534cb4 125             oldroute = self.get_route(name)
MM 126             if oldroute in self.routelist:
127                 self.routelist.remove(oldroute)
44c2bb 128             self.routes[name] = route
534cb4 129         if not static:
MM 130             self.routelist.append(route)
05c023 131         return route
CM 132
534cb4 133     def add_group(self, name):
MM 134         oldgroup = self.get_group(name)
135         oldroute = self.get_route(name)
136         if oldgroup is not None:
137             for route in oldgroup.routes:
138                 if route in self.routelist:
139                     self.routelist.remove(route)
140         elif oldroute is not None:
141             if oldroute in self.routelist:
142                 self.routelist.remove(oldroute)
143         group = RouteGroup(name)
144         self.groups[name] = group
145         self.routes[name] = group
146         return group
147
05c023 148     def generate(self, name, kw):
CM 149         return self.routes[name].generate(kw)
8392d7 150
11644e 151     def __call__(self, request):
cbfafb 152         environ = request.environ
11644e 153         try:
cbfafb 154             # empty if mounted under a path in mod_wsgi, for example
CM 155             path = environ['PATH_INFO'] or '/' 
f646fe 156         except KeyError:
22e72d 157             path = '/'
CM 158
05c023 159         for route in self.routelist:
CM 160             match = route.match(path)
161             if match is not None:
65476e 162                 preds = route.predicates
23ab84 163                 info = {'match':match, 'route':route}
44bc11 164                 if preds and not all((p(info, request) for p in preds)):
65476e 165                     continue
44bc11 166                 return info
8392d7 167
cbfafb 168         return {'route':None, 'match':None}
62267e 169
05c023 170 # stolen from bobo and modified
4018ad 171 old_route_re = re.compile(r'(\:[a-zA-Z]\w*)')
da171d 172 star_at_end = re.compile(r'\*\w*$')
b8fc93 173
e39ddf 174 # The torturous nature of the regex named ``route_re`` below is due to the
JG 175 # fact that we need to support at least one level of "inner" squigglies
176 # inside the expr of a {name:expr} pattern.  This regex used to be just
177 # (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}.
178 route_re = re.compile(r'(\{[a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
959523 179
4018ad 180 def update_pattern(matchobj):
BB 181     name = matchobj.group(0)
182     return '{%s}' % name[1:]
183
184 def _compile_route(route):
185     if old_route_re.search(route) and not route_re.search(route):
186         route = old_route_re.sub(update_pattern, route)
187
05c023 188     if not route.startswith('/'):
CM 189         route = '/' + route
560e6e 190
05c023 191     star = None
da171d 192     if star_at_end.search(route):
05c023 193         route, star = route.rsplit('*', 1)
da171d 194
05c023 195     pat = route_re.split(route)
CM 196     pat.reverse()
197     rpat = []
198     gen = []
560e6e 199     prefix = pat.pop() # invar: always at least one element (route='/'+route)
CM 200     rpat.append(re.escape(prefix))
201     gen.append(prefix)
671416 202     args = [] # list of placeholder names in the pattern
560e6e 203
05c023 204     while pat:
CM 205         name = pat.pop()
4018ad 206         name = name[1:-1]
BB 207         if ':' in name:
208             name, reg = name.split(':')
209         else:
210             reg = '[^/]+'
671416 211         args.append(name)
e84116 212         gen.append('%%(%s)s' % name)
4018ad 213         name = '(?P<%s>%s)' % (name, reg)
05c023 214         rpat.append(name)
CM 215         s = pat.pop()
216         if s:
217             rpat.append(re.escape(s))
218             gen.append(s)
219
220     if star:
671416 221         args.append(star)
05c023 222         rpat.append('(?P<%s>.*?)' % star)
CM 223         gen.append('%%(%s)s' % star)
224
225     pattern = ''.join(rpat) + '$'
226
227     match = re.compile(pattern).match
228     def matcher(path):
229         m = match(path)
230         if m is None:
231             return m
25cbe1 232         d = {}
ac4689 233         for k, v in m.groupdict().iteritems():
CM 234             if k == star:
235                 d[k] = traversal_path(v)
236             else:
237                 encoded = unquote(v)
238                 try:
239                     d[k] = encoded.decode('utf-8')
240                 except UnicodeDecodeError, e:
241                     raise URLDecodeError(
242                         e.encoding, e.object, e.start, e.end, e.reason
243                         )
0482bd 244                         
CM 245                         
25cbe1 246         return d
CM 247                     
05c023 248
CM 249     gen = ''.join(gen)
250     def generator(dict):
251         newdict = {}
252         for k, v in dict.items():
253             if isinstance(v, unicode):
254                 v = v.encode('utf-8')
25cbe1 255             if k == star and hasattr(v, '__iter__'):
CM 256                 v = '/'.join([quote_path_segment(x) for x in v])
257             elif k != star:
17358d 258                 try:
eb9fbf 259                     v = url_quote(v)
17358d 260                 except TypeError:
CM 261                     pass
05c023 262             newdict[k] = v
CM 263         return gen % newdict
264
671416 265     return matcher, generator, args
187941 266
MM 267 def DefaultsPregenerator(defaults, wrapped=None):
268     if wrapped is None:
269         wrapped = lambda r, e, k: (e, k)
270     def generator(request, elements, kwargs):
271         newkw = dict(defaults)
272         newkw.update(kwargs)
273         return wrapped(request, elements, newkw)
274     return generator