Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/datastructures/accept.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

144 statements  

1from __future__ import annotations 

2 

3import codecs 

4import re 

5 

6from .structures import ImmutableList 

7 

8 

9class Accept(ImmutableList): 

10 """An :class:`Accept` object is just a list subclass for lists of 

11 ``(value, quality)`` tuples. It is automatically sorted by specificity 

12 and quality. 

13 

14 All :class:`Accept` objects work similar to a list but provide extra 

15 functionality for working with the data. Containment checks are 

16 normalized to the rules of that header: 

17 

18 >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)]) 

19 >>> a.best 

20 'ISO-8859-1' 

21 >>> 'iso-8859-1' in a 

22 True 

23 >>> 'UTF8' in a 

24 True 

25 >>> 'utf7' in a 

26 False 

27 

28 To get the quality for an item you can use normal item lookup: 

29 

30 >>> print a['utf-8'] 

31 0.7 

32 >>> a['utf7'] 

33 0 

34 

35 .. versionchanged:: 0.5 

36 :class:`Accept` objects are forced immutable now. 

37 

38 .. versionchanged:: 1.0.0 

39 :class:`Accept` internal values are no longer ordered 

40 alphabetically for equal quality tags. Instead the initial 

41 order is preserved. 

42 

43 """ 

44 

45 def __init__(self, values=()): 

46 if values is None: 

47 list.__init__(self) 

48 self.provided = False 

49 elif isinstance(values, Accept): 

50 self.provided = values.provided 

51 list.__init__(self, values) 

52 else: 

53 self.provided = True 

54 values = sorted( 

55 values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True 

56 ) 

57 list.__init__(self, values) 

58 

59 def _specificity(self, value): 

60 """Returns a tuple describing the value's specificity.""" 

61 return (value != "*",) 

62 

63 def _value_matches(self, value, item): 

64 """Check if a value matches a given accept item.""" 

65 return item == "*" or item.lower() == value.lower() 

66 

67 def __getitem__(self, key): 

68 """Besides index lookup (getting item n) you can also pass it a string 

69 to get the quality for the item. If the item is not in the list, the 

70 returned quality is ``0``. 

71 """ 

72 if isinstance(key, str): 

73 return self.quality(key) 

74 return list.__getitem__(self, key) 

75 

76 def quality(self, key): 

77 """Returns the quality of the key. 

78 

79 .. versionadded:: 0.6 

80 In previous versions you had to use the item-lookup syntax 

81 (eg: ``obj[key]`` instead of ``obj.quality(key)``) 

82 """ 

83 for item, quality in self: 

84 if self._value_matches(key, item): 

85 return quality 

86 return 0 

87 

88 def __contains__(self, value): 

89 for item, _quality in self: 

90 if self._value_matches(value, item): 

91 return True 

92 return False 

93 

94 def __repr__(self): 

95 pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self) 

96 return f"{type(self).__name__}([{pairs_str}])" 

97 

98 def index(self, key): 

99 """Get the position of an entry or raise :exc:`ValueError`. 

100 

101 :param key: The key to be looked up. 

102 

103 .. versionchanged:: 0.5 

104 This used to raise :exc:`IndexError`, which was inconsistent 

105 with the list API. 

106 """ 

107 if isinstance(key, str): 

108 for idx, (item, _quality) in enumerate(self): 

109 if self._value_matches(key, item): 

110 return idx 

111 raise ValueError(key) 

112 return list.index(self, key) 

113 

114 def find(self, key): 

115 """Get the position of an entry or return -1. 

116 

117 :param key: The key to be looked up. 

118 """ 

119 try: 

120 return self.index(key) 

121 except ValueError: 

122 return -1 

123 

124 def values(self): 

125 """Iterate over all values.""" 

126 for item in self: 

127 yield item[0] 

128 

129 def to_header(self): 

130 """Convert the header set into an HTTP header string.""" 

131 result = [] 

132 for value, quality in self: 

133 if quality != 1: 

134 value = f"{value};q={quality}" 

135 result.append(value) 

136 return ",".join(result) 

137 

138 def __str__(self): 

139 return self.to_header() 

140 

141 def _best_single_match(self, match): 

142 for client_item, quality in self: 

143 if self._value_matches(match, client_item): 

144 # self is sorted by specificity descending, we can exit 

145 return client_item, quality 

146 return None 

147 

148 def best_match(self, matches, default=None): 

149 """Returns the best match from a list of possible matches based 

150 on the specificity and quality of the client. If two items have the 

151 same quality and specificity, the one is returned that comes first. 

152 

153 :param matches: a list of matches to check for 

154 :param default: the value that is returned if none match 

155 """ 

156 result = default 

157 best_quality = -1 

158 best_specificity = (-1,) 

159 for server_item in matches: 

160 match = self._best_single_match(server_item) 

161 if not match: 

162 continue 

163 client_item, quality = match 

164 specificity = self._specificity(client_item) 

165 if quality <= 0 or quality < best_quality: 

166 continue 

167 # better quality or same quality but more specific => better match 

168 if quality > best_quality or specificity > best_specificity: 

169 result = server_item 

170 best_quality = quality 

171 best_specificity = specificity 

172 return result 

173 

174 @property 

175 def best(self): 

176 """The best match as value.""" 

177 if self: 

178 return self[0][0] 

179 

180 

181_mime_split_re = re.compile(r"/|(?:\s*;\s*)") 

182 

183 

184def _normalize_mime(value): 

185 return _mime_split_re.split(value.lower()) 

186 

187 

188class MIMEAccept(Accept): 

189 """Like :class:`Accept` but with special methods and behavior for 

190 mimetypes. 

191 """ 

192 

193 def _specificity(self, value): 

194 return tuple(x != "*" for x in _mime_split_re.split(value)) 

195 

196 def _value_matches(self, value, item): 

197 # item comes from the client, can't match if it's invalid. 

198 if "/" not in item: 

199 return False 

200 

201 # value comes from the application, tell the developer when it 

202 # doesn't look valid. 

203 if "/" not in value: 

204 raise ValueError(f"invalid mimetype {value!r}") 

205 

206 # Split the match value into type, subtype, and a sorted list of parameters. 

207 normalized_value = _normalize_mime(value) 

208 value_type, value_subtype = normalized_value[:2] 

209 value_params = sorted(normalized_value[2:]) 

210 

211 # "*/*" is the only valid value that can start with "*". 

212 if value_type == "*" and value_subtype != "*": 

213 raise ValueError(f"invalid mimetype {value!r}") 

214 

215 # Split the accept item into type, subtype, and parameters. 

216 normalized_item = _normalize_mime(item) 

217 item_type, item_subtype = normalized_item[:2] 

218 item_params = sorted(normalized_item[2:]) 

219 

220 # "*/not-*" from the client is invalid, can't match. 

221 if item_type == "*" and item_subtype != "*": 

222 return False 

223 

224 return ( 

225 (item_type == "*" and item_subtype == "*") 

226 or (value_type == "*" and value_subtype == "*") 

227 ) or ( 

228 item_type == value_type 

229 and ( 

230 item_subtype == "*" 

231 or value_subtype == "*" 

232 or (item_subtype == value_subtype and item_params == value_params) 

233 ) 

234 ) 

235 

236 @property 

237 def accept_html(self): 

238 """True if this object accepts HTML.""" 

239 return ( 

240 "text/html" in self or "application/xhtml+xml" in self or self.accept_xhtml 

241 ) 

242 

243 @property 

244 def accept_xhtml(self): 

245 """True if this object accepts XHTML.""" 

246 return "application/xhtml+xml" in self or "application/xml" in self 

247 

248 @property 

249 def accept_json(self): 

250 """True if this object accepts JSON.""" 

251 return "application/json" in self 

252 

253 

254_locale_delim_re = re.compile(r"[_-]") 

255 

256 

257def _normalize_lang(value): 

258 """Process a language tag for matching.""" 

259 return _locale_delim_re.split(value.lower()) 

260 

261 

262class LanguageAccept(Accept): 

263 """Like :class:`Accept` but with normalization for language tags.""" 

264 

265 def _value_matches(self, value, item): 

266 return item == "*" or _normalize_lang(value) == _normalize_lang(item) 

267 

268 def best_match(self, matches, default=None): 

269 """Given a list of supported values, finds the best match from 

270 the list of accepted values. 

271 

272 Language tags are normalized for the purpose of matching, but 

273 are returned unchanged. 

274 

275 If no exact match is found, this will fall back to matching 

276 the first subtag (primary language only), first with the 

277 accepted values then with the match values. This partial is not 

278 applied to any other language subtags. 

279 

280 The default is returned if no exact or fallback match is found. 

281 

282 :param matches: A list of supported languages to find a match. 

283 :param default: The value that is returned if none match. 

284 """ 

285 # Look for an exact match first. If a client accepts "en-US", 

286 # "en-US" is a valid match at this point. 

287 result = super().best_match(matches) 

288 

289 if result is not None: 

290 return result 

291 

292 # Fall back to accepting primary tags. If a client accepts 

293 # "en-US", "en" is a valid match at this point. Need to use 

294 # re.split to account for 2 or 3 letter codes. 

295 fallback = Accept( 

296 [(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self] 

297 ) 

298 result = fallback.best_match(matches) 

299 

300 if result is not None: 

301 return result 

302 

303 # Fall back to matching primary tags. If the client accepts 

304 # "en", "en-US" is a valid match at this point. 

305 fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches] 

306 result = super().best_match(fallback_matches) 

307 

308 # Return a value from the original match list. Find the first 

309 # original value that starts with the matched primary tag. 

310 if result is not None: 

311 return next(item for item in matches if item.startswith(result)) 

312 

313 return default 

314 

315 

316class CharsetAccept(Accept): 

317 """Like :class:`Accept` but with normalization for charsets.""" 

318 

319 def _value_matches(self, value, item): 

320 def _normalize(name): 

321 try: 

322 return codecs.lookup(name).name 

323 except LookupError: 

324 return name.lower() 

325 

326 return item == "*" or _normalize(value) == _normalize(item)