Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import re 

2from zope.interface import implementer 

3 

4from pyramid.interfaces import IRoutesMapper, IRoute 

5 

6from pyramid.compat import ( 

7 PY2, 

8 native_, 

9 text_, 

10 text_type, 

11 string_types, 

12 binary_type, 

13 is_nonstr_iter, 

14 decode_path_info, 

15) 

16 

17from pyramid.exceptions import URLDecodeError 

18 

19from pyramid.traversal import quote_path_segment, split_path_info, PATH_SAFE 

20 

21_marker = object() 

22 

23 

24@implementer(IRoute) 

25class Route(object): 

26 def __init__( 

27 self, name, pattern, factory=None, predicates=(), pregenerator=None 

28 ): 

29 self.pattern = pattern 

30 self.path = pattern # indefinite b/w compat, not in interface 

31 self.match, self.generate = _compile_route(pattern) 

32 self.name = name 

33 self.factory = factory 

34 self.predicates = predicates 

35 self.pregenerator = pregenerator 

36 

37 

38@implementer(IRoutesMapper) 

39class RoutesMapper(object): 

40 def __init__(self): 

41 self.routelist = [] 

42 self.static_routes = [] 

43 

44 self.routes = {} 

45 

46 def has_routes(self): 

47 return bool(self.routelist) 

48 

49 def get_routes(self, include_static=False): 

50 if include_static is True: 

51 return self.routelist + self.static_routes 

52 

53 return self.routelist 

54 

55 def get_route(self, name): 

56 return self.routes.get(name) 

57 

58 def connect( 

59 self, 

60 name, 

61 pattern, 

62 factory=None, 

63 predicates=(), 

64 pregenerator=None, 

65 static=False, 

66 ): 

67 if name in self.routes: 

68 oldroute = self.routes[name] 

69 if oldroute in self.routelist: 

70 self.routelist.remove(oldroute) 

71 

72 route = Route(name, pattern, factory, predicates, pregenerator) 

73 if not static: 

74 self.routelist.append(route) 

75 else: 

76 self.static_routes.append(route) 

77 

78 self.routes[name] = route 

79 return route 

80 

81 def generate(self, name, kw): 

82 return self.routes[name].generate(kw) 

83 

84 def __call__(self, request): 

85 environ = request.environ 

86 try: 

87 # empty if mounted under a path in mod_wsgi, for example 

88 path = decode_path_info(environ['PATH_INFO'] or '/') 

89 except KeyError: 

90 path = '/' 

91 except UnicodeDecodeError as e: 

92 raise URLDecodeError( 

93 e.encoding, e.object, e.start, e.end, e.reason 

94 ) 

95 

96 for route in self.routelist: 

97 match = route.match(path) 

98 if match is not None: 

99 preds = route.predicates 

100 info = {'match': match, 'route': route} 

101 if preds and not all((p(info, request) for p in preds)): 

102 continue 

103 return info 

104 

105 return {'route': None, 'match': None} 

106 

107 

108# stolen from bobo and modified 

109old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)') 

110star_at_end = re.compile(r'\*(\w*)$') 

111 

112# The tortuous nature of the regex named ``route_re`` below is due to the 

113# fact that we need to support at least one level of "inner" squigglies 

114# inside the expr of a {name:expr} pattern. This regex used to be just 

115# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}. 

116route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})') 

117 

118 

119def update_pattern(matchobj): 

120 name = matchobj.group(0) 

121 return '{%s}' % name[1:] 

122 

123 

124def _compile_route(route): 

125 # This function really wants to consume Unicode patterns natively, but if 

126 # someone passes us a bytestring, we allow it by converting it to Unicode 

127 # using the ASCII decoding. We decode it using ASCII because we don't 

128 # want to accept bytestrings with high-order characters in them here as 

129 # we have no idea what the encoding represents. 

130 if route.__class__ is not text_type: 

131 try: 

132 route = text_(route, 'ascii') 

133 except UnicodeDecodeError: 

134 raise ValueError( 

135 'The pattern value passed to add_route must be ' 

136 'either a Unicode string or a plain string without ' 

137 'any non-ASCII characters (you provided %r).' % route 

138 ) 

139 

140 if old_route_re.search(route) and not route_re.search(route): 

141 route = old_route_re.sub(update_pattern, route) 

142 

143 if not route.startswith('/'): 

144 route = '/' + route 

145 

146 remainder = None 

147 if star_at_end.search(route): 

148 route, remainder = route.rsplit('*', 1) 

149 

150 pat = route_re.split(route) 

151 

152 # every element in "pat" will be Unicode (regardless of whether the 

153 # route_re regex pattern is itself Unicode or str) 

154 pat.reverse() 

155 rpat = [] 

156 gen = [] 

157 prefix = pat.pop() # invar: always at least one element (route='/'+route) 

158 

159 # We want to generate URL-encoded URLs, so we url-quote the prefix, being 

160 # careful not to quote any embedded slashes. We have to replace '%' with 

161 # '%%' afterwards, as the strings that go into "gen" are used as string 

162 # replacement targets. 

163 gen.append( 

164 quote_path_segment(prefix, safe='/').replace('%', '%%') 

165 ) # native 

166 rpat.append(re.escape(prefix)) # unicode 

167 

168 while pat: 

169 name = pat.pop() # unicode 

170 name = name[1:-1] 

171 if ':' in name: 

172 # reg may contain colons as well, 

173 # so we must strictly split name into two parts 

174 name, reg = name.split(':', 1) 

175 else: 

176 reg = '[^/]+' 

177 gen.append('%%(%s)s' % native_(name)) # native 

178 name = '(?P<%s>%s)' % (name, reg) # unicode 

179 rpat.append(name) 

180 s = pat.pop() # unicode 

181 if s: 

182 rpat.append(re.escape(s)) # unicode 

183 # We want to generate URL-encoded URLs, so we url-quote this 

184 # literal in the pattern, being careful not to quote the embedded 

185 # slashes. We have to replace '%' with '%%' afterwards, as the 

186 # strings that go into "gen" are used as string replacement 

187 # targets. What is appended to gen is a native string. 

188 gen.append(quote_path_segment(s, safe='/').replace('%', '%%')) 

189 

190 if remainder: 

191 rpat.append('(?P<%s>.*?)' % remainder) # unicode 

192 gen.append('%%(%s)s' % native_(remainder)) # native 

193 

194 pattern = ''.join(rpat) + '$' # unicode 

195 

196 match = re.compile(pattern).match 

197 

198 def matcher(path): 

199 # This function really wants to consume Unicode patterns natively, 

200 # but if someone passes us a bytestring, we allow it by converting it 

201 # to Unicode using the ASCII decoding. We decode it using ASCII 

202 # because we don't want to accept bytestrings with high-order 

203 # characters in them here as we have no idea what the encoding 

204 # represents. 

205 if path.__class__ is not text_type: 

206 path = text_(path, 'ascii') 

207 m = match(path) 

208 if m is None: 

209 return None 

210 d = {} 

211 for k, v in m.groupdict().items(): 

212 # k and v will be Unicode 2.6.4 and lower doesnt accept unicode 

213 # kwargs as **kw, so we explicitly cast the keys to native 

214 # strings in case someone wants to pass the result as **kw 

215 nk = native_(k, 'ascii') 

216 if k == remainder: 

217 d[nk] = split_path_info(v) 

218 else: 

219 d[nk] = v 

220 return d 

221 

222 gen = ''.join(gen) 

223 

224 def q(v): 

225 return quote_path_segment(v, safe=PATH_SAFE) 

226 

227 def generator(dict): 

228 newdict = {} 

229 for k, v in dict.items(): 

230 if PY2: 

231 if v.__class__ is text_type: 

232 # url_quote below needs bytes, not unicode on Py2 

233 v = v.encode('utf-8') 

234 else: 

235 if v.__class__ is binary_type: 

236 # url_quote below needs a native string, not bytes on Py3 

237 v = v.decode('utf-8') 

238 

239 if k == remainder: 

240 # a stararg argument 

241 if is_nonstr_iter(v): 

242 v = '/'.join([q(x) for x in v]) # native 

243 else: 

244 if v.__class__ not in string_types: 

245 v = str(v) 

246 v = q(v) 

247 else: 

248 if v.__class__ not in string_types: 

249 v = str(v) 

250 # v may be bytes (py2) or native string (py3) 

251 v = q(v) 

252 

253 # at this point, the value will be a native string 

254 newdict[k] = v 

255 

256 result = gen % newdict # native string result 

257 return result 

258 

259 return matcher, generator