Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/datastructures/accept.py: 27%
144 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 07:17 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 07:17 +0000
1from __future__ import annotations
3import codecs
4import re
6from .structures import ImmutableList
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.
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:
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
28 To get the quality for an item you can use normal item lookup:
30 >>> print a['utf-8']
31 0.7
32 >>> a['utf7']
33 0
35 .. versionchanged:: 0.5
36 :class:`Accept` objects are forced immutable now.
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.
43 """
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)
59 def _specificity(self, value):
60 """Returns a tuple describing the value's specificity."""
61 return (value != "*",)
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()
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)
76 def quality(self, key):
77 """Returns the quality of the key.
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
88 def __contains__(self, value):
89 for item, _quality in self:
90 if self._value_matches(value, item):
91 return True
92 return False
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}])"
98 def index(self, key):
99 """Get the position of an entry or raise :exc:`ValueError`.
101 :param key: The key to be looked up.
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)
114 def find(self, key):
115 """Get the position of an entry or return -1.
117 :param key: The key to be looked up.
118 """
119 try:
120 return self.index(key)
121 except ValueError:
122 return -1
124 def values(self):
125 """Iterate over all values."""
126 for item in self:
127 yield item[0]
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)
138 def __str__(self):
139 return self.to_header()
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
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.
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
174 @property
175 def best(self):
176 """The best match as value."""
177 if self:
178 return self[0][0]
181_mime_split_re = re.compile(r"/|(?:\s*;\s*)")
184def _normalize_mime(value):
185 return _mime_split_re.split(value.lower())
188class MIMEAccept(Accept):
189 """Like :class:`Accept` but with special methods and behavior for
190 mimetypes.
191 """
193 def _specificity(self, value):
194 return tuple(x != "*" for x in _mime_split_re.split(value))
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
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}")
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:])
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}")
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:])
220 # "*/not-*" from the client is invalid, can't match.
221 if item_type == "*" and item_subtype != "*":
222 return False
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 )
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 )
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
248 @property
249 def accept_json(self):
250 """True if this object accepts JSON."""
251 return "application/json" in self
254_locale_delim_re = re.compile(r"[_-]")
257def _normalize_lang(value):
258 """Process a language tag for matching."""
259 return _locale_delim_re.split(value.lower())
262class LanguageAccept(Accept):
263 """Like :class:`Accept` but with normalization for language tags."""
265 def _value_matches(self, value, item):
266 return item == "*" or _normalize_lang(value) == _normalize_lang(item)
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.
272 Language tags are normalized for the purpose of matching, but
273 are returned unchanged.
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.
280 The default is returned if no exact or fallback match is found.
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)
289 if result is not None:
290 return result
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)
300 if result is not None:
301 return result
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)
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))
313 return default
316class CharsetAccept(Accept):
317 """Like :class:`Accept` but with normalization for charsets."""
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()
326 return item == "*" or _normalize(value) == _normalize(item)