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

1from hashlib import md5 

2from webob.acceptparse import Accept 

3 

4from pyramid.compat import bytes_, is_nonstr_iter 

5from pyramid.exceptions import ConfigurationError 

6from pyramid.interfaces import IPredicateList, PHASE1_CONFIG 

7from pyramid.predicates import Notted 

8from pyramid.registry import predvalseq 

9from pyramid.util import TopologicalSorter 

10 

11 

12MAX_ORDER = 1 << 30 

13DEFAULT_PHASH = md5().hexdigest() 

14 

15 

16class PredicateConfiguratorMixin(object): 

17 def get_predlist(self, name): 

18 predlist = self.registry.queryUtility(IPredicateList, name=name) 

19 if predlist is None: 

20 predlist = PredicateList() 

21 self.registry.registerUtility(predlist, IPredicateList, name=name) 

22 return predlist 

23 

24 def _add_predicate( 

25 self, type, name, factory, weighs_more_than=None, weighs_less_than=None 

26 ): 

27 factory = self.maybe_dotted(factory) 

28 discriminator = ('%s option' % type, name) 

29 intr = self.introspectable( 

30 '%s predicates' % type, 

31 discriminator, 

32 '%s predicate named %s' % (type, name), 

33 '%s predicate' % type, 

34 ) 

35 intr['name'] = name 

36 intr['factory'] = factory 

37 intr['weighs_more_than'] = weighs_more_than 

38 intr['weighs_less_than'] = weighs_less_than 

39 

40 def register(): 

41 predlist = self.get_predlist(type) 

42 predlist.add( 

43 name, 

44 factory, 

45 weighs_more_than=weighs_more_than, 

46 weighs_less_than=weighs_less_than, 

47 ) 

48 

49 self.action( 

50 discriminator, 

51 register, 

52 introspectables=(intr,), 

53 order=PHASE1_CONFIG, 

54 ) # must be registered early 

55 

56 

57class not_(object): 

58 """ 

59 

60 You can invert the meaning of any predicate value by wrapping it in a call 

61 to :class:`pyramid.config.not_`. 

62 

63 .. code-block:: python 

64 :linenos: 

65 

66 from pyramid.config import not_ 

67 

68 config.add_view( 

69 'mypackage.views.my_view', 

70 route_name='ok', 

71 request_method=not_('POST') 

72 ) 

73 

74 The above example will ensure that the view is called if the request method 

75 is *not* ``POST``, at least if no other view is more specific. 

76 

77 This technique of wrapping a predicate value in ``not_`` can be used 

78 anywhere predicate values are accepted: 

79 

80 - :meth:`pyramid.config.Configurator.add_view` 

81 

82 - :meth:`pyramid.config.Configurator.add_route` 

83 

84 - :meth:`pyramid.config.Configurator.add_subscriber` 

85 

86 - :meth:`pyramid.view.view_config` 

87 

88 - :meth:`pyramid.events.subscriber` 

89 

90 .. versionadded:: 1.5 

91 """ 

92 

93 def __init__(self, value): 

94 self.value = value 

95 

96 

97# under = after 

98# over = before 

99 

100 

101class PredicateList(object): 

102 def __init__(self): 

103 self.sorter = TopologicalSorter() 

104 self.last_added = None 

105 

106 def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): 

107 # Predicates should be added to a predicate list in (presumed) 

108 # computation expense order. 

109 # if weighs_more_than is None and weighs_less_than is None: 

110 # weighs_more_than = self.last_added or FIRST 

111 # weighs_less_than = LAST 

112 self.last_added = name 

113 self.sorter.add( 

114 name, factory, after=weighs_more_than, before=weighs_less_than 

115 ) 

116 

117 def names(self): 

118 # Return the list of valid predicate names. 

119 return self.sorter.names 

120 

121 def make(self, config, **kw): 

122 # Given a configurator and a list of keywords, a predicate list is 

123 # computed. Elsewhere in the code, we evaluate predicates using a 

124 # generator expression. All predicates associated with a view or 

125 # route must evaluate true for the view or route to "match" during a 

126 # request. The fastest predicate should be evaluated first, then the 

127 # next fastest, and so on, as if one returns false, the remainder of 

128 # the predicates won't need to be evaluated. 

129 # 

130 # While we compute predicates, we also compute a predicate hash (aka 

131 # phash) that can be used by a caller to identify identical predicate 

132 # lists. 

133 ordered = self.sorter.sorted() 

134 phash = md5() 

135 weights = [] 

136 preds = [] 

137 for n, (name, predicate_factory) in enumerate(ordered): 

138 vals = kw.pop(name, None) 

139 if vals is None: # XXX should this be a sentinel other than None? 

140 continue 

141 if not isinstance(vals, predvalseq): 

142 vals = (vals,) 

143 for val in vals: 

144 realval = val 

145 notted = False 

146 if isinstance(val, not_): 

147 realval = val.value 

148 notted = True 

149 pred = predicate_factory(realval, config) 

150 if notted: 

151 pred = Notted(pred) 

152 hashes = pred.phash() 

153 if not is_nonstr_iter(hashes): 

154 hashes = [hashes] 

155 for h in hashes: 

156 phash.update(bytes_(h)) 

157 weights.append(1 << n + 1) 

158 preds.append(pred) 

159 if kw: 

160 from difflib import get_close_matches 

161 

162 closest = [] 

163 names = [name for name, _ in ordered] 

164 for name in kw: 

165 closest.extend(get_close_matches(name, names, 3)) 

166 

167 raise ConfigurationError( 

168 'Unknown predicate values: %r (did you mean %s)' 

169 % (kw, ','.join(closest)) 

170 ) 

171 # A "order" is computed for the predicate list. An order is 

172 # a scoring. 

173 # 

174 # Each predicate is associated with a weight value. The weight of a 

175 # predicate symbolizes the relative potential "importance" of the 

176 # predicate to all other predicates. A larger weight indicates 

177 # greater importance. 

178 # 

179 # All weights for a given predicate list are bitwise ORed together 

180 # to create a "score"; this score is then subtracted from 

181 # MAX_ORDER and divided by an integer representing the number of 

182 # predicates+1 to determine the order. 

183 # 

184 # For views, the order represents the ordering in which a "multiview" 

185 # ( a collection of views that share the same context/request/name 

186 # triad but differ in other ways via predicates) will attempt to call 

187 # its set of views. Views with lower orders will be tried first. 

188 # The intent is to a) ensure that views with more predicates are 

189 # always evaluated before views with fewer predicates and b) to 

190 # ensure a stable call ordering of views that share the same number 

191 # of predicates. Views which do not have any predicates get an order 

192 # of MAX_ORDER, meaning that they will be tried very last. 

193 score = 0 

194 for bit in weights: 

195 score = score | bit 

196 order = (MAX_ORDER - score) / (len(preds) + 1) 

197 return order, preds, phash.hexdigest() 

198 

199 

200def normalize_accept_offer(offer, allow_range=False): 

201 if allow_range and '*' in offer: 

202 return offer.lower() 

203 return str(Accept.parse_offer(offer)) 

204 

205 

206def sort_accept_offers(offers, order=None): 

207 """ 

208 Sort a list of offers by preference. 

209 

210 For a given ``type/subtype`` category of offers, this algorithm will 

211 always sort offers with params higher than the bare offer. 

212 

213 :param offers: A list of offers to be sorted. 

214 :param order: A weighted list of offers where items closer to the start of 

215 the list will be a preferred over items closer to the end. 

216 :return: A list of offers sorted first by specificity (higher to lower) 

217 then by ``order``. 

218 

219 """ 

220 if order is None: 

221 order = [] 

222 

223 max_weight = len(offers) 

224 

225 def find_order_index(value, default=None): 

226 return next((i for i, x in enumerate(order) if x == value), default) 

227 

228 def offer_sort_key(value): 

229 """ 

230 (type_weight, params_weight) 

231 

232 type_weight: 

233 - index of specific ``type/subtype`` in order list 

234 - ``max_weight * 2`` if no match is found 

235 

236 params_weight: 

237 - index of specific ``type/subtype;params`` in order list 

238 - ``max_weight`` if not found 

239 - ``max_weight + 1`` if no params at all 

240 

241 """ 

242 parsed = Accept.parse_offer(value) 

243 

244 type_w = find_order_index( 

245 parsed.type + '/' + parsed.subtype, max_weight 

246 ) 

247 

248 if parsed.params: 

249 param_w = find_order_index(value, max_weight) 

250 

251 else: 

252 param_w = max_weight + 1 

253 

254 return (type_w, param_w) 

255 

256 return sorted(offers, key=offer_sort_key)