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

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

161 statements  

1from __future__ import annotations 

2 

3import codecs 

4import collections.abc as cabc 

5import re 

6import typing as t 

7 

8from .structures import ImmutableList 

9 

10 

11class Accept(ImmutableList[tuple[str, float]]): 

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

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

14 and quality. 

15 

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

17 functionality for working with the data. Containment checks are 

18 normalized to the rules of that header: 

19 

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

21 >>> a.best 

22 'ISO-8859-1' 

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

24 True 

25 >>> 'UTF8' in a 

26 True 

27 >>> 'utf7' in a 

28 False 

29 

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

31 

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

33 0.7 

34 >>> a['utf7'] 

35 0 

36 

37 .. versionchanged:: 0.5 

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

39 

40 .. versionchanged:: 1.0.0 

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

42 alphabetically for equal quality tags. Instead the initial 

43 order is preserved. 

44 

45 """ 

46 

47 def __init__( 

48 self, values: Accept | cabc.Iterable[tuple[str, float]] | None = () 

49 ) -> None: 

50 if values is None: 

51 super().__init__() 

52 self.provided = False 

53 elif isinstance(values, Accept): 

54 self.provided = values.provided 

55 super().__init__(values) 

56 else: 

57 self.provided = True 

58 values = sorted( 

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

60 ) 

61 super().__init__(values) 

62 

63 def _specificity(self, value: str) -> tuple[bool, ...]: 

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

65 return (value != "*",) 

66 

67 def _value_matches(self, value: str, item: str) -> bool: 

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

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

70 

71 @t.overload 

72 def __getitem__(self, key: str) -> float: ... 

73 @t.overload 

74 def __getitem__(self, key: t.SupportsIndex) -> tuple[str, float]: ... 

75 @t.overload 

76 def __getitem__(self, key: slice) -> list[tuple[str, float]]: ... 

77 def __getitem__( 

78 self, key: str | t.SupportsIndex | slice 

79 ) -> float | tuple[str, float] | list[tuple[str, float]]: 

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

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

82 returned quality is ``0``. 

83 """ 

84 if isinstance(key, str): 

85 return self.quality(key) 

86 return list.__getitem__(self, key) 

87 

88 def quality(self, key: str) -> float: 

89 """Returns the quality of the key. 

90 

91 .. versionadded:: 0.6 

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

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

94 """ 

95 for item, quality in self: 

96 if self._value_matches(key, item): 

97 return quality 

98 return 0 

99 

100 def __contains__(self, value: str) -> bool: # type: ignore[override] 

101 for item, _quality in self: 

102 if self._value_matches(value, item): 

103 return True 

104 return False 

105 

106 def __repr__(self) -> str: 

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

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

109 

110 def index(self, key: str | tuple[str, float]) -> int: # type: ignore[override] 

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

112 

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

114 

115 .. versionchanged:: 0.5 

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

117 with the list API. 

118 """ 

119 if isinstance(key, str): 

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

121 if self._value_matches(key, item): 

122 return idx 

123 raise ValueError(key) 

124 return list.index(self, key) 

125 

126 def find(self, key: str | tuple[str, float]) -> int: 

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

128 

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

130 """ 

131 try: 

132 return self.index(key) 

133 except ValueError: 

134 return -1 

135 

136 def values(self) -> cabc.Iterator[str]: 

137 """Iterate over all values.""" 

138 for item in self: 

139 yield item[0] 

140 

141 def to_header(self) -> str: 

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

143 result = [] 

144 for value, quality in self: 

145 if quality != 1: 

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

147 result.append(value) 

148 return ",".join(result) 

149 

150 def __str__(self) -> str: 

151 return self.to_header() 

152 

153 def _best_single_match(self, match: str) -> tuple[str, float] | None: 

154 for client_item, quality in self: 

155 if self._value_matches(match, client_item): 

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

157 return client_item, quality 

158 return None 

159 

160 @t.overload 

161 def best_match(self, matches: cabc.Iterable[str]) -> str | None: ... 

162 @t.overload 

163 def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ... 

164 def best_match( 

165 self, matches: cabc.Iterable[str], default: str | None = None 

166 ) -> str | None: 

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

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

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

170 

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

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

173 """ 

174 result = default 

175 best_quality: float = -1 

176 best_specificity: tuple[float, ...] = (-1,) 

177 for server_item in matches: 

178 match = self._best_single_match(server_item) 

179 if not match: 

180 continue 

181 client_item, quality = match 

182 specificity = self._specificity(client_item) 

183 if quality <= 0 or quality < best_quality: 

184 continue 

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

186 if quality > best_quality or specificity > best_specificity: 

187 result = server_item 

188 best_quality = quality 

189 best_specificity = specificity 

190 return result 

191 

192 @property 

193 def best(self) -> str | None: 

194 """The best match as value.""" 

195 if self: 

196 return self[0][0] 

197 

198 return None 

199 

200 

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

202 

203 

204def _normalize_mime(value: str) -> list[str]: 

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

206 

207 

208class MIMEAccept(Accept): 

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

210 mimetypes. 

211 """ 

212 

213 def _specificity(self, value: str) -> tuple[bool, ...]: 

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

215 

216 def _value_matches(self, value: str, item: str) -> bool: 

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

218 if "/" not in item: 

219 return False 

220 

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

222 # doesn't look valid. 

223 if "/" not in value: 

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

225 

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

227 normalized_value = _normalize_mime(value) 

228 value_type, value_subtype = normalized_value[:2] 

229 value_params = sorted(normalized_value[2:]) 

230 

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

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

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

234 

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

236 normalized_item = _normalize_mime(item) 

237 item_type, item_subtype = normalized_item[:2] 

238 item_params = sorted(normalized_item[2:]) 

239 

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

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

242 return False 

243 

244 return ( 

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

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

247 ) or ( 

248 item_type == value_type 

249 and ( 

250 item_subtype == "*" 

251 or value_subtype == "*" 

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

253 ) 

254 ) 

255 

256 @property 

257 def accept_html(self) -> bool: 

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

259 return "text/html" in self or self.accept_xhtml # type: ignore[comparison-overlap] 

260 

261 @property 

262 def accept_xhtml(self) -> bool: 

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

264 return "application/xhtml+xml" in self or "application/xml" in self # type: ignore[comparison-overlap] 

265 

266 @property 

267 def accept_json(self) -> bool: 

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

269 return "application/json" in self # type: ignore[comparison-overlap] 

270 

271 

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

273 

274 

275def _normalize_lang(value: str) -> list[str]: 

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

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

278 

279 

280class LanguageAccept(Accept): 

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

282 

283 def _value_matches(self, value: str, item: str) -> bool: 

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

285 

286 @t.overload 

287 def best_match(self, matches: cabc.Iterable[str]) -> str | None: ... 

288 @t.overload 

289 def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ... 

290 def best_match( 

291 self, matches: cabc.Iterable[str], default: str | None = None 

292 ) -> str | None: 

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

294 the list of accepted values. 

295 

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

297 are returned unchanged. 

298 

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

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

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

302 applied to any other language subtags. 

303 

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

305 

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

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

308 """ 

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

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

311 result = super().best_match(matches) 

312 

313 if result is not None: 

314 return result 

315 

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

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

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

319 fallback = Accept( 

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

321 ) 

322 result = fallback.best_match(matches) 

323 

324 if result is not None: 

325 return result 

326 

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

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

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

330 result = super().best_match(fallback_matches) 

331 

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

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

334 if result is not None: 

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

336 

337 return default 

338 

339 

340class CharsetAccept(Accept): 

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

342 

343 def _value_matches(self, value: str, item: str) -> bool: 

344 def _normalize(name: str) -> str: 

345 try: 

346 return codecs.lookup(name).name 

347 except LookupError: 

348 return name.lower() 

349 

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