Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/webob/acceptparse.py : 25%

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
1"""
2Parse four ``Accept*`` headers used in server-driven content negotiation.
4The four headers are ``Accept``, ``Accept-Charset``, ``Accept-Encoding`` and
5``Accept-Language``.
6"""
8from collections import namedtuple
9import re
10import textwrap
11import warnings
14# RFC 7230 Section 3.2.3 "Whitespace"
15# OWS = *( SP / HTAB )
16# ; optional whitespace
17OWS_re = '[ \t]*'
19# RFC 7230 Section 3.2.6 "Field Value Components":
20# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
21# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
22# / DIGIT / ALPHA
23tchar_re = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]"
25# token = 1*tchar
26token_re = tchar_re + '+'
27token_compiled_re = re.compile('^' + token_re + '$')
29# RFC 7231 Section 5.3.1 "Quality Values"
30# qvalue = ( "0" [ "." 0*3DIGIT ] )
31# / ( "1" [ "." 0*3("0") ] )
32qvalue_re = (
33 r'(?:0(?:\.[0-9]{0,3})?)'
34 '|'
35 r'(?:1(?:\.0{0,3})?)'
36)
37# weight = OWS ";" OWS "q=" qvalue
38weight_re = OWS_re + ';' + OWS_re + '[qQ]=(' + qvalue_re + ')'
41def _item_n_weight_re(item_re):
42 return '(' + item_re + ')(?:' + weight_re + ')?'
45def _item_qvalue_pair_to_header_element(pair):
46 item, qvalue = pair
47 if qvalue == 1.0:
48 element = item
49 elif qvalue == 0.0:
50 element = '{};q=0'.format(item)
51 else:
52 element = '{};q={}'.format(item, qvalue)
53 return element
56def _list_0_or_more__compiled_re(element_re):
57 # RFC 7230 Section 7 "ABNF List Extension: #rule":
58 # #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ]
59 return re.compile(
60 '^(?:$)|' +
61 '(?:' +
62 '(?:,|(?:' + element_re + '))' +
63 '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*' +
64 ')$',
65 )
68def _list_1_or_more__compiled_re(element_re):
69 # RFC 7230 Section 7 "ABNF List Extension: #rule":
70 # 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] )
71 # and RFC 7230 Errata ID: 4169
72 return re.compile(
73 '^(?:,' + OWS_re + ')*' + element_re +
74 '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*$',
75 )
78class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])):
79 """
80 A pre-parsed offer tuple represeting a value in the format
81 ``type/subtype;param0=value0;param1=value1``.
83 :ivar type: The media type's root category.
84 :ivar subtype: The media type's subtype.
85 :ivar params: A tuple of 2-tuples containing parameter names and values.
87 """
88 __slots__ = ()
90 def __str__(self):
91 """
92 Return the properly quoted media type string.
94 """
95 value = self.type + '/' + self.subtype
96 return Accept._form_media_range(value, self.params)
99class Accept(object):
100 """
101 Represent an ``Accept`` header.
103 Base class for :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, and
104 :class:`AcceptInvalidHeader`.
105 """
107 # RFC 6838 describes syntax rules for media types that are different to
108 # (and stricter than) those in RFC 7231, but if RFC 7231 intended us to
109 # follow the rules in RFC 6838 for media ranges, it would not have
110 # specified its own syntax rules for media ranges, so it appears we should
111 # use the rules in RFC 7231 for now.
113 # RFC 5234 Appendix B.1 "Core Rules":
114 # VCHAR = %x21-7E
115 # ; visible (printing) characters
116 vchar_re = '\x21-\x7e'
117 # RFC 7230 Section 3.2.6 "Field Value Components":
118 # quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
119 # qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
120 # obs-text = %x80-FF
121 # quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
122 obs_text_re = '\x80-\xff'
123 qdtext_re = '[\t \x21\x23-\x5b\\\x5d-\x7e' + obs_text_re + ']'
124 # The '\\' between \x5b and \x5d is needed to escape \x5d (']')
125 quoted_pair_re = r'\\' + '[\t ' + vchar_re + obs_text_re + ']'
126 quoted_string_re = \
127 '"(?:(?:' + qdtext_re + ')|(?:' + quoted_pair_re + '))*"'
129 # RFC 7231 Section 3.1.1.1 "Media Type":
130 # type = token
131 # subtype = token
132 # parameter = token "=" ( token / quoted-string )
133 type_re = token_re
134 subtype_re = token_re
135 parameter_re = token_re + '=' + \
136 '(?:(?:' + token_re + ')|(?:' + quoted_string_re + '))'
138 # Section 5.3.2 "Accept":
139 # media-range = ( "*/*"
140 # / ( type "/" "*" )
141 # / ( type "/" subtype )
142 # ) *( OWS ";" OWS parameter )
143 media_range_re = (
144 '(' +
145 '(?:' + type_re + '/' + subtype_re + ')' +
146 # '*' is included through type_re and subtype_re, so this covers */*
147 # and type/*
148 ')' +
149 '(' +
150 '(?:' + OWS_re + ';' + OWS_re +
151 '(?![qQ]=)' + # media type parameter cannot be named "q"
152 parameter_re + ')*' +
153 ')'
154 )
155 # accept-params = weight *( accept-ext )
156 # accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
157 accept_ext_re = (
158 OWS_re + ';' + OWS_re + token_re + '(?:' +
159 '=(?:' +
160 '(?:' + token_re + ')|(?:' + quoted_string_re + ')' +
161 ')' +
162 ')?'
163 )
164 accept_params_re = weight_re + '((?:' + accept_ext_re + ')*)'
166 media_range_n_accept_params_re = media_range_re + '(?:' + \
167 accept_params_re + ')?'
168 media_range_n_accept_params_compiled_re = re.compile(
169 media_range_n_accept_params_re,
170 )
172 accept_compiled_re = _list_0_or_more__compiled_re(
173 element_re=media_range_n_accept_params_re,
174 )
176 # For parsing repeated groups within the media type parameters and
177 # extension parameters segments
178 parameters_compiled_re = re.compile(
179 OWS_re + ';' + OWS_re + '(' + token_re + ')=(' + token_re + '|' +
180 quoted_string_re + ')',
181 )
182 accept_ext_compiled_re = re.compile(
183 OWS_re + ';' + OWS_re + '(' + token_re + ')' +
184 '(?:' +
185 '=(' +
186 '(?:' +
187 '(?:' + token_re + ')|(?:' + quoted_string_re + ')' +
188 ')' +
189 ')' +
190 ')?',
191 )
193 # For parsing the media types in the `offers` argument to
194 # .acceptable_offers(), we re-use the media range regex for media types.
195 # This is not intended to be a validation of the offers; its main purpose
196 # is to extract the media type and any media type parameters.
197 media_type_re = media_range_re
198 media_type_compiled_re = re.compile('^' + media_type_re + '$')
200 @classmethod
201 def _escape_and_quote_parameter_value(cls, param_value):
202 """
203 Escape and quote parameter value where necessary.
205 For media type and extension parameter values.
206 """
207 if param_value == '':
208 param_value = '""'
209 else:
210 param_value = param_value.replace('\\', '\\\\').replace(
211 '"', r'\"',
212 )
213 if not token_compiled_re.match(param_value):
214 param_value = '"' + param_value + '"'
215 return param_value
217 @classmethod
218 def _form_extension_params_segment(cls, extension_params):
219 """
220 Convert iterable of extension parameters to str segment for header.
222 `extension_params` is an iterable where each item is either a parameter
223 string or a (name, value) tuple.
224 """
225 extension_params_segment = ''
226 for item in extension_params:
227 try:
228 extension_params_segment += (';' + item)
229 except TypeError:
230 param_name, param_value = item
231 param_value = cls._escape_and_quote_parameter_value(
232 param_value=param_value,
233 )
234 extension_params_segment += (
235 ';' + param_name + '=' + param_value
236 )
237 return extension_params_segment
239 @classmethod
240 def _form_media_range(cls, type_subtype, media_type_params):
241 """
242 Combine `type_subtype` and `media_type_params` to form a media range.
244 `type_subtype` is a ``str``, and `media_type_params` is an iterable of
245 (parameter name, parameter value) tuples.
246 """
247 media_type_params_segment = ''
248 for param_name, param_value in media_type_params:
249 param_value = cls._escape_and_quote_parameter_value(
250 param_value=param_value,
251 )
252 media_type_params_segment += (';' + param_name + '=' + param_value)
253 return type_subtype + media_type_params_segment
255 @classmethod
256 def _iterable_to_header_element(cls, iterable):
257 """
258 Convert iterable of tuples into header element ``str``.
260 Each tuple is expected to be in one of two forms: (media_range, qvalue,
261 extension_params_segment), or (media_range, qvalue).
262 """
263 try:
264 media_range, qvalue, extension_params_segment = iterable
265 except ValueError:
266 media_range, qvalue = iterable
267 extension_params_segment = ''
269 if qvalue == 1.0:
270 if extension_params_segment:
271 element = '{};q=1{}'.format(
272 media_range, extension_params_segment,
273 )
274 else:
275 element = media_range
276 elif qvalue == 0.0:
277 element = '{};q=0{}'.format(media_range, extension_params_segment)
278 else:
279 element = '{};q={}{}'.format(
280 media_range, qvalue, extension_params_segment,
281 )
282 return element
284 @classmethod
285 def _parse_media_type_params(cls, media_type_params_segment):
286 """
287 Parse media type parameters segment into list of (name, value) tuples.
288 """
289 media_type_params = cls.parameters_compiled_re.findall(
290 media_type_params_segment,
291 )
292 for index, (name, value) in enumerate(media_type_params):
293 if value.startswith('"') and value.endswith('"'):
294 value = cls._process_quoted_string_token(token=value)
295 media_type_params[index] = (name, value)
296 return media_type_params
298 @classmethod
299 def _process_quoted_string_token(cls, token):
300 """
301 Return unescaped and unquoted value from quoted token.
302 """
303 # RFC 7230, section 3.2.6 "Field Value Components": "Recipients that
304 # process the value of a quoted-string MUST handle a quoted-pair as if
305 # it were replaced by the octet following the backslash."
306 return re.sub(r'\\(?![\\])', '', token[1:-1]).replace('\\\\', '\\')
308 @classmethod
309 def _python_value_to_header_str(cls, value):
310 """
311 Convert Python value to header string for __add__/__radd__.
312 """
313 if isinstance(value, str):
314 return value
315 if hasattr(value, 'items'):
316 if value == {}:
317 value = []
318 else:
319 value_list = []
320 for media_range, item in value.items():
321 # item is either (media range, (qvalue, extension
322 # parameters segment)), or (media range, qvalue) (supported
323 # for backward compatibility)
324 if isinstance(item, (float, int)):
325 value_list.append((media_range, item, ''))
326 else:
327 value_list.append((media_range, item[0], item[1]))
328 value = sorted(
329 value_list,
330 key=lambda item: item[1], # qvalue
331 reverse=True,
332 )
333 if isinstance(value, (tuple, list)):
334 header_elements = []
335 for item in value:
336 if isinstance(item, (tuple, list)):
337 item = cls._iterable_to_header_element(iterable=item)
338 header_elements.append(item)
339 header_str = ', '.join(header_elements)
340 else:
341 header_str = str(value)
342 return header_str
344 @classmethod
345 def parse(cls, value):
346 """
347 Parse an ``Accept`` header.
349 :param value: (``str``) header value
350 :return: If `value` is a valid ``Accept`` header, returns an iterator
351 of (*media_range*, *qvalue*, *media_type_params*,
352 *extension_params*) tuples, as parsed from the header from
353 left to right.
355 | *media_range* is the media range, including any media type
356 parameters. The media range is returned in a canonicalised
357 form (except the case of the characters are unchanged):
358 unnecessary spaces around the semicolons before media type
359 parameters are removed; the parameter values are returned in
360 a form where only the '``\\``' and '``"``' characters are
361 escaped, and the values are quoted with double quotes only
362 if they need to be quoted.
364 | *qvalue* is the quality value of the media range.
366 | *media_type_params* is the media type parameters, as a list
367 of (parameter name, value) tuples.
369 | *extension_params* is the extension parameters, as a list
370 where each item is either a parameter string or a (parameter
371 name, value) tuple.
372 :raises ValueError: if `value` is an invalid header
373 """
374 # Check if header is valid
375 # Using Python stdlib's `re` module, there is currently no way to check
376 # the match *and* get all the groups using the same regex, so we have
377 # to do this in steps using multiple regexes.
378 if cls.accept_compiled_re.match(value) is None:
379 raise ValueError('Invalid value for an Accept header.')
380 def generator(value):
381 for match in (
382 cls.media_range_n_accept_params_compiled_re.finditer(value)
383 ):
384 groups = match.groups()
386 type_subtype = groups[0]
388 media_type_params = cls._parse_media_type_params(
389 media_type_params_segment=groups[1],
390 )
392 media_range = cls._form_media_range(
393 type_subtype=type_subtype,
394 media_type_params=media_type_params,
395 )
397 # qvalue (groups[2]) and extension_params (groups[3]) are both
398 # None if neither qvalue or extension parameters are found in
399 # the match.
401 qvalue = groups[2]
402 qvalue = float(qvalue) if qvalue else 1.0
404 extension_params = groups[3]
405 if extension_params:
406 extension_params = cls.accept_ext_compiled_re.findall(
407 extension_params,
408 )
409 for index, (token_key, token_value) in enumerate(
410 extension_params
411 ):
412 if token_value:
413 if (
414 token_value.startswith('"') and
415 token_value.endswith('"')
416 ):
417 token_value = cls._process_quoted_string_token(
418 token=token_value,
419 )
420 extension_params[index] = (
421 token_key, token_value,
422 )
423 else:
424 extension_params[index] = token_key
425 else:
426 extension_params = []
428 yield (
429 media_range, qvalue, media_type_params, extension_params,
430 )
431 return generator(value=value)
433 @classmethod
434 def parse_offer(cls, offer):
435 """
436 Parse an offer into its component parts.
438 :param offer: A media type or range in the format
439 ``type/subtype[;params]``.
440 :return: A named tuple containing ``(*type*, *subtype*, *params*)``.
442 | *params* is a list containing ``(*parameter name*, *value*)``
443 values.
445 :raises ValueError: If the offer does not match the required format.
447 """
448 if isinstance(offer, AcceptOffer):
449 return offer
450 match = cls.media_type_compiled_re.match(offer)
451 if not match:
452 raise ValueError('Invalid value for an Accept offer.')
454 groups = match.groups()
455 offer_type, offer_subtype = groups[0].split('/')
456 offer_params = cls._parse_media_type_params(
457 media_type_params_segment=groups[1],
458 )
459 if offer_type == '*' or offer_subtype == '*':
460 raise ValueError('Invalid value for an Accept offer.')
461 return AcceptOffer(
462 offer_type.lower(),
463 offer_subtype.lower(),
464 tuple((name.lower(), value) for name, value in offer_params),
465 )
467 @classmethod
468 def _parse_and_normalize_offers(cls, offers):
469 """
470 Throw out any offers that do not match the media range ABNF.
472 :return: A list of offers split into the format ``[offer_index,
473 parsed_offer]``.
475 """
476 parsed_offers = []
477 for index, offer in enumerate(offers):
478 try:
479 parsed_offer = cls.parse_offer(offer)
480 except ValueError:
481 continue
482 parsed_offers.append([index, parsed_offer])
483 return parsed_offers
486class AcceptValidHeader(Accept):
487 """
488 Represent a valid ``Accept`` header.
490 A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.2
491 <7231#section-5.3.2>`.
493 This object should not be modified. To add to the header, we can use the
494 addition operators (``+`` and ``+=``), which return a new object (see the
495 docstring for :meth:`AcceptValidHeader.__add__`).
496 """
498 @property
499 def header_value(self):
500 """(``str`` or ``None``) The header value."""
501 return self._header_value
503 @property
504 def parsed(self):
505 """
506 (``list`` or ``None``) Parsed form of the header.
508 A list of (*media_range*, *qvalue*, *media_type_params*,
509 *extension_params*) tuples, where
511 *media_range* is the media range, including any media type parameters.
512 The media range is returned in a canonicalised form (except the case of
513 the characters are unchanged): unnecessary spaces around the semicolons
514 before media type parameters are removed; the parameter values are
515 returned in a form where only the '``\\``' and '``"``' characters are
516 escaped, and the values are quoted with double quotes only if they need
517 to be quoted.
519 *qvalue* is the quality value of the media range.
521 *media_type_params* is the media type parameters, as a list of
522 (parameter name, value) tuples.
524 *extension_params* is the extension parameters, as a list where each
525 item is either a parameter string or a (parameter name, value) tuple.
526 """
527 return self._parsed
529 def __init__(self, header_value):
530 """
531 Create an :class:`AcceptValidHeader` instance.
533 :param header_value: (``str``) header value.
534 :raises ValueError: if `header_value` is an invalid value for an
535 ``Accept`` header.
536 """
537 self._header_value = header_value
538 self._parsed = list(self.parse(header_value))
539 self._parsed_nonzero = [item for item in self.parsed if item[1]]
540 # item[1] is the qvalue
542 def copy(self):
543 """
544 Create a copy of the header object.
546 """
547 return self.__class__(self._header_value)
549 def __add__(self, other):
550 """
551 Add to header, creating a new header object.
553 `other` can be:
555 * ``None``
556 * a ``str`` header value
557 * a ``dict``, with media ranges ``str``'s (including any media type
558 parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
559 *extension_params*) tuples as values, where *extension_params* is a
560 ``str`` of the extension parameters segment of the header element,
561 starting with the first '``;``'
562 * a ``tuple`` or ``list``, where each item is either a header element
563 ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
564 or ``list`` where *media_range* is a ``str`` of the media range
565 including any media type parameters, and *extension_params* is a
566 ``str`` of the extension parameters segment of the header element,
567 starting with the first '``;``'
568 * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
569 :class:`AcceptInvalidHeader` instance
570 * object of any other type that returns a value for ``__str__``
572 If `other` is a valid header value or another
573 :class:`AcceptValidHeader` instance, and the header value it represents
574 is not `''`, then the two header values are joined with ``', '``, and a
575 new :class:`AcceptValidHeader` instance with the new header value is
576 returned.
578 If `other` is a valid header value or another
579 :class:`AcceptValidHeader` instance representing a header value of
580 `''`; or if it is ``None`` or an :class:`AcceptNoHeader` instance; or
581 if it is an invalid header value, or an :class:`AcceptInvalidHeader`
582 instance, then a new :class:`AcceptValidHeader` instance with the same
583 header value as ``self`` is returned.
584 """
585 if isinstance(other, AcceptValidHeader):
586 if other.header_value == '':
587 return self.__class__(header_value=self.header_value)
588 else:
589 return create_accept_header(
590 header_value=self.header_value + ', ' + other.header_value,
591 )
593 if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
594 return self.__class__(header_value=self.header_value)
596 return self._add_instance_and_non_accept_type(
597 instance=self, other=other,
598 )
600 def __bool__(self):
601 """
602 Return whether ``self`` represents a valid ``Accept`` header.
604 Return ``True`` if ``self`` represents a valid header, and ``False`` if
605 it represents an invalid header, or the header not being in the
606 request.
608 For this class, it always returns ``True``.
609 """
610 return True
611 __nonzero__ = __bool__ # Python 2
613 def __contains__(self, offer):
614 """
615 Return ``bool`` indicating whether `offer` is acceptable.
617 .. warning::
619 The behavior of :meth:`AcceptValidHeader.__contains__` is currently
620 being maintained for backward compatibility, but it will change in
621 the future to better conform to the RFC.
623 :param offer: (``str``) media type offer
624 :return: (``bool``) Whether ``offer`` is acceptable according to the
625 header.
627 This uses the old criterion of a match in
628 :meth:`AcceptValidHeader._old_match`, which is not as specified in
629 :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
630 correctly take into account media type parameters:
632 >>> 'text/html;p=1' in AcceptValidHeader('text/html')
633 False
635 or media ranges with ``q=0`` in the header::
637 >>> 'text/html' in AcceptValidHeader('text/*, text/html;q=0')
638 True
639 >>> 'text/html' in AcceptValidHeader('text/html;q=0, */*')
640 True
642 (See the docstring for :meth:`AcceptValidHeader._old_match` for other
643 problems with the old criterion for matching.)
644 """
645 warnings.warn(
646 'The behavior of AcceptValidHeader.__contains__ is '
647 'currently being maintained for backward compatibility, but it '
648 'will change in the future to better conform to the RFC.',
649 DeprecationWarning,
650 )
651 for (
652 media_range, quality, media_type_params, extension_params
653 ) in self._parsed_nonzero:
654 if self._old_match(media_range, offer):
655 return True
656 return False
658 def __iter__(self):
659 """
660 Return all the ranges with non-0 qvalues, in order of preference.
662 .. warning::
664 The behavior of this method is currently maintained for backward
665 compatibility, but will change in the future.
667 :return: iterator of all the media ranges in the header with non-0
668 qvalues, in descending order of qvalue. If two ranges have the
669 same qvalue, they are returned in the order of their positions
670 in the header, from left to right.
672 Please note that this is a simple filter for the ranges in the header
673 with non-0 qvalues, and is not necessarily the same as what the client
674 prefers, e.g. ``'audio/basic;q=0, */*'`` means 'everything but
675 audio/basic', but ``list(instance)`` would return only ``['*/*']``.
676 """
677 warnings.warn(
678 'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
679 'maintained for backward compatibility, but will change in the '
680 'future.',
681 DeprecationWarning,
682 )
684 for media_range, qvalue, media_type_params, extension_params in sorted(
685 self._parsed_nonzero,
686 key=lambda i: i[1],
687 reverse=True
688 ):
689 yield media_range
691 def __radd__(self, other):
692 """
693 Add to header, creating a new header object.
695 See the docstring for :meth:`AcceptValidHeader.__add__`.
696 """
697 return self._add_instance_and_non_accept_type(
698 instance=self, other=other, instance_on_the_right=True,
699 )
701 def __repr__(self):
702 return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
704 def __str__(self):
705 r"""
706 Return a tidied up version of the header value.
708 e.g. If ``self.header_value`` is ``r',,text/html ; p1="\"\1\"" ;
709 q=0.50; e1=1 ;e2 , text/plain ,'``, ``str(instance)`` returns
710 ``r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain'``.
711 """
712 # self.parsed tuples are in the form: (media_range, qvalue,
713 # media_type_params, extension_params)
714 # self._iterable_to_header_element() requires iterable to be in the
715 # form: (media_range, qvalue, extension_params_segment).
716 return ', '.join(
717 self._iterable_to_header_element(
718 iterable=(
719 tuple_[0], # media_range
720 tuple_[1], # qvalue
721 self._form_extension_params_segment(
722 extension_params=tuple_[3], # extension_params
723 )
724 ),
725 ) for tuple_ in self.parsed
726 )
728 def _add_instance_and_non_accept_type(
729 self, instance, other, instance_on_the_right=False,
730 ):
731 if not other:
732 return self.__class__(header_value=instance.header_value)
734 other_header_value = self._python_value_to_header_str(value=other)
736 if other_header_value == '':
737 # if ``other`` is an object whose type we don't recognise, and
738 # str(other) returns ''
739 return self.__class__(header_value=instance.header_value)
741 try:
742 self.parse(value=other_header_value)
743 except ValueError: # invalid header value
744 return self.__class__(header_value=instance.header_value)
746 new_header_value = (
747 (other_header_value + ', ' + instance.header_value)
748 if instance_on_the_right
749 else (instance.header_value + ', ' + other_header_value)
750 )
751 return self.__class__(header_value=new_header_value)
753 def _old_match(self, mask, offer):
754 """
755 Check if the offer is covered by the mask
757 ``offer`` may contain wildcards to facilitate checking if a ``mask``
758 would match a 'permissive' offer.
760 Wildcard matching forces the match to take place against the type or
761 subtype of the mask and offer (depending on where the wildcard matches)
763 .. warning::
765 This is maintained for backward compatibility, and will be
766 deprecated in the future.
768 This method was WebOb's old criterion for deciding whether a media type
769 matches a media range, used in
771 - :meth:`AcceptValidHeader.__contains__`
772 - :meth:`AcceptValidHeader.best_match`
773 - :meth:`AcceptValidHeader.quality`
775 It allows offers of *, */*, type/*, */subtype and types with no
776 subtypes, which are not media types as specified in :rfc:`RFC 7231,
777 section 5.3.2 <7231#section-5.3.2>`. This is also undocumented in any
778 of the public APIs that uses this method.
779 """
780 # Match if comparisons are the same or either is a complete wildcard
781 if (mask.lower() == offer.lower() or
782 '*/*' in (mask, offer) or
783 '*' == offer):
784 return True
786 # Set mask type with wildcard subtype for malformed masks
787 try:
788 mask_type, mask_subtype = [x.lower() for x in mask.split('/')]
789 except ValueError:
790 mask_type = mask
791 mask_subtype = '*'
793 # Set offer type with wildcard subtype for malformed offers
794 try:
795 offer_type, offer_subtype = [x.lower() for x in offer.split('/')]
796 except ValueError:
797 offer_type = offer
798 offer_subtype = '*'
800 if mask_subtype == '*':
801 # match on type only
802 if offer_type == '*':
803 return True
804 else:
805 return mask_type.lower() == offer_type.lower()
807 if mask_type == '*':
808 # match on subtype only
809 if offer_subtype == '*':
810 return True
811 else:
812 return mask_subtype.lower() == offer_subtype.lower()
814 if offer_subtype == '*':
815 # match on type only
816 return mask_type.lower() == offer_type.lower()
818 if offer_type == '*':
819 # match on subtype only
820 return mask_subtype.lower() == offer_subtype.lower()
822 return offer.lower() == mask.lower()
824 def accept_html(self):
825 """
826 Return ``True`` if any HTML-like type is accepted.
828 The HTML-like types are 'text/html', 'application/xhtml+xml',
829 'application/xml' and 'text/xml'.
830 """
831 return bool(
832 self.acceptable_offers(
833 offers=[
834 'text/html',
835 'application/xhtml+xml',
836 'application/xml',
837 'text/xml',
838 ],
839 )
840 )
841 accepts_html = property(fget=accept_html, doc=accept_html.__doc__)
842 # note the plural
844 def acceptable_offers(self, offers):
845 """
846 Return the offers that are acceptable according to the header.
848 The offers are returned in descending order of preference, where
849 preference is indicated by the qvalue of the media range in the header
850 that best matches the offer.
852 This uses the matching rules described in :rfc:`RFC 7231, section 5.3.2
853 <7231#section-5.3.2>`.
855 Any offers that cannot be parsed via
856 :meth:`.Accept.parse_offer` will be ignored.
858 :param offers: ``iterable`` of ``str`` media types (media types can
859 include media type parameters) or pre-parsed instances
860 of :class:`.AcceptOffer`.
861 :return: A list of tuples of the form (media type, qvalue), in
862 descending order of qvalue. Where two offers have the same
863 qvalue, they are returned in the same order as their order in
864 `offers`.
865 """
866 parsed = self.parsed
868 # RFC 7231, section 3.1.1.1 "Media Type":
869 # "The type, subtype, and parameter name tokens are case-insensitive.
870 # Parameter values might or might not be case-sensitive, depending on
871 # the semantics of the parameter name."
872 lowercased_ranges = [
873 (
874 media_range.partition(';')[0].lower(),
875 qvalue,
876 tuple(
877 (name.lower(), value)
878 for name, value in media_type_params
879 ),
880 )
881 for media_range, qvalue, media_type_params, __ in
882 parsed
883 ]
884 lowercased_offers_parsed = self._parse_and_normalize_offers(offers)
886 acceptable_offers_n_quality_factors = {}
887 for offer_index, parsed_offer in lowercased_offers_parsed:
888 offer = offers[offer_index]
889 offer_type, offer_subtype, offer_media_type_params = parsed_offer
890 for (
891 range_type_subtype, range_qvalue, range_media_type_params,
892 ) in lowercased_ranges:
893 range_type, range_subtype = range_type_subtype.split('/', 1)
895 # The specificity values below are based on the list in the
896 # example in RFC 7231 section 5.3.2 explaining how "media
897 # ranges can be overridden by more specific media ranges or
898 # specific media types". We assign specificity to the list
899 # items in reverse order, so specificity 4, 3, 2, 1 correspond
900 # to 1, 2, 3, 4 in the list, respectively (so that higher
901 # specificity has higher precedence).
902 if (
903 offer_type == range_type
904 and offer_subtype == range_subtype
905 ):
906 if range_media_type_params == ():
907 # If offer_media_type_params == () the offer and the
908 # range match exactly, with neither having media type
909 # parameters.
910 # If offer_media_type_params is not (), the offer and
911 # the range are a match. See the table towards the end
912 # of RFC 7231 section 5.3.2, where the media type
913 # 'text/html;level=3' matches the range 'text/html' in
914 # the header.
915 # Both cases are a match with a specificity of 3.
916 specificity = 3
917 elif offer_media_type_params == range_media_type_params:
918 specificity = 4
919 else: # pragma: no cover
920 # no cover because of
921 # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement
922 continue
923 else:
924 if range_subtype == '*' and offer_type == range_type:
925 specificity = 2
926 elif range_type_subtype == '*/*':
927 specificity = 1
928 else: # pragma: no cover
929 # no cover because of
930 # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement
931 continue
932 try:
933 if specificity <= (
934 acceptable_offers_n_quality_factors[offer][2]
935 ):
936 continue
937 except KeyError:
938 # the entry for the offer is not already in
939 # acceptable_offers_n_quality_factors
940 pass
941 acceptable_offers_n_quality_factors[offer] = (
942 range_qvalue, # qvalue of matched range
943 offer_index,
944 specificity, # specifity of matched range
945 )
947 acceptable_offers_n_quality_factors = [
948 # key is offer, value[0] is qvalue, value[1] is offer_index
949 (key, value[0], value[1])
950 for key, value in acceptable_offers_n_quality_factors.items()
951 if value[0] # != 0.0
952 # We have to filter out the offers with qvalues of 0 here instead
953 # of just skipping them early in the large ``for`` loop because
954 # that would not work for e.g. when the header is 'text/html;q=0,
955 # text/html' (which does not make sense, but is nonetheless valid),
956 # and offers is ['text/html']
957 ]
958 # sort by offer_index, ascending
959 acceptable_offers_n_quality_factors.sort(key=lambda tuple_: tuple_[2])
960 # (stable) sort by qvalue, descending
961 acceptable_offers_n_quality_factors.sort(
962 key=lambda tuple_: tuple_[1], reverse=True,
963 )
964 # drop offer_index
965 acceptable_offers_n_quality_factors = [
966 (item[0], item[1]) for item in acceptable_offers_n_quality_factors
967 ]
968 return acceptable_offers_n_quality_factors
969 # If a media range is repeated in the header (which would not make
970 # sense, but would be valid according to the rules in the RFC), an
971 # offer for which the media range is the most specific match would take
972 # its qvalue from the first appearance of the range in the header.
974 def best_match(self, offers, default_match=None):
975 """
976 Return the best match from the sequence of media type `offers`.
978 .. warning::
980 This is currently maintained for backward compatibility, and will be
981 deprecated in the future.
983 :meth:`AcceptValidHeader.best_match` uses its own algorithm (one not
984 specified in :rfc:`RFC 7231 <7231>`) to determine what is a best
985 match. The algorithm has many issues, and does not conform to
986 :rfc:`RFC 7231 <7231>`.
988 Each media type in `offers` is checked against each non-``q=0`` range
989 in the header. If the two are a match according to WebOb's old
990 criterion for a match, the quality value of the match is the qvalue of
991 the media range from the header multiplied by the server quality value
992 of the offer (if the server quality value is not supplied, it is 1).
994 The offer in the match with the highest quality value is the best
995 match. If there is more than one match with the highest qvalue, the
996 match where the media range has a lower number of '*'s is the best
997 match. If the two have the same number of '*'s, the one that shows up
998 first in `offers` is the best match.
1000 :param offers: (iterable)
1002 | Each item in the iterable may be a ``str`` media type,
1003 or a (media type, server quality value) ``tuple`` or
1004 ``list``. (The two may be mixed in the iterable.)
1006 :param default_match: (optional, any type) the value to be returned if
1007 there is no match
1009 :return: (``str``, or the type of `default_match`)
1011 | The offer that is the best match. If there is no match, the
1012 value of `default_match` is returned.
1014 This uses the old criterion of a match in
1015 :meth:`AcceptValidHeader._old_match`, which is not as specified in
1016 :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
1017 correctly take into account media type parameters:
1019 >>> instance = AcceptValidHeader('text/html')
1020 >>> instance.best_match(offers=['text/html;p=1']) is None
1021 True
1023 or media ranges with ``q=0`` in the header::
1025 >>> instance = AcceptValidHeader('text/*, text/html;q=0')
1026 >>> instance.best_match(offers=['text/html'])
1027 'text/html'
1029 >>> instance = AcceptValidHeader('text/html;q=0, */*')
1030 >>> instance.best_match(offers=['text/html'])
1031 'text/html'
1033 (See the docstring for :meth:`AcceptValidHeader._old_match` for other
1034 problems with the old criterion for matching.)
1036 Another issue is that this method considers the best matching range for
1037 an offer to be the matching range with the highest quality value,
1038 (where quality values are tied, the most specific media range is
1039 chosen); whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`
1040 specifies that we should consider the best matching range for a media
1041 type offer to be the most specific matching range.::
1043 >>> instance = AcceptValidHeader('text/html;q=0.5, text/*')
1044 >>> instance.best_match(offers=['text/html', 'text/plain'])
1045 'text/html'
1046 """
1047 warnings.warn(
1048 'The behavior of AcceptValidHeader.best_match is currently being '
1049 'maintained for backward compatibility, but it will be deprecated'
1050 ' in the future, as it does not conform to the RFC.',
1051 DeprecationWarning,
1052 )
1053 best_quality = -1
1054 best_offer = default_match
1055 matched_by = '*/*'
1056 for offer in offers:
1057 if isinstance(offer, (tuple, list)):
1058 offer, server_quality = offer
1059 else:
1060 server_quality = 1
1061 for item in self._parsed_nonzero:
1062 mask = item[0]
1063 quality = item[1]
1064 possible_quality = server_quality * quality
1065 if possible_quality < best_quality:
1066 continue
1067 elif possible_quality == best_quality:
1068 # 'text/plain' overrides 'message/*' overrides '*/*'
1069 # (if all match w/ the same q=)
1070 if matched_by.count('*') <= mask.count('*'):
1071 continue
1072 if self._old_match(mask, offer):
1073 best_quality = possible_quality
1074 best_offer = offer
1075 matched_by = mask
1076 return best_offer
1078 def quality(self, offer):
1079 """
1080 Return quality value of given offer, or ``None`` if there is no match.
1082 .. warning::
1084 This is currently maintained for backward compatibility, and will be
1085 deprecated in the future.
1087 :param offer: (``str``) media type offer
1088 :return: (``float`` or ``None``)
1090 | The highest quality value from the media range(s) that match
1091 the `offer`, or ``None`` if there is no match.
1093 This uses the old criterion of a match in
1094 :meth:`AcceptValidHeader._old_match`, which is not as specified in
1095 :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
1096 correctly take into account media type parameters:
1098 >>> instance = AcceptValidHeader('text/html')
1099 >>> instance.quality('text/html;p=1') is None
1100 True
1102 or media ranges with ``q=0`` in the header::
1104 >>> instance = AcceptValidHeader('text/*, text/html;q=0')
1105 >>> instance.quality('text/html')
1106 1.0
1107 >>> AcceptValidHeader('text/html;q=0, */*').quality('text/html')
1108 1.0
1110 (See the docstring for :meth:`AcceptValidHeader._old_match` for other
1111 problems with the old criterion for matching.)
1113 Another issue is that this method considers the best matching range for
1114 an offer to be the matching range with the highest quality value,
1115 whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` specifies
1116 that we should consider the best matching range for a media type offer
1117 to be the most specific matching range.::
1119 >>> instance = AcceptValidHeader('text/html;q=0.5, text/*')
1120 >>> instance.quality('text/html')
1121 1.0
1122 """
1123 warnings.warn(
1124 'The behavior of AcceptValidHeader.quality is currently being '
1125 'maintained for backward compatibility, but it will be deprecated '
1126 'in the future, as it does not conform to the RFC.',
1127 DeprecationWarning,
1128 )
1129 bestq = 0
1130 for item in self.parsed:
1131 media_range = item[0]
1132 qvalue = item[1]
1133 if self._old_match(media_range, offer):
1134 bestq = max(bestq, qvalue)
1135 return bestq or None
1138class MIMEAccept(Accept):
1139 """
1140 Backwards compatibility shim for the new functionality provided by
1141 AcceptValidHeader, AcceptInvalidHeader, or AcceptNoHeader, that acts like
1142 the old MIMEAccept from WebOb version 1.7 or lower.
1144 This shim does use the newer Accept header parsing, which will mean your
1145 application may be less liberal in what Accept headers are correctly
1146 parsed. It is recommended that user agents be updated to send appropriate
1147 Accept headers that are valid according to rfc:`RFC 7231, section 5.3.2
1148 <7231#section-5.3.2>`
1150 .. deprecated:: 1.8
1152 Instead of directly creating the Accept object, please see:
1153 :func:`create_accept_header(header_value)
1154 <webob.acceptparse.create_accept_header>`, which will create the
1155 appropriate object.
1157 This shim has an extended deprecation period to allow for application
1158 developers to switch the to new API.
1160 """
1162 def __init__(self, header_value):
1163 warnings.warn(
1164 'The MIMEAccept class has been replaced by '
1165 'webob.acceptparse.create_accept_header. This compatibility shim '
1166 'will be deprecated in a future version of WebOb.',
1167 DeprecationWarning
1168 )
1169 self._accept = create_accept_header(header_value)
1170 if self._accept.parsed:
1171 self._parsed = [(media, q) for (media, q, _, _) in self._accept.parsed]
1172 self._parsed_nonzero = [(m, q) for (m, q) in self._parsed if q]
1173 else:
1174 self._parsed = []
1175 self._parsed_nonzero = []
1177 @staticmethod
1178 def parse(value):
1179 try:
1180 parsed_accepted = Accept.parse(value)
1182 for (media, q, _, _) in parsed_accepted:
1183 yield (media, q)
1184 except ValueError:
1185 pass
1187 def __repr__(self):
1188 return self._accept.__repr__()
1190 def __iter__(self):
1191 return self._accept.__iter__()
1193 def __str__(self):
1194 return self._accept.__str__()
1196 def __add__(self, other):
1197 if isinstance(other, self.__class__):
1198 return self.__class__(str(self._accept.__add__(other._accept)))
1199 else:
1200 return self.__class__(str(self._accept.__add__(other)))
1202 def __radd__(self, other):
1203 return self.__class__(str(self._accept.__radd__(other)))
1205 def __contains__(self, offer):
1206 return offer in self._accept
1208 def quality(self, offer):
1209 return self._accept.quality(offer)
1211 def best_match(self, offers, default_match=None):
1212 return self._accept.best_match(offers, default_match=default_match)
1214 def accept_html(self):
1215 return self._accept.accept_html()
1218class _AcceptInvalidOrNoHeader(Accept):
1219 """
1220 Represent when an ``Accept`` header is invalid or not in request.
1222 This is the base class for the behaviour that :class:`.AcceptInvalidHeader`
1223 and :class:`.AcceptNoHeader` have in common.
1225 :rfc:`7231` does not provide any guidance on what should happen if the
1226 ``Accept`` header has an invalid value. This implementation disregards the
1227 header when the header is invalid, so :class:`.AcceptInvalidHeader` and
1228 :class:`.AcceptNoHeader` have much behaviour in common.
1229 """
1231 def __bool__(self):
1232 """
1233 Return whether ``self`` represents a valid ``Accept`` header.
1235 Return ``True`` if ``self`` represents a valid header, and ``False`` if
1236 it represents an invalid header, or the header not being in the
1237 request.
1239 For this class, it always returns ``False``.
1240 """
1241 return False
1242 __nonzero__ = __bool__ # Python 2
1244 def __contains__(self, offer):
1245 """
1246 Return ``bool`` indicating whether `offer` is acceptable.
1248 .. warning::
1250 The behavior of ``.__contains__`` for the ``Accept`` classes is
1251 currently being maintained for backward compatibility, but it will
1252 change in the future to better conform to the RFC.
1254 :param offer: (``str``) media type offer
1255 :return: (``bool``) Whether ``offer`` is acceptable according to the
1256 header.
1258 For this class, either there is no ``Accept`` header in the request, or
1259 the header is invalid, so any media type is acceptable, and this always
1260 returns ``True``.
1261 """
1262 warnings.warn(
1263 'The behavior of .__contains__ for the Accept classes is '
1264 'currently being maintained for backward compatibility, but it '
1265 'will change in the future to better conform to the RFC.',
1266 DeprecationWarning,
1267 )
1268 return True
1270 def __iter__(self):
1271 """
1272 Return all the ranges with non-0 qvalues, in order of preference.
1274 .. warning::
1276 The behavior of this method is currently maintained for backward
1277 compatibility, but will change in the future.
1279 :return: iterator of all the media ranges in the header with non-0
1280 qvalues, in descending order of qvalue. If two ranges have the
1281 same qvalue, they are returned in the order of their positions
1282 in the header, from left to right.
1284 When there is no ``Accept`` header in the request or the header is
1285 invalid, there are no media ranges, so this always returns an empty
1286 iterator.
1287 """
1288 warnings.warn(
1289 'The behavior of AcceptValidHeader.__iter__ is currently '
1290 'maintained for backward compatibility, but will change in the '
1291 'future.',
1292 DeprecationWarning,
1293 )
1294 return iter(())
1296 def accept_html(self):
1297 """
1298 Return ``True`` if any HTML-like type is accepted.
1300 The HTML-like types are 'text/html', 'application/xhtml+xml',
1301 'application/xml' and 'text/xml'.
1303 When the header is invalid, or there is no `Accept` header in the
1304 request, all `offers` are considered acceptable, so this always returns
1305 ``True``.
1306 """
1307 return bool(
1308 self.acceptable_offers(
1309 offers=[
1310 'text/html',
1311 'application/xhtml+xml',
1312 'application/xml',
1313 'text/xml',
1314 ],
1315 )
1316 )
1317 accepts_html = property(fget=accept_html, doc=accept_html.__doc__)
1318 # note the plural
1320 def acceptable_offers(self, offers):
1321 """
1322 Return the offers that are acceptable according to the header.
1324 Any offers that cannot be parsed via
1325 :meth:`.Accept.parse_offer` will be ignored.
1327 :param offers: ``iterable`` of ``str`` media types (media types can
1328 include media type parameters)
1329 :return: When the header is invalid, or there is no ``Accept`` header
1330 in the request, all `offers` are considered acceptable, so
1331 this method returns a list of (media type, qvalue) tuples
1332 where each offer in `offers` is paired with the qvalue of 1.0,
1333 in the same order as in `offers`.
1334 """
1335 return [
1336 (offers[offer_index], 1.0)
1337 for offer_index, _
1338 # avoid returning any offers that don't match the grammar so
1339 # that the return values here are consistent with what would be
1340 # returned in AcceptValidHeader
1341 in self._parse_and_normalize_offers(offers)
1342 ]
1344 def best_match(self, offers, default_match=None):
1345 """
1346 Return the best match from the sequence of language tag `offers`.
1348 This is the ``.best_match()`` method for when the header is invalid or
1349 not found in the request, corresponding to
1350 :meth:`AcceptValidHeader.best_match`.
1352 .. warning::
1354 This is currently maintained for backward compatibility, and will be
1355 deprecated in the future (see the documentation for
1356 :meth:`AcceptValidHeader.best_match`).
1358 When the header is invalid, or there is no `Accept` header in the
1359 request, all `offers` are considered acceptable, so the best match is
1360 the media type in `offers` with the highest server quality value (if
1361 the server quality value is not supplied for a media type, it is 1).
1363 If more than one media type in `offers` have the same highest server
1364 quality value, then the one that shows up first in `offers` is the best
1365 match.
1367 :param offers: (iterable)
1369 | Each item in the iterable may be a ``str`` media type,
1370 or a (media type, server quality value) ``tuple`` or
1371 ``list``. (The two may be mixed in the iterable.)
1373 :param default_match: (optional, any type) the value to be returned if
1374 `offers` is empty.
1376 :return: (``str``, or the type of `default_match`)
1378 | The offer that has the highest server quality value. If
1379 `offers` is empty, the value of `default_match` is returned.
1380 """
1381 warnings.warn(
1382 'The behavior of .best_match for the Accept classes is currently '
1383 'being maintained for backward compatibility, but the method will'
1384 ' be deprecated in the future, as its behavior is not specified '
1385 'in (and currently does not conform to) RFC 7231.',
1386 DeprecationWarning,
1387 )
1388 best_quality = -1
1389 best_offer = default_match
1390 for offer in offers:
1391 if isinstance(offer, (list, tuple)):
1392 offer, quality = offer
1393 else:
1394 quality = 1
1395 if quality > best_quality:
1396 best_offer = offer
1397 best_quality = quality
1398 return best_offer
1400 def quality(self, offer):
1401 """
1402 Return quality value of given offer, or ``None`` if there is no match.
1404 This is the ``.quality()`` method for when the header is invalid or not
1405 found in the request, corresponding to
1406 :meth:`AcceptValidHeader.quality`.
1408 .. warning::
1410 This is currently maintained for backward compatibility, and will be
1411 deprecated in the future (see the documentation for
1412 :meth:`AcceptValidHeader.quality`).
1414 :param offer: (``str``) media type offer
1415 :return: (``float``) ``1.0``.
1417 When the ``Accept`` header is invalid or not in the request, all offers
1418 are equally acceptable, so 1.0 is always returned.
1419 """
1420 warnings.warn(
1421 'The behavior of .quality for the Accept classes is currently '
1422 'being maintained for backward compatibility, but the method will'
1423 ' be deprecated in the future, as its behavior does not conform to'
1424 'RFC 7231.',
1425 DeprecationWarning,
1426 )
1427 return 1.0
1430class AcceptNoHeader(_AcceptInvalidOrNoHeader):
1431 """
1432 Represent when there is no ``Accept`` header in the request.
1434 This object should not be modified. To add to the header, we can use the
1435 addition operators (``+`` and ``+=``), which return a new object (see the
1436 docstring for :meth:`AcceptNoHeader.__add__`).
1437 """
1439 @property
1440 def header_value(self):
1441 """
1442 (``str`` or ``None``) The header value.
1444 As there is no header in the request, this is ``None``.
1445 """
1446 return self._header_value
1448 @property
1449 def parsed(self):
1450 """
1451 (``list`` or ``None``) Parsed form of the header.
1453 As there is no header in the request, this is ``None``.
1454 """
1455 return self._parsed
1457 def __init__(self):
1458 """
1459 Create an :class:`AcceptNoHeader` instance.
1460 """
1461 self._header_value = None
1462 self._parsed = None
1463 self._parsed_nonzero = None
1465 def copy(self):
1466 """
1467 Create a copy of the header object.
1469 """
1470 return self.__class__()
1472 def __add__(self, other):
1473 """
1474 Add to header, creating a new header object.
1476 `other` can be:
1478 * ``None``
1479 * a ``str`` header value
1480 * a ``dict``, with media ranges ``str``'s (including any media type
1481 parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
1482 *extension_params*) tuples as values, where *extension_params* is a
1483 ``str`` of the extension parameters segment of the header element,
1484 starting with the first '``;``'
1485 * a ``tuple`` or ``list``, where each item is either a header element
1486 ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
1487 or ``list`` where *media_range* is a ``str`` of the media range
1488 including any media type parameters, and *extension_params* is a
1489 ``str`` of the extension parameters segment of the header element,
1490 starting with the first '``;``'
1491 * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
1492 :class:`AcceptInvalidHeader` instance
1493 * object of any other type that returns a value for ``__str__``
1495 If `other` is a valid header value or an :class:`AcceptValidHeader`
1496 instance, a new :class:`AcceptValidHeader` instance with the valid
1497 header value is returned.
1499 If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid
1500 header value, or an :class:`AcceptInvalidHeader` instance, a new
1501 :class:`AcceptNoHeader` instance is returned.
1502 """
1503 if isinstance(other, AcceptValidHeader):
1504 return AcceptValidHeader(header_value=other.header_value)
1506 if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
1507 return self.__class__()
1509 return self._add_instance_and_non_accept_type(
1510 instance=self, other=other,
1511 )
1513 def __radd__(self, other):
1514 """
1515 Add to header, creating a new header object.
1517 See the docstring for :meth:`AcceptNoHeader.__add__`.
1518 """
1519 return self.__add__(other=other)
1521 def __repr__(self):
1522 return '<{}>'.format(self.__class__.__name__)
1524 def __str__(self):
1525 """Return the ``str`` ``'<no header in request>'``."""
1526 return '<no header in request>'
1528 def _add_instance_and_non_accept_type(self, instance, other):
1529 if other is None:
1530 return self.__class__()
1532 other_header_value = self._python_value_to_header_str(value=other)
1534 try:
1535 return AcceptValidHeader(header_value=other_header_value)
1536 except ValueError: # invalid header value
1537 return self.__class__()
1540class AcceptInvalidHeader(_AcceptInvalidOrNoHeader):
1541 """
1542 Represent an invalid ``Accept`` header.
1544 An invalid header is one that does not conform to
1545 :rfc:`7231#section-5.3.2`.
1547 :rfc:`7231` does not provide any guidance on what should happen if the
1548 ``Accept`` header has an invalid value. This implementation disregards the
1549 header, and treats it as if there is no ``Accept`` header in the request.
1551 This object should not be modified. To add to the header, we can use the
1552 addition operators (``+`` and ``+=``), which return a new object (see the
1553 docstring for :meth:`AcceptInvalidHeader.__add__`).
1554 """
1556 @property
1557 def header_value(self):
1558 """(``str`` or ``None``) The header value."""
1559 return self._header_value
1561 @property
1562 def parsed(self):
1563 """
1564 (``list`` or ``None``) Parsed form of the header.
1566 As the header is invalid and cannot be parsed, this is ``None``.
1567 """
1568 return self._parsed
1570 def __init__(self, header_value):
1571 """
1572 Create an :class:`AcceptInvalidHeader` instance.
1573 """
1574 self._header_value = header_value
1575 self._parsed = None
1576 self._parsed_nonzero = None
1578 def copy(self):
1579 """
1580 Create a copy of the header object.
1582 """
1583 return self.__class__(self._header_value)
1585 def __add__(self, other):
1586 """
1587 Add to header, creating a new header object.
1589 `other` can be:
1591 * ``None``
1592 * a ``str`` header value
1593 * a ``dict``, with media ranges ``str``'s (including any media type
1594 parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
1595 *extension_params*) tuples as values, where *extension_params* is a
1596 ``str`` of the extension parameters segment of the header element,
1597 starting with the first '``;``'
1598 * a ``tuple`` or ``list``, where each item is either a header element
1599 ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
1600 or ``list`` where *media_range* is a ``str`` of the media range
1601 including any media type parameters, and *extension_params* is a
1602 ``str`` of the extension parameters segment of the header element,
1603 starting with the first '``;``'
1604 * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
1605 :class:`AcceptInvalidHeader` instance
1606 * object of any other type that returns a value for ``__str__``
1608 If `other` is a valid header value or an :class:`AcceptValidHeader`
1609 instance, then a new :class:`AcceptValidHeader` instance with the valid
1610 header value is returned.
1612 If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid
1613 header value, or an :class:`AcceptInvalidHeader` instance, a new
1614 :class:`AcceptNoHeader` instance is returned.
1615 """
1616 if isinstance(other, AcceptValidHeader):
1617 return AcceptValidHeader(header_value=other.header_value)
1619 if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
1620 return AcceptNoHeader()
1622 return self._add_instance_and_non_accept_type(
1623 instance=self, other=other,
1624 )
1626 def __radd__(self, other):
1627 """
1628 Add to header, creating a new header object.
1630 See the docstring for :meth:`AcceptValidHeader.__add__`.
1631 """
1632 return self._add_instance_and_non_accept_type(
1633 instance=self, other=other, instance_on_the_right=True,
1634 )
1636 def __repr__(self):
1637 return '<{}>'.format(self.__class__.__name__)
1638 # We do not display the header_value, as it is untrusted input. The
1639 # header_value could always be easily obtained from the .header_value
1640 # property.
1642 def __str__(self):
1643 """Return the ``str`` ``'<invalid header value>'``."""
1644 return '<invalid header value>'
1646 def _add_instance_and_non_accept_type(
1647 self, instance, other, instance_on_the_right=False,
1648 ):
1649 if other is None:
1650 return AcceptNoHeader()
1652 other_header_value = self._python_value_to_header_str(value=other)
1654 try:
1655 return AcceptValidHeader(header_value=other_header_value)
1656 except ValueError: # invalid header value
1657 return AcceptNoHeader()
1660def create_accept_header(header_value):
1661 """
1662 Create an object representing the ``Accept`` header in a request.
1664 :param header_value: (``str``) header value
1665 :return: If `header_value` is ``None``, an :class:`AcceptNoHeader`
1666 instance.
1668 | If `header_value` is a valid ``Accept`` header, an
1669 :class:`AcceptValidHeader` instance.
1671 | If `header_value` is an invalid ``Accept`` header, an
1672 :class:`AcceptInvalidHeader` instance.
1673 """
1674 if header_value is None:
1675 return AcceptNoHeader()
1676 if isinstance(header_value, Accept):
1677 return header_value.copy()
1678 try:
1679 return AcceptValidHeader(header_value=header_value)
1680 except ValueError:
1681 return AcceptInvalidHeader(header_value=header_value)
1684def accept_property():
1685 doc = """
1686 Property representing the ``Accept`` header.
1688 (:rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`)
1690 The header value in the request environ is parsed and a new object
1691 representing the header is created every time we *get* the value of the
1692 property. (*set* and *del* change the header value in the request
1693 environ, and do not involve parsing.)
1694 """
1696 ENVIRON_KEY = 'HTTP_ACCEPT'
1698 def fget(request):
1699 """Get an object representing the header in the request."""
1700 return create_accept_header(
1701 header_value=request.environ.get(ENVIRON_KEY)
1702 )
1704 def fset(request, value):
1705 """
1706 Set the corresponding key in the request environ.
1708 `value` can be:
1710 * ``None``
1711 * a ``str`` header value
1712 * a ``dict``, with media ranges ``str``'s (including any media type
1713 parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
1714 *extension_params*) tuples as values, where *extension_params* is a
1715 ``str`` of the extension parameters segment of the header element,
1716 starting with the first '``;``'
1717 * a ``tuple`` or ``list``, where each item is either a header element
1718 ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
1719 or ``list`` where *media_range* is a ``str`` of the media range
1720 including any media type parameters, and *extension_params* is a
1721 ``str`` of the extension parameters segment of the header element,
1722 starting with the first '``;``'
1723 * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
1724 :class:`AcceptInvalidHeader` instance
1725 * object of any other type that returns a value for ``__str__``
1726 """
1727 if value is None or isinstance(value, AcceptNoHeader):
1728 fdel(request=request)
1729 else:
1730 if isinstance(value, (AcceptValidHeader, AcceptInvalidHeader)):
1731 header_value = value.header_value
1732 else:
1733 header_value = Accept._python_value_to_header_str(value=value)
1734 request.environ[ENVIRON_KEY] = header_value
1736 def fdel(request):
1737 """Delete the corresponding key from the request environ."""
1738 try:
1739 del request.environ[ENVIRON_KEY]
1740 except KeyError:
1741 pass
1743 return property(fget, fset, fdel, textwrap.dedent(doc))
1746class AcceptCharset(object):
1747 """
1748 Represent an ``Accept-Charset`` header.
1750 Base class for :class:`AcceptCharsetValidHeader`,
1751 :class:`AcceptCharsetNoHeader`, and :class:`AcceptCharsetInvalidHeader`.
1752 """
1754 # RFC 7231 Section 3.1.1.2 "Charset":
1755 # charset = token
1756 charset_re = token_re
1757 # RFC 7231 Section 5.3.3 "Accept-Charset":
1758 # Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
1759 charset_n_weight_re = _item_n_weight_re(item_re=charset_re)
1760 charset_n_weight_compiled_re = re.compile(charset_n_weight_re)
1761 accept_charset_compiled_re = _list_1_or_more__compiled_re(
1762 element_re=charset_n_weight_re,
1763 )
1765 @classmethod
1766 def _python_value_to_header_str(cls, value):
1767 if isinstance(value, str):
1768 header_str = value
1769 else:
1770 if hasattr(value, 'items'):
1771 value = sorted(
1772 value.items(),
1773 key=lambda item: item[1],
1774 reverse=True,
1775 )
1776 if isinstance(value, (tuple, list)):
1777 result = []
1778 for item in value:
1779 if isinstance(item, (tuple, list)):
1780 item = _item_qvalue_pair_to_header_element(pair=item)
1781 result.append(item)
1782 header_str = ', '.join(result)
1783 else:
1784 header_str = str(value)
1785 return header_str
1787 @classmethod
1788 def parse(cls, value):
1789 """
1790 Parse an ``Accept-Charset`` header.
1792 :param value: (``str``) header value
1793 :return: If `value` is a valid ``Accept-Charset`` header, returns an
1794 iterator of (charset, quality value) tuples, as parsed from
1795 the header from left to right.
1796 :raises ValueError: if `value` is an invalid header
1797 """
1798 # Check if header is valid
1799 # Using Python stdlib's `re` module, there is currently no way to check
1800 # the match *and* get all the groups using the same regex, so we have
1801 # to use one regex to check the match, and another to get the groups.
1802 if cls.accept_charset_compiled_re.match(value) is None:
1803 raise ValueError('Invalid value for an Accept-Charset header.')
1804 def generator(value):
1805 for match in (cls.charset_n_weight_compiled_re.finditer(value)):
1806 charset = match.group(1)
1807 qvalue = match.group(2)
1808 qvalue = float(qvalue) if qvalue else 1.0
1809 yield (charset, qvalue)
1810 return generator(value=value)
1813class AcceptCharsetValidHeader(AcceptCharset):
1814 """
1815 Represent a valid ``Accept-Charset`` header.
1817 A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.3
1818 <7231#section-5.3.3>`.
1820 This object should not be modified. To add to the header, we can use the
1821 addition operators (``+`` and ``+=``), which return a new object (see the
1822 docstring for :meth:`AcceptCharsetValidHeader.__add__`).
1823 """
1825 @property
1826 def header_value(self):
1827 """(``str``) The header value."""
1828 return self._header_value
1830 @property
1831 def parsed(self):
1832 """
1833 (``list``) Parsed form of the header.
1835 A list of (charset, quality value) tuples.
1836 """
1837 return self._parsed
1839 def __init__(self, header_value):
1840 """
1841 Create an :class:`AcceptCharsetValidHeader` instance.
1843 :param header_value: (``str``) header value.
1844 :raises ValueError: if `header_value` is an invalid value for an
1845 ``Accept-Charset`` header.
1846 """
1847 self._header_value = header_value
1848 self._parsed = list(self.parse(header_value))
1849 self._parsed_nonzero = [
1850 item for item in self.parsed if item[1] # item[1] is the qvalue
1851 ]
1853 def copy(self):
1854 """
1855 Create a copy of the header object.
1857 """
1858 return self.__class__(self._header_value)
1860 def __add__(self, other):
1861 """
1862 Add to header, creating a new header object.
1864 `other` can be:
1866 * ``None``
1867 * a ``str`` header value
1868 * a ``dict``, where keys are charsets and values are qvalues
1869 * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
1870 ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
1871 can be mixed within the ``tuple`` or ``list``)
1872 * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
1873 or :class:`AcceptCharsetInvalidHeader` instance
1874 * object of any other type that returns a value for ``__str__``
1876 If `other` is a valid header value or another
1877 :class:`AcceptCharsetValidHeader` instance, the two header values are
1878 joined with ``', '``, and a new :class:`AcceptCharsetValidHeader`
1879 instance with the new header value is returned.
1881 If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
1882 invalid header value, or an :class:`AcceptCharsetInvalidHeader`
1883 instance, a new :class:`AcceptCharsetValidHeader` instance with the
1884 same header value as ``self`` is returned.
1885 """
1886 if isinstance(other, AcceptCharsetValidHeader):
1887 return create_accept_charset_header(
1888 header_value=self.header_value + ', ' + other.header_value,
1889 )
1891 if isinstance(
1892 other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
1893 ):
1894 return self.__class__(header_value=self.header_value)
1896 return self._add_instance_and_non_accept_charset_type(
1897 instance=self, other=other,
1898 )
1900 def __bool__(self):
1901 """
1902 Return whether ``self`` represents a valid ``Accept-Charset`` header.
1904 Return ``True`` if ``self`` represents a valid header, and ``False`` if
1905 it represents an invalid header, or the header not being in the
1906 request.
1908 For this class, it always returns ``True``.
1909 """
1910 return True
1911 __nonzero__ = __bool__ # Python 2
1913 def __contains__(self, offer):
1914 """
1915 Return ``bool`` indicating whether `offer` is acceptable.
1917 .. warning::
1919 The behavior of :meth:`AcceptCharsetValidHeader.__contains__` is
1920 currently being maintained for backward compatibility, but it will
1921 change in the future to better conform to the RFC.
1923 :param offer: (``str``) charset offer
1924 :return: (``bool``) Whether ``offer`` is acceptable according to the
1925 header.
1927 This does not fully conform to :rfc:`RFC 7231, section 5.3.3
1928 <7231#section-5.3.3>`: it incorrect interprets ``*`` to mean 'match any
1929 charset in the header', rather than 'match any charset that is not
1930 mentioned elsewhere in the header'::
1932 >>> 'UTF-8' in AcceptCharsetValidHeader('UTF-8;q=0, *')
1933 True
1934 """
1935 warnings.warn(
1936 'The behavior of AcceptCharsetValidHeader.__contains__ is '
1937 'currently being maintained for backward compatibility, but it '
1938 'will change in the future to better conform to the RFC.',
1939 DeprecationWarning,
1940 )
1941 for mask, quality in self._parsed_nonzero:
1942 if self._old_match(mask, offer):
1943 return True
1944 return False
1946 def __iter__(self):
1947 """
1948 Return all the items with non-0 qvalues, in order of preference.
1950 .. warning::
1952 The behavior of this method is currently maintained for backward
1953 compatibility, but will change in the future.
1955 :return: iterator of all the items (charset or ``*``) in the header
1956 with non-0 qvalues, in descending order of qvalue. If two
1957 items have the same qvalue, they are returned in the order of
1958 their positions in the header, from left to right.
1960 Please note that this is a simple filter for the items in the header
1961 with non-0 qvalues, and is not necessarily the same as what the client
1962 prefers, e.g. ``'utf-7;q=0, *'`` means 'everything but utf-7', but
1963 ``list(instance)`` would return only ``['*']``.
1964 """
1965 warnings.warn(
1966 'The behavior of AcceptCharsetValidHeader.__iter__ is currently '
1967 'maintained for backward compatibility, but will change in the '
1968 'future.',
1969 DeprecationWarning,
1970 )
1971 for m,q in sorted(
1972 self._parsed_nonzero,
1973 key=lambda i: i[1],
1974 reverse=True
1975 ):
1976 yield m
1978 def __radd__(self, other):
1979 """
1980 Add to header, creating a new header object.
1982 See the docstring for :meth:`AcceptCharsetValidHeader.__add__`.
1983 """
1984 return self._add_instance_and_non_accept_charset_type(
1985 instance=self, other=other, instance_on_the_right=True,
1986 )
1988 def __repr__(self):
1989 return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
1991 def __str__(self):
1992 r"""
1993 Return a tidied up version of the header value.
1995 e.g. If the ``header_value`` is ``', \t,iso-8859-5;q=0.000 \t,
1996 utf-8;q=1.000, UTF-7, unicode-1-1;q=0.210 ,'``, ``str(instance)``
1997 returns ``'iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21'``.
1998 """
1999 return ', '.join(
2000 _item_qvalue_pair_to_header_element(pair=tuple_)
2001 for tuple_ in self.parsed
2002 )
2004 def _add_instance_and_non_accept_charset_type(
2005 self, instance, other, instance_on_the_right=False,
2006 ):
2007 if not other:
2008 return self.__class__(header_value=instance.header_value)
2010 other_header_value = self._python_value_to_header_str(value=other)
2012 try:
2013 self.parse(value=other_header_value)
2014 except ValueError: # invalid header value
2015 return self.__class__(header_value=instance.header_value)
2017 new_header_value = (
2018 (other_header_value + ', ' + instance.header_value)
2019 if instance_on_the_right
2020 else (instance.header_value + ', ' + other_header_value)
2021 )
2022 return self.__class__(header_value=new_header_value)
2024 def _old_match(self, mask, offer):
2025 """
2026 Return whether charset offer matches header item (charset or ``*``).
2028 .. warning::
2030 This is maintained for backward compatibility, and will be
2031 deprecated in the future.
2033 This method was WebOb's old criterion for deciding whether a charset
2034 matches a header item (charset or ``*``), used in
2036 - :meth:`AcceptCharsetValidHeader.__contains__`
2037 - :meth:`AcceptCharsetValidHeader.best_match`
2038 - :meth:`AcceptCharsetValidHeader.quality`
2040 It does not conform to :rfc:`RFC 7231, section 5.3.3
2041 <7231#section-5.3.3>` in that it does not interpret ``*`` values in the
2042 header correctly: ``*`` should only match charsets not mentioned
2043 elsewhere in the header.
2044 """
2045 return mask == '*' or offer.lower() == mask.lower()
2047 def acceptable_offers(self, offers):
2048 """
2049 Return the offers that are acceptable according to the header.
2051 The offers are returned in descending order of preference, where
2052 preference is indicated by the qvalue of the charset or ``*`` in the
2053 header matching the offer.
2055 This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3
2056 <7231#section-5.3.3>`.
2058 :param offers: ``iterable`` of ``str`` charsets
2059 :return: A list of tuples of the form (charset, qvalue), in descending
2060 order of qvalue. Where two offers have the same qvalue, they
2061 are returned in the same order as their order in `offers`.
2062 """
2063 lowercased_parsed = [
2064 (charset.lower(), qvalue) for (charset, qvalue) in self.parsed
2065 ]
2066 lowercased_offers = [offer.lower() for offer in offers]
2068 not_acceptable_charsets = set()
2069 acceptable_charsets = dict()
2070 asterisk_qvalue = None
2072 for charset, qvalue in lowercased_parsed:
2073 if charset == '*':
2074 if asterisk_qvalue is None:
2075 asterisk_qvalue = qvalue
2076 elif (
2077 charset not in acceptable_charsets and charset not in
2078 not_acceptable_charsets
2079 # if we have not already encountered this charset in the header
2080 ):
2081 if qvalue == 0.0:
2082 not_acceptable_charsets.add(charset)
2083 else:
2084 acceptable_charsets[charset] = qvalue
2085 acceptable_charsets = list(acceptable_charsets.items())
2086 # Sort acceptable_charsets by qvalue, descending order
2087 acceptable_charsets.sort(key=lambda tuple_: tuple_[1], reverse=True)
2089 filtered_offers = []
2090 for index, offer in enumerate(lowercased_offers):
2091 # If offer matches a non-* charset with q=0, it is filtered out
2092 if any((
2093 (offer == charset) for charset in not_acceptable_charsets
2094 )):
2095 continue
2097 matched_charset_qvalue = None
2098 for charset, qvalue in acceptable_charsets:
2099 if offer == charset:
2100 matched_charset_qvalue = qvalue
2101 break
2102 else:
2103 if asterisk_qvalue:
2104 matched_charset_qvalue = asterisk_qvalue
2105 if matched_charset_qvalue is not None: # if there was a match
2106 filtered_offers.append((
2107 offers[index], matched_charset_qvalue, index
2108 ))
2110 # sort by position in `offers` argument, ascending
2111 filtered_offers.sort(key=lambda tuple_: tuple_[2])
2112 # When qvalues are tied, position in `offers` is the tiebreaker.
2114 # sort by qvalue, descending
2115 filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True)
2117 return [(item[0], item[1]) for item in filtered_offers]
2118 # (offer, qvalue), dropping the position
2120 def best_match(self, offers, default_match=None):
2121 """
2122 Return the best match from the sequence of charset `offers`.
2124 .. warning::
2126 This is currently maintained for backward compatibility, and will be
2127 deprecated in the future.
2129 :meth:`AcceptCharsetValidHeader.best_match` has many issues, and
2130 does not conform to :rfc:`RFC 7231 <7231>`.
2132 Each charset in `offers` is checked against each non-``q=0`` item
2133 (charset or ``*``) in the header. If the two are a match according to
2134 WebOb's old criterion for a match, the quality value of the match is
2135 the qvalue of the item from the header multiplied by the server quality
2136 value of the offer (if the server quality value is not supplied, it is
2137 1).
2139 The offer in the match with the highest quality value is the best
2140 match. If there is more than one match with the highest qvalue, the one
2141 that shows up first in `offers` is the best match.
2143 :param offers: (iterable)
2145 | Each item in the iterable may be a ``str`` charset, or
2146 a (charset, server quality value) ``tuple`` or
2147 ``list``. (The two may be mixed in the iterable.)
2149 :param default_match: (optional, any type) the value to be returned if
2150 there is no match
2152 :return: (``str``, or the type of `default_match`)
2154 | The offer that is the best match. If there is no match, the
2155 value of `default_match` is returned.
2157 The algorithm behind this method was written for the ``Accept`` header
2158 rather than the ``Accept-Charset`` header. It uses the old criterion of
2159 a match in :meth:`AcceptCharsetValidHeader._old_match`, which does not
2160 conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that
2161 it does not interpret ``*`` values in the header correctly: ``*``
2162 should only match charsets not mentioned elsewhere in the header::
2164 >>> AcceptCharsetValidHeader('utf-8;q=0, *').best_match(['utf-8'])
2165 'utf-8'
2166 """
2167 warnings.warn(
2168 'The behavior of AcceptCharsetValidHeader.best_match is currently'
2169 ' being maintained for backward compatibility, but it will be '
2170 'deprecated in the future, as it does not conform to the RFC.',
2171 DeprecationWarning,
2172 )
2173 best_quality = -1
2174 best_offer = default_match
2175 matched_by = '*/*'
2176 for offer in offers:
2177 if isinstance(offer, (tuple, list)):
2178 offer, server_quality = offer
2179 else:
2180 server_quality = 1
2181 for mask, quality in self._parsed_nonzero:
2182 possible_quality = server_quality * quality
2183 if possible_quality < best_quality:
2184 continue
2185 elif possible_quality == best_quality:
2186 # 'text/plain' overrides 'message/*' overrides '*/*'
2187 # (if all match w/ the same q=)
2188 # [We can see that this was written for the Accept header,
2189 # not the Accept-Charset header.]
2190 if matched_by.count('*') <= mask.count('*'):
2191 continue
2192 if self._old_match(mask, offer):
2193 best_quality = possible_quality
2194 best_offer = offer
2195 matched_by = mask
2196 return best_offer
2198 def quality(self, offer):
2199 """
2200 Return quality value of given offer, or ``None`` if there is no match.
2202 .. warning::
2204 This is currently maintained for backward compatibility, and will be
2205 deprecated in the future.
2207 :param offer: (``str``) charset offer
2208 :return: (``float`` or ``None``)
2210 | The quality value from the charset that matches the `offer`,
2211 or ``None`` if there is no match.
2213 This uses the old criterion of a match in
2214 :meth:`AcceptCharsetValidHeader._old_match`, which does not conform to
2215 :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that it does
2216 not interpret ``*`` values in the header correctly: ``*`` should only
2217 match charsets not mentioned elsewhere in the header::
2219 >>> AcceptCharsetValidHeader('utf-8;q=0, *').quality('utf-8')
2220 1.0
2221 >>> AcceptCharsetValidHeader('utf-8;q=0.9, *').quality('utf-8')
2222 1.0
2223 """
2224 warnings.warn(
2225 'The behavior of AcceptCharsetValidHeader.quality is currently '
2226 'being maintained for backward compatibility, but it will be '
2227 'deprecated in the future, as it does not conform to the RFC.',
2228 DeprecationWarning,
2229 )
2230 bestq = 0
2231 for mask, q in self.parsed:
2232 if self._old_match(mask, offer):
2233 bestq = max(bestq, q)
2234 return bestq or None
2237class _AcceptCharsetInvalidOrNoHeader(AcceptCharset):
2238 """
2239 Represent when an ``Accept-Charset`` header is invalid or not in request.
2241 This is the base class for the behaviour that
2242 :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader`
2243 have in common.
2245 :rfc:`7231` does not provide any guidance on what should happen if the
2246 ``Accept-Charset`` header has an invalid value. This implementation
2247 disregards the header when the header is invalid, so
2248 :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader`
2249 have much behaviour in common.
2250 """
2252 def __bool__(self):
2253 """
2254 Return whether ``self`` represents a valid ``Accept-Charset`` header.
2256 Return ``True`` if ``self`` represents a valid header, and ``False`` if
2257 it represents an invalid header, or the header not being in the
2258 request.
2260 For this class, it always returns ``False``.
2261 """
2262 return False
2263 __nonzero__ = __bool__ # Python 2
2265 def __contains__(self, offer):
2266 """
2267 Return ``bool`` indicating whether `offer` is acceptable.
2269 .. warning::
2271 The behavior of ``.__contains__`` for the ``AcceptCharset`` classes
2272 is currently being maintained for backward compatibility, but it
2273 will change in the future to better conform to the RFC.
2275 :param offer: (``str``) charset offer
2276 :return: (``bool``) Whether ``offer`` is acceptable according to the
2277 header.
2279 For this class, either there is no ``Accept-Charset`` header in the
2280 request, or the header is invalid, so any charset is acceptable, and
2281 this always returns ``True``.
2282 """
2283 warnings.warn(
2284 'The behavior of .__contains__ for the AcceptCharset classes is '
2285 'currently being maintained for backward compatibility, but it '
2286 'will change in the future to better conform to the RFC.',
2287 DeprecationWarning,
2288 )
2289 return True
2291 def __iter__(self):
2292 """
2293 Return all the items with non-0 qvalues, in order of preference.
2295 .. warning::
2297 The behavior of this method is currently maintained for backward
2298 compatibility, but will change in the future.
2300 :return: iterator of all the items (charset or ``*``) in the header
2301 with non-0 qvalues, in descending order of qvalue. If two
2302 items have the same qvalue, they are returned in the order of
2303 their positions in the header, from left to right.
2305 When there is no ``Accept-Charset`` header in the request or the header
2306 is invalid, there are no items, and this always returns an empty
2307 iterator.
2308 """
2309 warnings.warn(
2310 'The behavior of AcceptCharsetValidHeader.__iter__ is currently '
2311 'maintained for backward compatibility, but will change in the '
2312 'future.',
2313 DeprecationWarning,
2314 )
2315 return iter(())
2317 def acceptable_offers(self, offers):
2318 """
2319 Return the offers that are acceptable according to the header.
2321 The offers are returned in descending order of preference, where
2322 preference is indicated by the qvalue of the charset or ``*`` in the
2323 header matching the offer.
2325 This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3
2326 <7231#section-5.3.3>`.
2328 :param offers: ``iterable`` of ``str`` charsets
2329 :return: A list of tuples of the form (charset, qvalue), in descending
2330 order of qvalue. Where two offers have the same qvalue, they
2331 are returned in the same order as their order in `offers`.
2333 | When the header is invalid or there is no ``Accept-Charset``
2334 header in the request, all `offers` are considered
2335 acceptable, so this method returns a list of (charset,
2336 qvalue) tuples where each offer in `offers` is paired with
2337 the qvalue of 1.0, in the same order as `offers`.
2338 """
2339 return [(offer, 1.0) for offer in offers]
2341 def best_match(self, offers, default_match=None):
2342 """
2343 Return the best match from the sequence of charset `offers`.
2345 This is the ``.best_match()`` method for when the header is invalid or
2346 not found in the request, corresponding to
2347 :meth:`AcceptCharsetValidHeader.best_match`.
2349 .. warning::
2351 This is currently maintained for backward compatibility, and will be
2352 deprecated in the future (see the documentation for
2353 :meth:`AcceptCharsetValidHeader.best_match`).
2355 When the header is invalid, or there is no `Accept-Charset` header in
2356 the request, all the charsets in `offers` are considered acceptable, so
2357 the best match is the charset in `offers` with the highest server
2358 quality value (if the server quality value is not supplied, it is 1).
2360 If more than one charsets in `offers` have the same highest server
2361 quality value, then the one that shows up first in `offers` is the best
2362 match.
2364 :param offers: (iterable)
2366 | Each item in the iterable may be a ``str`` charset, or
2367 a (charset, server quality value) ``tuple`` or
2368 ``list``. (The two may be mixed in the iterable.)
2370 :param default_match: (optional, any type) the value to be returned if
2371 `offers` is empty.
2373 :return: (``str``, or the type of `default_match`)
2375 | The charset that has the highest server quality value. If
2376 `offers` is empty, the value of `default_match` is returned.
2377 """
2378 warnings.warn(
2379 'The behavior of .best_match for the AcceptCharset classes is '
2380 'currently being maintained for backward compatibility, but the '
2381 'method will be deprecated in the future, as its behavior is not '
2382 'specified in (and currently does not conform to) RFC 7231.',
2383 DeprecationWarning,
2384 )
2385 best_quality = -1
2386 best_offer = default_match
2387 for offer in offers:
2388 if isinstance(offer, (list, tuple)):
2389 offer, quality = offer
2390 else:
2391 quality = 1
2392 if quality > best_quality:
2393 best_offer = offer
2394 best_quality = quality
2395 return best_offer
2397 def quality(self, offer):
2398 """
2399 Return quality value of given offer, or ``None`` if there is no match.
2401 This is the ``.quality()`` method for when the header is invalid or not
2402 found in the request, corresponding to
2403 :meth:`AcceptCharsetValidHeader.quality`.
2405 .. warning::
2407 This is currently maintained for backward compatibility, and will be
2408 deprecated in the future (see the documentation for
2409 :meth:`AcceptCharsetValidHeader.quality`).
2411 :param offer: (``str``) charset offer
2412 :return: (``float``) ``1.0``.
2414 When the ``Accept-Charset`` header is invalid or not in the request,
2415 all offers are equally acceptable, so 1.0 is always returned.
2416 """
2417 warnings.warn(
2418 'The behavior of .quality for the Accept-Charset classes is '
2419 'currently being maintained for backward compatibility, but the '
2420 'method will be deprecated in the future, as its behavior does not'
2421 ' conform to RFC 7231.',
2422 DeprecationWarning,
2423 )
2424 return 1.0
2427class AcceptCharsetNoHeader(_AcceptCharsetInvalidOrNoHeader):
2428 """
2429 Represent when there is no ``Accept-Charset`` header in the request.
2431 This object should not be modified. To add to the header, we can use the
2432 addition operators (``+`` and ``+=``), which return a new object (see the
2433 docstring for :meth:`AcceptCharsetNoHeader.__add__`).
2434 """
2436 @property
2437 def header_value(self):
2438 """
2439 (``str`` or ``None``) The header value.
2441 As there is no header in the request, this is ``None``.
2442 """
2443 return self._header_value
2445 @property
2446 def parsed(self):
2447 """
2448 (``list`` or ``None``) Parsed form of the header.
2450 As there is no header in the request, this is ``None``.
2451 """
2452 return self._parsed
2454 def __init__(self):
2455 """
2456 Create an :class:`AcceptCharsetNoHeader` instance.
2457 """
2458 self._header_value = None
2459 self._parsed = None
2460 self._parsed_nonzero = None
2462 def copy(self):
2463 """
2464 Create a copy of the header object.
2466 """
2467 return self.__class__()
2469 def __add__(self, other):
2470 """
2471 Add to header, creating a new header object.
2473 `other` can be:
2475 * ``None``
2476 * a ``str`` header value
2477 * a ``dict``, where keys are charsets and values are qvalues
2478 * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
2479 ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
2480 can be mixed within the ``tuple`` or ``list``)
2481 * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
2482 or :class:`AcceptCharsetInvalidHeader` instance
2483 * object of any other type that returns a value for ``__str__``
2485 If `other` is a valid header value or an
2486 :class:`AcceptCharsetValidHeader` instance, a new
2487 :class:`AcceptCharsetValidHeader` instance with the valid header value
2488 is returned.
2490 If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
2491 invalid header value, or an :class:`AcceptCharsetInvalidHeader`
2492 instance, a new :class:`AcceptCharsetNoHeader` instance is returned.
2493 """
2494 if isinstance(other, AcceptCharsetValidHeader):
2495 return AcceptCharsetValidHeader(header_value=other.header_value)
2497 if isinstance(
2498 other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
2499 ):
2500 return self.__class__()
2502 return self._add_instance_and_non_accept_charset_type(
2503 instance=self, other=other,
2504 )
2506 def __radd__(self, other):
2507 """
2508 Add to header, creating a new header object.
2510 See the docstring for :meth:`AcceptCharsetNoHeader.__add__`.
2511 """
2512 return self.__add__(other=other)
2514 def __repr__(self):
2515 return '<{}>'.format(self.__class__.__name__)
2517 def __str__(self):
2518 """Return the ``str`` ``'<no header in request>'``."""
2519 return '<no header in request>'
2521 def _add_instance_and_non_accept_charset_type(self, instance, other):
2522 if not other:
2523 return self.__class__()
2525 other_header_value = self._python_value_to_header_str(value=other)
2527 try:
2528 return AcceptCharsetValidHeader(header_value=other_header_value)
2529 except ValueError: # invalid header value
2530 return self.__class__()
2533class AcceptCharsetInvalidHeader(_AcceptCharsetInvalidOrNoHeader):
2534 """
2535 Represent an invalid ``Accept-Charset`` header.
2537 An invalid header is one that does not conform to
2538 :rfc:`7231#section-5.3.3`. As specified in the RFC, an empty header is an
2539 invalid ``Accept-Charset`` header.
2541 :rfc:`7231` does not provide any guidance on what should happen if the
2542 ``Accept-Charset`` header has an invalid value. This implementation
2543 disregards the header, and treats it as if there is no ``Accept-Charset``
2544 header in the request.
2546 This object should not be modified. To add to the header, we can use the
2547 addition operators (``+`` and ``+=``), which return a new object (see the
2548 docstring for :meth:`AcceptCharsetInvalidHeader.__add__`).
2549 """
2551 @property
2552 def header_value(self):
2553 """(``str`` or ``None``) The header value."""
2554 return self._header_value
2556 @property
2557 def parsed(self):
2558 """
2559 (``list`` or ``None``) Parsed form of the header.
2561 As the header is invalid and cannot be parsed, this is ``None``.
2562 """
2563 return self._parsed
2565 def __init__(self, header_value):
2566 """
2567 Create an :class:`AcceptCharsetInvalidHeader` instance.
2568 """
2569 self._header_value = header_value
2570 self._parsed = None
2571 self._parsed_nonzero = None
2573 def copy(self):
2574 """
2575 Create a copy of the header object.
2577 """
2578 return self.__class__(self._header_value)
2580 def __add__(self, other):
2581 """
2582 Add to header, creating a new header object.
2584 `other` can be:
2586 * ``None``
2587 * a ``str`` header value
2588 * a ``dict``, where keys are charsets and values are qvalues
2589 * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
2590 ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
2591 can be mixed within the ``tuple`` or ``list``)
2592 * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
2593 or :class:`AcceptCharsetInvalidHeader` instance
2594 * object of any other type that returns a value for ``__str__``
2596 If `other` is a valid header value or an
2597 :class:`AcceptCharsetValidHeader` instance, a new
2598 :class:`AcceptCharsetValidHeader` instance with the valid header value
2599 is returned.
2601 If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
2602 invalid header value, or an :class:`AcceptCharsetInvalidHeader`
2603 instance, a new :class:`AcceptCharsetNoHeader` instance is returned.
2604 """
2605 if isinstance(other, AcceptCharsetValidHeader):
2606 return AcceptCharsetValidHeader(header_value=other.header_value)
2608 if isinstance(
2609 other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
2610 ):
2611 return AcceptCharsetNoHeader()
2613 return self._add_instance_and_non_accept_charset_type(
2614 instance=self, other=other,
2615 )
2617 def __radd__(self, other):
2618 """
2619 Add to header, creating a new header object.
2621 See the docstring for :meth:`AcceptCharsetValidHeader.__add__`.
2622 """
2623 return self._add_instance_and_non_accept_charset_type(
2624 instance=self, other=other, instance_on_the_right=True,
2625 )
2627 def __repr__(self):
2628 return '<{}>'.format(self.__class__.__name__)
2629 # We do not display the header_value, as it is untrusted input. The
2630 # header_value could always be easily obtained from the .header_value
2631 # property.
2633 def __str__(self):
2634 """Return the ``str`` ``'<invalid header value>'``."""
2635 return '<invalid header value>'
2637 def _add_instance_and_non_accept_charset_type(
2638 self, instance, other, instance_on_the_right=False,
2639 ):
2640 if not other:
2641 return AcceptCharsetNoHeader()
2643 other_header_value = self._python_value_to_header_str(value=other)
2645 try:
2646 return AcceptCharsetValidHeader(header_value=other_header_value)
2647 except ValueError: # invalid header value
2648 return AcceptCharsetNoHeader()
2651def create_accept_charset_header(header_value):
2652 """
2653 Create an object representing the ``Accept-Charset`` header in a request.
2655 :param header_value: (``str``) header value
2656 :return: If `header_value` is ``None``, an :class:`AcceptCharsetNoHeader`
2657 instance.
2659 | If `header_value` is a valid ``Accept-Charset`` header, an
2660 :class:`AcceptCharsetValidHeader` instance.
2662 | If `header_value` is an invalid ``Accept-Charset`` header, an
2663 :class:`AcceptCharsetInvalidHeader` instance.
2664 """
2665 if header_value is None:
2666 return AcceptCharsetNoHeader()
2667 if isinstance(header_value, AcceptCharset):
2668 return header_value.copy()
2669 try:
2670 return AcceptCharsetValidHeader(header_value=header_value)
2671 except ValueError:
2672 return AcceptCharsetInvalidHeader(header_value=header_value)
2675def accept_charset_property():
2676 doc = """
2677 Property representing the ``Accept-Charset`` header.
2679 (:rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`)
2681 The header value in the request environ is parsed and a new object
2682 representing the header is created every time we *get* the value of the
2683 property. (*set* and *del* change the header value in the request
2684 environ, and do not involve parsing.)
2685 """
2687 ENVIRON_KEY = 'HTTP_ACCEPT_CHARSET'
2689 def fget(request):
2690 """Get an object representing the header in the request."""
2691 return create_accept_charset_header(
2692 header_value=request.environ.get(ENVIRON_KEY)
2693 )
2695 def fset(request, value):
2696 """
2697 Set the corresponding key in the request environ.
2699 `value` can be:
2701 * ``None``
2702 * a ``str`` header value
2703 * a ``dict``, where keys are charsets and values are qvalues
2704 * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
2705 ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
2706 can be mixed within the ``tuple`` or ``list``)
2707 * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
2708 or :class:`AcceptCharsetInvalidHeader` instance
2709 * object of any other type that returns a value for ``__str__``
2710 """
2711 if value is None or isinstance(value, AcceptCharsetNoHeader):
2712 fdel(request=request)
2713 else:
2714 if isinstance(
2715 value, (AcceptCharsetValidHeader, AcceptCharsetInvalidHeader)
2716 ):
2717 header_value = value.header_value
2718 else:
2719 header_value = AcceptCharset._python_value_to_header_str(
2720 value=value,
2721 )
2722 request.environ[ENVIRON_KEY] = header_value
2724 def fdel(request):
2725 """Delete the corresponding key from the request environ."""
2726 try:
2727 del request.environ[ENVIRON_KEY]
2728 except KeyError:
2729 pass
2731 return property(fget, fset, fdel, textwrap.dedent(doc))
2734class AcceptEncoding(object):
2735 """
2736 Represent an ``Accept-Encoding`` header.
2738 Base class for :class:`AcceptEncodingValidHeader`,
2739 :class:`AcceptEncodingNoHeader`, and :class:`AcceptEncodingInvalidHeader`.
2740 """
2742 # RFC 7231 Section 3.1.2.1 "Content Codings":
2743 # content-coding = token
2744 # Section 5.3.4 "Accept-Encoding":
2745 # Accept-Encoding = #( codings [ weight ] )
2746 # codings = content-coding / "identity" / "*"
2747 codings_re = token_re
2748 # "identity" (case-insensitive) and "*" are both already included in token
2749 # rule
2750 codings_n_weight_re = _item_n_weight_re(item_re=codings_re)
2751 codings_n_weight_compiled_re = re.compile(codings_n_weight_re)
2752 accept_encoding_compiled_re = _list_0_or_more__compiled_re(
2753 element_re=codings_n_weight_re,
2754 )
2756 @classmethod
2757 def _python_value_to_header_str(cls, value):
2758 if isinstance(value, str):
2759 header_str = value
2760 else:
2761 if hasattr(value, 'items'):
2762 value = sorted(
2763 value.items(),
2764 key=lambda item: item[1],
2765 reverse=True,
2766 )
2767 if isinstance(value, (tuple, list)):
2768 result = []
2769 for item in value:
2770 if isinstance(item, (tuple, list)):
2771 item = _item_qvalue_pair_to_header_element(pair=item)
2772 result.append(item)
2773 header_str = ', '.join(result)
2774 else:
2775 header_str = str(value)
2776 return header_str
2778 @classmethod
2779 def parse(cls, value):
2780 """
2781 Parse an ``Accept-Encoding`` header.
2783 :param value: (``str``) header value
2784 :return: If `value` is a valid ``Accept-Encoding`` header, returns an
2785 iterator of (codings, quality value) tuples, as parsed from
2786 the header from left to right.
2787 :raises ValueError: if `value` is an invalid header
2788 """
2789 # Check if header is valid
2790 # Using Python stdlib's `re` module, there is currently no way to check
2791 # the match *and* get all the groups using the same regex, so we have
2792 # to use one regex to check the match, and another to get the groups.
2793 if cls.accept_encoding_compiled_re.match(value) is None:
2794 raise ValueError('Invalid value for an Accept-Encoding header.')
2795 def generator(value):
2796 for match in (cls.codings_n_weight_compiled_re.finditer(value)):
2797 codings = match.group(1)
2798 qvalue = match.group(2)
2799 qvalue = float(qvalue) if qvalue else 1.0
2800 yield (codings, qvalue)
2801 return generator(value=value)
2804class AcceptEncodingValidHeader(AcceptEncoding):
2805 """
2806 Represent a valid ``Accept-Encoding`` header.
2808 A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.4
2809 <7231#section-5.3.4>`.
2811 This object should not be modified. To add to the header, we can use the
2812 addition operators (``+`` and ``+=``), which return a new object (see the
2813 docstring for :meth:`AcceptEncodingValidHeader.__add__`).
2814 """
2816 @property
2817 def header_value(self):
2818 """(``str`` or ``None``) The header value."""
2819 return self._header_value
2821 @property
2822 def parsed(self):
2823 """
2824 (``list`` or ``None``) Parsed form of the header.
2826 A list of (*codings*, *qvalue*) tuples, where
2828 *codings* (``str``) is a content-coding, the string "``identity``", or
2829 "``*``"; and
2831 *qvalue* (``float``) is the quality value of the codings.
2832 """
2833 return self._parsed
2835 def __init__(self, header_value):
2836 """
2837 Create an :class:`AcceptEncodingValidHeader` instance.
2839 :param header_value: (``str``) header value.
2840 :raises ValueError: if `header_value` is an invalid value for an
2841 ``Accept-Encoding`` header.
2842 """
2843 self._header_value = header_value
2844 self._parsed = list(self.parse(header_value))
2845 self._parsed_nonzero = [item for item in self.parsed if item[1]]
2846 # item[1] is the qvalue
2848 def copy(self):
2849 """
2850 Create a copy of the header object.
2852 """
2853 return self.__class__(self._header_value)
2855 def __add__(self, other):
2856 """
2857 Add to header, creating a new header object.
2859 `other` can be:
2861 * ``None``
2862 * a ``str`` header value
2863 * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
2864 keys, and qvalue ``float``'s as values
2865 * a ``tuple`` or ``list``, where each item is either a header element
2866 ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
2867 or ``list``
2868 * an :class:`AcceptEncodingValidHeader`,
2869 :class:`AcceptEncodingNoHeader`, or
2870 :class:`AcceptEncodingInvalidHeader` instance
2871 * object of any other type that returns a value for ``__str__``
2873 If `other` is a valid header value or another
2874 :class:`AcceptEncodingValidHeader` instance, and the header value it
2875 represents is not ``''``, then the two header values are joined with
2876 ``', '``, and a new :class:`AcceptEncodingValidHeader` instance with
2877 the new header value is returned.
2879 If `other` is a valid header value or another
2880 :class:`AcceptEncodingValidHeader` instance representing a header value
2881 of ``''``; or if it is ``None`` or an :class:`AcceptEncodingNoHeader`
2882 instance; or if it is an invalid header value, or an
2883 :class:`AcceptEncodingInvalidHeader` instance, then a new
2884 :class:`AcceptEncodingValidHeader` instance with the same header value
2885 as ``self`` is returned.
2886 """
2887 if isinstance(other, AcceptEncodingValidHeader):
2888 if other.header_value == '':
2889 return self.__class__(header_value=self.header_value)
2890 else:
2891 return create_accept_encoding_header(
2892 header_value=self.header_value + ', ' + other.header_value,
2893 )
2895 if isinstance(
2896 other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
2897 ):
2898 return self.__class__(header_value=self.header_value)
2900 return self._add_instance_and_non_accept_encoding_type(
2901 instance=self, other=other,
2902 )
2904 def __bool__(self):
2905 """
2906 Return whether ``self`` represents a valid ``Accept-Encoding`` header.
2908 Return ``True`` if ``self`` represents a valid header, and ``False`` if
2909 it represents an invalid header, or the header not being in the
2910 request.
2912 For this class, it always returns ``True``.
2913 """
2914 return True
2915 __nonzero__ = __bool__ # Python 2
2917 def __contains__(self, offer):
2918 """
2919 Return ``bool`` indicating whether `offer` is acceptable.
2921 .. warning::
2923 The behavior of :meth:`AcceptEncodingValidHeader.__contains__` is
2924 currently being maintained for backward compatibility, but it will
2925 change in the future to better conform to the RFC.
2927 :param offer: (``str``) a content-coding or ``identity`` offer
2928 :return: (``bool``) Whether ``offer`` is acceptable according to the
2929 header.
2931 The behavior of this method does not fully conform to :rfc:`7231`.
2932 It does not correctly interpret ``*``::
2934 >>> 'gzip' in AcceptEncodingValidHeader('gzip;q=0, *')
2935 True
2937 and does not handle the ``identity`` token correctly::
2939 >>> 'identity' in AcceptEncodingValidHeader('gzip')
2940 False
2941 """
2942 warnings.warn(
2943 'The behavior of AcceptEncodingValidHeader.__contains__ is '
2944 'currently being maintained for backward compatibility, but it '
2945 'will change in the future to better conform to the RFC.',
2946 DeprecationWarning,
2947 )
2948 for mask, quality in self._parsed_nonzero:
2949 if self._old_match(mask, offer):
2950 return True
2952 def __iter__(self):
2953 """
2954 Return all the ranges with non-0 qvalues, in order of preference.
2956 .. warning::
2958 The behavior of this method is currently maintained for backward
2959 compatibility, but will change in the future.
2961 :return: iterator of all the (content-coding/``identity``/``*``) items
2962 in the header with non-0 qvalues, in descending order of
2963 qvalue. If two items have the same qvalue, they are returned
2964 in the order of their positions in the header, from left to
2965 right.
2967 Please note that this is a simple filter for the items in the header
2968 with non-0 qvalues, and is not necessarily the same as what the client
2969 prefers, e.g. ``'gzip;q=0, *'`` means 'everything but gzip', but
2970 ``list(instance)`` would return only ``['*']``.
2971 """
2972 warnings.warn(
2973 'The behavior of AcceptEncodingLanguageValidHeader.__iter__ is '
2974 'currently maintained for backward compatibility, but will change'
2975 ' in the future.',
2976 DeprecationWarning,
2977 )
2979 for m,q in sorted(
2980 self._parsed_nonzero,
2981 key=lambda i: i[1],
2982 reverse=True
2983 ):
2984 yield m
2986 def __radd__(self, other):
2987 """
2988 Add to header, creating a new header object.
2990 See the docstring for :meth:`AcceptEncodingValidHeader.__add__`.
2991 """
2992 return self._add_instance_and_non_accept_encoding_type(
2993 instance=self, other=other, instance_on_the_right=True,
2994 )
2996 def __repr__(self):
2997 return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
2999 def __str__(self):
3000 r"""
3001 Return a tidied up version of the header value.
3003 e.g. If the ``header_value`` is ``",\t, a ;\t q=0.20 , b ,',"``,
3004 ``str(instance)`` returns ``"a;q=0.2, b, '"``.
3005 """
3006 return ', '.join(
3007 _item_qvalue_pair_to_header_element(pair=tuple_)
3008 for tuple_ in self.parsed
3009 )
3011 def _add_instance_and_non_accept_encoding_type(
3012 self, instance, other, instance_on_the_right=False,
3013 ):
3014 if not other:
3015 return self.__class__(header_value=instance.header_value)
3017 other_header_value = self._python_value_to_header_str(value=other)
3019 if other_header_value == '':
3020 # if ``other`` is an object whose type we don't recognise, and
3021 # str(other) returns ''
3022 return self.__class__(header_value=instance.header_value)
3024 try:
3025 self.parse(value=other_header_value)
3026 except ValueError: # invalid header value
3027 return self.__class__(header_value=instance.header_value)
3029 new_header_value = (
3030 (other_header_value + ', ' + instance.header_value)
3031 if instance_on_the_right
3032 else (instance.header_value + ', ' + other_header_value)
3033 )
3034 return self.__class__(header_value=new_header_value)
3036 def _old_match(self, mask, offer):
3037 """
3038 Return whether content-coding offer matches codings header item.
3040 .. warning::
3042 This is maintained for backward compatibility, and will be
3043 deprecated in the future.
3045 This method was WebOb's old criterion for deciding whether a
3046 content-coding offer matches a header item (content-coding,
3047 ``identity`` or ``*``), used in
3049 - :meth:`AcceptCharsetValidHeader.__contains__`
3050 - :meth:`AcceptCharsetValidHeader.best_match`
3051 - :meth:`AcceptCharsetValidHeader.quality`
3053 It does not conform to :rfc:`RFC 7231, section 5.3.4
3054 <7231#section-5.3.4>` in that it does not interpret ``*`` values in the
3055 header correctly: ``*`` should only match content-codings not mentioned
3056 elsewhere in the header.
3057 """
3058 return mask == '*' or offer.lower() == mask.lower()
3060 def acceptable_offers(self, offers):
3061 """
3062 Return the offers that are acceptable according to the header.
3064 The offers are returned in descending order of preference, where
3065 preference is indicated by the qvalue of the item (content-coding,
3066 "identity" or "*") in the header that matches the offer.
3068 This uses the matching rules described in :rfc:`RFC 7231, section 5.3.4
3069 <7231#section-5.3.4>`.
3071 :param offers: ``iterable`` of ``str``s, where each ``str`` is a
3072 content-coding or the string ``identity`` (the token
3073 used to represent "no encoding")
3074 :return: A list of tuples of the form (content-coding or "identity",
3075 qvalue), in descending order of qvalue. Where two offers have
3076 the same qvalue, they are returned in the same order as their
3077 order in `offers`.
3079 Use the string ``'identity'`` (without the quotes) in `offers` to
3080 indicate an offer with no content-coding. From the RFC: 'If the
3081 representation has no content-coding, then it is acceptable by default
3082 unless specifically excluded by the Accept-Encoding field stating
3083 either "identity;q=0" or "\\*;q=0" without a more specific entry for
3084 "identity".' The RFC does not specify the qvalue that should be
3085 assigned to the representation/offer with no content-coding; this
3086 implementation assigns it a qvalue of 1.0.
3087 """
3088 lowercased_parsed = [
3089 (codings.lower(), qvalue) for (codings, qvalue) in self.parsed
3090 ]
3091 lowercased_offers = [offer.lower() for offer in offers]
3093 not_acceptable_codingss = set()
3094 acceptable_codingss = dict()
3095 asterisk_qvalue = None
3097 for codings, qvalue in lowercased_parsed:
3098 if codings == '*':
3099 if asterisk_qvalue is None:
3100 asterisk_qvalue = qvalue
3101 elif (
3102 codings not in acceptable_codingss and codings not in
3103 not_acceptable_codingss
3104 # if we have not already encountered this codings in the header
3105 ):
3106 if qvalue == 0.0:
3107 not_acceptable_codingss.add(codings)
3108 else:
3109 acceptable_codingss[codings] = qvalue
3110 acceptable_codingss = list(acceptable_codingss.items())
3111 # Sort acceptable_codingss by qvalue, descending order
3112 acceptable_codingss.sort(key=lambda tuple_: tuple_[1], reverse=True)
3114 filtered_offers = []
3115 for index, offer in enumerate(lowercased_offers):
3116 # If offer matches a non-* codings with q=0, it is filtered out
3117 if any((
3118 (offer == codings) for codings in not_acceptable_codingss
3119 )):
3120 continue
3122 matched_codings_qvalue = None
3123 for codings, qvalue in acceptable_codingss:
3124 if offer == codings:
3125 matched_codings_qvalue = qvalue
3126 break
3127 else:
3128 if asterisk_qvalue:
3129 matched_codings_qvalue = asterisk_qvalue
3130 elif asterisk_qvalue != 0.0 and offer == 'identity':
3131 matched_codings_qvalue = 1.0
3132 if matched_codings_qvalue is not None: # if there was a match
3133 filtered_offers.append((
3134 offers[index], matched_codings_qvalue, index
3135 ))
3137 # sort by position in `offers` argument, ascending
3138 filtered_offers.sort(key=lambda tuple_: tuple_[2])
3139 # When qvalues are tied, position in `offers` is the tiebreaker.
3141 # sort by qvalue, descending
3142 filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True)
3144 return [(item[0], item[1]) for item in filtered_offers]
3145 # (offer, qvalue), dropping the position
3147 def best_match(self, offers, default_match=None):
3148 """
3149 Return the best match from the sequence of `offers`.
3151 .. warning::
3153 This is currently maintained for backward compatibility, and will be
3154 deprecated in the future.
3156 :meth:`AcceptEncodingValidHeader.best_match` uses its own algorithm
3157 (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
3158 best match. The algorithm has many issues, and does not conform to
3159 the RFC.
3161 Each offer in `offers` is checked against each non-``q=0`` item
3162 (content-coding/``identity``/``*``) in the header. If the two are a
3163 match according to WebOb's old criterion for a match, the quality value
3164 of the match is the qvalue of the item from the header multiplied by
3165 the server quality value of the offer (if the server quality value is
3166 not supplied, it is 1).
3168 The offer in the match with the highest quality value is the best
3169 match. If there is more than one match with the highest qvalue, the one
3170 that shows up first in `offers` is the best match.
3172 :param offers: (iterable)
3174 | Each item in the iterable may be a ``str`` *codings*,
3175 or a (*codings*, server quality value) ``tuple`` or
3176 ``list``, where *codings* is either a content-coding,
3177 or the string ``identity`` (which represents *no
3178 encoding*). ``str`` and ``tuple``/``list`` elements
3179 may be mixed within the iterable.
3181 :param default_match: (optional, any type) the value to be returned if
3182 there is no match
3184 :return: (``str``, or the type of `default_match`)
3186 | The offer that is the best match. If there is no match, the
3187 value of `default_match` is returned.
3189 This method does not conform to :rfc:`RFC 7231, section 5.3.4
3190 <7231#section-5.3.4>`, in that it does not correctly interpret ``*``::
3192 >>> AcceptEncodingValidHeader('gzip;q=0, *').best_match(['gzip'])
3193 'gzip'
3195 and does not handle the ``identity`` token correctly::
3197 >>> instance = AcceptEncodingValidHeader('gzip')
3198 >>> instance.best_match(['identity']) is None
3199 True
3200 """
3201 warnings.warn(
3202 'The behavior of AcceptEncodingValidHeader.best_match is '
3203 'currently being maintained for backward compatibility, but it '
3204 'will be deprecated in the future, as it does not conform to the'
3205 ' RFC.',
3206 DeprecationWarning,
3207 )
3208 best_quality = -1
3209 best_offer = default_match
3210 matched_by = '*/*'
3211 for offer in offers:
3212 if isinstance(offer, (tuple, list)):
3213 offer, server_quality = offer
3214 else:
3215 server_quality = 1
3216 for item in self._parsed_nonzero:
3217 mask = item[0]
3218 quality = item[1]
3219 possible_quality = server_quality * quality
3220 if possible_quality < best_quality:
3221 continue
3222 elif possible_quality == best_quality:
3223 # 'text/plain' overrides 'message/*' overrides '*/*'
3224 # (if all match w/ the same q=)
3225 # [We can see that this was written for the Accept header,
3226 # not the Accept-Encoding header.]
3227 if matched_by.count('*') <= mask.count('*'):
3228 continue
3229 if self._old_match(mask, offer):
3230 best_quality = possible_quality
3231 best_offer = offer
3232 matched_by = mask
3233 return best_offer
3235 def quality(self, offer):
3236 """
3237 Return quality value of given offer, or ``None`` if there is no match.
3239 .. warning::
3241 This is currently maintained for backward compatibility, and will be
3242 deprecated in the future.
3244 :param offer: (``str``) A content-coding, or ``identity``.
3245 :return: (``float`` or ``None``)
3247 | The quality value from the header item
3248 (content-coding/``identity``/``*``) that matches the
3249 `offer`, or ``None`` if there is no match.
3251 The behavior of this method does not conform to :rfc:`RFC 7231, section
3252 5.3.4<7231#section-5.3.4>`, in that it does not correctly interpret
3253 ``*``::
3255 >>> AcceptEncodingValidHeader('gzip;q=0, *').quality('gzip')
3256 1.0
3258 and does not handle the ``identity`` token correctly::
3260 >>> AcceptEncodingValidHeader('gzip').quality('identity') is None
3261 True
3262 """
3263 warnings.warn(
3264 'The behavior of AcceptEncodingValidHeader.quality is currently '
3265 'being maintained for backward compatibility, but it will be '
3266 'deprecated in the future, as it does not conform to the RFC.',
3267 DeprecationWarning,
3268 )
3269 bestq = 0
3270 for mask, q in self.parsed:
3271 if self._old_match(mask, offer):
3272 bestq = max(bestq, q)
3273 return bestq or None
3276class _AcceptEncodingInvalidOrNoHeader(AcceptEncoding):
3277 """
3278 Represent when an ``Accept-Encoding`` header is invalid or not in request.
3280 This is the base class for the behaviour that
3281 :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader`
3282 have in common.
3284 :rfc:`7231` does not provide any guidance on what should happen if the
3285 ``AcceptEncoding`` header has an invalid value. This implementation
3286 disregards the header when the header is invalid, so
3287 :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader`
3288 have much behaviour in common.
3289 """
3291 def __bool__(self):
3292 """
3293 Return whether ``self`` represents a valid ``Accept-Encoding`` header.
3295 Return ``True`` if ``self`` represents a valid header, and ``False`` if
3296 it represents an invalid header, or the header not being in the
3297 request.
3299 For this class, it always returns ``False``.
3300 """
3301 return False
3302 __nonzero__ = __bool__ # Python 2
3304 def __contains__(self, offer):
3305 """
3306 Return ``bool`` indicating whether `offer` is acceptable.
3308 .. warning::
3310 The behavior of ``.__contains__`` for the ``Accept-Encoding``
3311 classes is currently being maintained for backward compatibility,
3312 but it will change in the future to better conform to the RFC.
3314 :param offer: (``str``) a content-coding or ``identity`` offer
3315 :return: (``bool``) Whether ``offer`` is acceptable according to the
3316 header.
3318 For this class, either there is no ``Accept-Encoding`` header in the
3319 request, or the header is invalid, so any content-coding is acceptable,
3320 and this always returns ``True``.
3321 """
3322 warnings.warn(
3323 'The behavior of .__contains__ for the Accept-Encoding classes is '
3324 'currently being maintained for backward compatibility, but it '
3325 'will change in the future to better conform to the RFC.',
3326 DeprecationWarning,
3327 )
3328 return True
3330 def __iter__(self):
3331 """
3332 Return all the header items with non-0 qvalues, in order of preference.
3334 .. warning::
3336 The behavior of this method is currently maintained for backward
3337 compatibility, but will change in the future.
3339 :return: iterator of all the (content-coding/``identity``/``*``) items
3340 in the header with non-0 qvalues, in descending order of
3341 qvalue. If two items have the same qvalue, they are returned
3342 in the order of their positions in the header, from left to
3343 right.
3345 When there is no ``Accept-Encoding`` header in the request or the
3346 header is invalid, there are no items in the header, so this always
3347 returns an empty iterator.
3348 """
3349 warnings.warn(
3350 'The behavior of AcceptEncodingValidHeader.__iter__ is currently '
3351 'maintained for backward compatibility, but will change in the '
3352 'future.',
3353 DeprecationWarning,
3354 )
3355 return iter(())
3357 def acceptable_offers(self, offers):
3358 """
3359 Return the offers that are acceptable according to the header.
3361 :param offers: ``iterable`` of ``str``s, where each ``str`` is a
3362 content-coding or the string ``identity`` (the token
3363 used to represent "no encoding")
3364 :return: When the header is invalid, or there is no ``Accept-Encoding``
3365 header in the request, all `offers` are considered acceptable,
3366 so this method returns a list of (content-coding or
3367 "identity", qvalue) tuples where each offer in `offers` is
3368 paired with the qvalue of 1.0, in the same order as in
3369 `offers`.
3370 """
3371 return [(offer, 1.0) for offer in offers]
3373 def best_match(self, offers, default_match=None):
3374 """
3375 Return the best match from the sequence of `offers`.
3377 This is the ``.best_match()`` method for when the header is invalid or
3378 not found in the request, corresponding to
3379 :meth:`AcceptEncodingValidHeader.best_match`.
3381 .. warning::
3383 This is currently maintained for backward compatibility, and will be
3384 deprecated in the future (see the documentation for
3385 :meth:`AcceptEncodingValidHeader.best_match`).
3387 When the header is invalid, or there is no `Accept-Encoding` header in
3388 the request, all `offers` are considered acceptable, so the best match
3389 is the offer in `offers` with the highest server quality value (if the
3390 server quality value is not supplied for a media type, it is 1).
3392 If more than one offer in `offers` have the same highest server quality
3393 value, then the one that shows up first in `offers` is the best match.
3395 :param offers: (iterable)
3397 | Each item in the iterable may be a ``str`` *codings*,
3398 or a (*codings*, server quality value) ``tuple`` or
3399 ``list``, where *codings* is either a content-coding,
3400 or the string ``identity`` (which represents *no
3401 encoding*). ``str`` and ``tuple``/``list`` elements
3402 may be mixed within the iterable.
3404 :param default_match: (optional, any type) the value to be returned if
3405 `offers` is empty.
3407 :return: (``str``, or the type of `default_match`)
3409 | The offer that has the highest server quality value. If
3410 `offers` is empty, the value of `default_match` is returned.
3411 """
3412 warnings.warn(
3413 'The behavior of .best_match for the Accept-Encoding classes is '
3414 'currently being maintained for backward compatibility, but the '
3415 'method will be deprecated in the future, as its behavior is not '
3416 'specified in (and currently does not conform to) RFC 7231.',
3417 DeprecationWarning,
3418 )
3419 best_quality = -1
3420 best_offer = default_match
3421 for offer in offers:
3422 if isinstance(offer, (list, tuple)):
3423 offer, quality = offer
3424 else:
3425 quality = 1
3426 if quality > best_quality:
3427 best_offer = offer
3428 best_quality = quality
3429 return best_offer
3431 def quality(self, offer):
3432 """
3433 Return quality value of given offer, or ``None`` if there is no match.
3435 This is the ``.quality()`` method for when the header is invalid or not
3436 found in the request, corresponding to
3437 :meth:`AcceptEncodingValidHeader.quality`.
3439 .. warning::
3441 This is currently maintained for backward compatibility, and will be
3442 deprecated in the future (see the documentation for
3443 :meth:`AcceptEncodingValidHeader.quality`).
3445 :param offer: (``str``) A content-coding, or ``identity``.
3446 :return: (``float``) ``1.0``.
3448 When the ``Accept-Encoding`` header is invalid or not in the request,
3449 all offers are equally acceptable, so 1.0 is always returned.
3450 """
3451 warnings.warn(
3452 'The behavior of .quality for the Accept-Encoding classes is '
3453 'currently being maintained for backward compatibility, but the '
3454 'method will be deprecated in the future, as its behavior does '
3455 'not conform to RFC 7231.',
3456 DeprecationWarning,
3457 )
3458 return 1.0
3461class AcceptEncodingNoHeader(_AcceptEncodingInvalidOrNoHeader):
3462 """
3463 Represent when there is no ``Accept-Encoding`` header in the request.
3465 This object should not be modified. To add to the header, we can use the
3466 addition operators (``+`` and ``+=``), which return a new object (see the
3467 docstring for :meth:`AcceptEncodingNoHeader.__add__`).
3468 """
3470 @property
3471 def header_value(self):
3472 """
3473 (``str`` or ``None``) The header value.
3475 As there is no header in the request, this is ``None``.
3476 """
3477 return self._header_value
3479 @property
3480 def parsed(self):
3481 """
3482 (``list`` or ``None``) Parsed form of the header.
3484 As there is no header in the request, this is ``None``.
3485 """
3486 return self._parsed
3488 def __init__(self):
3489 """
3490 Create an :class:`AcceptEncodingNoHeader` instance.
3491 """
3492 self._header_value = None
3493 self._parsed = None
3494 self._parsed_nonzero = None
3496 def copy(self):
3497 """
3498 Create a copy of the header object.
3500 """
3501 return self.__class__()
3503 def __add__(self, other):
3504 """
3505 Add to header, creating a new header object.
3507 `other` can be:
3509 * ``None``
3510 * a ``str`` header value
3511 * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
3512 keys, and qvalue ``float``'s as values
3513 * a ``tuple`` or ``list``, where each item is either a header element
3514 ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
3515 or ``list``
3516 * an :class:`AcceptEncodingValidHeader`,
3517 :class:`AcceptEncodingNoHeader`, or
3518 :class:`AcceptEncodingInvalidHeader` instance
3519 * object of any other type that returns a value for ``__str__``
3521 If `other` is a valid header value or an
3522 :class:`AcceptEncodingValidHeader` instance, a new
3523 :class:`AcceptEncodingValidHeader` instance with the valid header value
3524 is returned.
3526 If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an
3527 invalid header value, or an :class:`AcceptEncodingInvalidHeader`
3528 instance, a new :class:`AcceptEncodingNoHeader` instance is returned.
3529 """
3530 if isinstance(other, AcceptEncodingValidHeader):
3531 return AcceptEncodingValidHeader(header_value=other.header_value)
3533 if isinstance(
3534 other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
3535 ):
3536 return self.__class__()
3538 return self._add_instance_and_non_accept_encoding_type(
3539 instance=self, other=other,
3540 )
3542 def __radd__(self, other):
3543 """
3544 Add to header, creating a new header object.
3546 See the docstring for :meth:`AcceptEncodingNoHeader.__add__`.
3547 """
3548 return self.__add__(other=other)
3550 def __repr__(self):
3551 return '<{}>'.format(self.__class__.__name__)
3553 def __str__(self):
3554 """Return the ``str`` ``'<no header in request>'``."""
3555 return '<no header in request>'
3557 def _add_instance_and_non_accept_encoding_type(self, instance, other):
3558 if other is None:
3559 return self.__class__()
3561 other_header_value = self._python_value_to_header_str(value=other)
3563 try:
3564 return AcceptEncodingValidHeader(header_value=other_header_value)
3565 except ValueError: # invalid header value
3566 return self.__class__()
3569class AcceptEncodingInvalidHeader(_AcceptEncodingInvalidOrNoHeader):
3570 """
3571 Represent an invalid ``Accept-Encoding`` header.
3573 An invalid header is one that does not conform to
3574 :rfc:`7231#section-5.3.4`.
3576 :rfc:`7231` does not provide any guidance on what should happen if the
3577 ``Accept-Encoding`` header has an invalid value. This implementation
3578 disregards the header, and treats it as if there is no ``Accept-Encoding``
3579 header in the request.
3581 This object should not be modified. To add to the header, we can use the
3582 addition operators (``+`` and ``+=``), which return a new object (see the
3583 docstring for :meth:`AcceptEncodingInvalidHeader.__add__`).
3584 """
3586 @property
3587 def header_value(self):
3588 """(``str`` or ``None``) The header value."""
3589 return self._header_value
3591 @property
3592 def parsed(self):
3593 """
3594 (``list`` or ``None``) Parsed form of the header.
3596 As the header is invalid and cannot be parsed, this is ``None``.
3597 """
3598 return self._parsed
3600 def __init__(self, header_value):
3601 """
3602 Create an :class:`AcceptEncodingInvalidHeader` instance.
3603 """
3604 self._header_value = header_value
3605 self._parsed = None
3606 self._parsed_nonzero = None
3608 def copy(self):
3609 """
3610 Create a copy of the header object.
3612 """
3613 return self.__class__(self._header_value)
3615 def __add__(self, other):
3616 """
3617 Add to header, creating a new header object.
3619 `other` can be:
3621 * ``None``
3622 * a ``str`` header value
3623 * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
3624 keys, and qvalue ``float``'s as values
3625 * a ``tuple`` or ``list``, where each item is either a header element
3626 ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
3627 or ``list``
3628 * an :class:`AcceptEncodingValidHeader`,
3629 :class:`AcceptEncodingNoHeader`, or
3630 :class:`AcceptEncodingInvalidHeader` instance
3631 * object of any other type that returns a value for ``__str__``
3633 If `other` is a valid header value or an
3634 :class:`AcceptEncodingValidHeader` instance, then a new
3635 :class:`AcceptEncodingValidHeader` instance with the valid header value
3636 is returned.
3638 If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an
3639 invalid header value, or an :class:`AcceptEncodingInvalidHeader`
3640 instance, a new :class:`AcceptEncodingNoHeader` instance is returned.
3641 """
3642 if isinstance(other, AcceptEncodingValidHeader):
3643 return AcceptEncodingValidHeader(header_value=other.header_value)
3645 if isinstance(
3646 other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
3647 ):
3648 return AcceptEncodingNoHeader()
3650 return self._add_instance_and_non_accept_encoding_type(
3651 instance=self, other=other,
3652 )
3654 def __radd__(self, other):
3655 """
3656 Add to header, creating a new header object.
3658 See the docstring for :meth:`AcceptEncodingValidHeader.__add__`.
3659 """
3660 return self._add_instance_and_non_accept_encoding_type(
3661 instance=self, other=other, instance_on_the_right=True,
3662 )
3664 def __repr__(self):
3665 return '<{}>'.format(self.__class__.__name__)
3666 # We do not display the header_value, as it is untrusted input. The
3667 # header_value could always be easily obtained from the .header_value
3668 # property.
3670 def __str__(self):
3671 """Return the ``str`` ``'<invalid header value>'``."""
3672 return '<invalid header value>'
3674 def _add_instance_and_non_accept_encoding_type(
3675 self, instance, other, instance_on_the_right=False,
3676 ):
3677 if other is None:
3678 return AcceptEncodingNoHeader()
3680 other_header_value = self._python_value_to_header_str(value=other)
3682 try:
3683 return AcceptEncodingValidHeader(header_value=other_header_value)
3684 except ValueError: # invalid header value
3685 return AcceptEncodingNoHeader()
3688def create_accept_encoding_header(header_value):
3689 """
3690 Create an object representing the ``Accept-Encoding`` header in a request.
3692 :param header_value: (``str``) header value
3693 :return: If `header_value` is ``None``, an :class:`AcceptEncodingNoHeader`
3694 instance.
3696 | If `header_value` is a valid ``Accept-Encoding`` header, an
3697 :class:`AcceptEncodingValidHeader` instance.
3699 | If `header_value` is an invalid ``Accept-Encoding`` header, an
3700 :class:`AcceptEncodingInvalidHeader` instance.
3701 """
3702 if header_value is None:
3703 return AcceptEncodingNoHeader()
3704 if isinstance(header_value, AcceptEncoding):
3705 return header_value.copy()
3706 try:
3707 return AcceptEncodingValidHeader(header_value=header_value)
3708 except ValueError:
3709 return AcceptEncodingInvalidHeader(header_value=header_value)
3712def accept_encoding_property():
3713 doc = """
3714 Property representing the ``Accept-Encoding`` header.
3716 (:rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`)
3718 The header value in the request environ is parsed and a new object
3719 representing the header is created every time we *get* the value of the
3720 property. (*set* and *del* change the header value in the request
3721 environ, and do not involve parsing.)
3722 """
3724 ENVIRON_KEY = 'HTTP_ACCEPT_ENCODING'
3726 def fget(request):
3727 """Get an object representing the header in the request."""
3728 return create_accept_encoding_header(
3729 header_value=request.environ.get(ENVIRON_KEY)
3730 )
3732 def fset(request, value):
3733 """
3734 Set the corresponding key in the request environ.
3736 `value` can be:
3738 * ``None``
3739 * a ``str`` header value
3740 * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
3741 keys, and qvalue ``float``'s as values
3742 * a ``tuple`` or ``list``, where each item is either a header element
3743 ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
3744 or ``list``
3745 * an :class:`AcceptEncodingValidHeader`,
3746 :class:`AcceptEncodingNoHeader`, or
3747 :class:`AcceptEncodingInvalidHeader` instance
3748 * object of any other type that returns a value for ``__str__``
3749 """
3750 if value is None or isinstance(value, AcceptEncodingNoHeader):
3751 fdel(request=request)
3752 else:
3753 if isinstance(
3754 value, (AcceptEncodingValidHeader, AcceptEncodingInvalidHeader)
3755 ):
3756 header_value = value.header_value
3757 else:
3758 header_value = AcceptEncoding._python_value_to_header_str(
3759 value=value,
3760 )
3761 request.environ[ENVIRON_KEY] = header_value
3763 def fdel(request):
3764 """Delete the corresponding key from the request environ."""
3765 try:
3766 del request.environ[ENVIRON_KEY]
3767 except KeyError:
3768 pass
3770 return property(fget, fset, fdel, textwrap.dedent(doc))
3773class AcceptLanguage(object):
3774 """
3775 Represent an ``Accept-Language`` header.
3777 Base class for :class:`AcceptLanguageValidHeader`,
3778 :class:`AcceptLanguageNoHeader`, and :class:`AcceptLanguageInvalidHeader`.
3779 """
3781 # RFC 7231 Section 5.3.5 "Accept-Language":
3782 # Accept-Language = 1#( language-range [ weight ] )
3783 # language-range =
3784 # <language-range, see [RFC4647], Section 2.1>
3785 # RFC 4647 Section 2.1 "Basic Language Range":
3786 # language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
3787 # alphanum = ALPHA / DIGIT
3788 lang_range_re = (
3789 r'\*|'
3790 '(?:'
3791 '[A-Za-z]{1,8}'
3792 '(?:-[A-Za-z0-9]{1,8})*'
3793 ')'
3794 )
3795 lang_range_n_weight_re = _item_n_weight_re(item_re=lang_range_re)
3796 lang_range_n_weight_compiled_re = re.compile(lang_range_n_weight_re)
3797 accept_language_compiled_re = _list_1_or_more__compiled_re(
3798 element_re=lang_range_n_weight_re,
3799 )
3801 @classmethod
3802 def _python_value_to_header_str(cls, value):
3803 if isinstance(value, str):
3804 header_str = value
3805 else:
3806 if hasattr(value, 'items'):
3807 value = sorted(
3808 value.items(),
3809 key=lambda item: item[1],
3810 reverse=True,
3811 )
3812 if isinstance(value, (tuple, list)):
3813 result = []
3814 for element in value:
3815 if isinstance(element, (tuple, list)):
3816 element = _item_qvalue_pair_to_header_element(
3817 pair=element
3818 )
3819 result.append(element)
3820 header_str = ', '.join(result)
3821 else:
3822 header_str = str(value)
3823 return header_str
3825 @classmethod
3826 def parse(cls, value):
3827 """
3828 Parse an ``Accept-Language`` header.
3830 :param value: (``str``) header value
3831 :return: If `value` is a valid ``Accept-Language`` header, returns an
3832 iterator of (language range, quality value) tuples, as parsed
3833 from the header from left to right.
3834 :raises ValueError: if `value` is an invalid header
3835 """
3836 # Check if header is valid
3837 # Using Python stdlib's `re` module, there is currently no way to check
3838 # the match *and* get all the groups using the same regex, so we have
3839 # to use one regex to check the match, and another to get the groups.
3840 if cls.accept_language_compiled_re.match(value) is None:
3841 raise ValueError('Invalid value for an Accept-Language header.')
3842 def generator(value):
3843 for match in (
3844 cls.lang_range_n_weight_compiled_re.finditer(value)
3845 ):
3846 lang_range = match.group(1)
3847 qvalue = match.group(2)
3848 qvalue = float(qvalue) if qvalue else 1.0
3849 yield (lang_range, qvalue)
3850 return generator(value=value)
3853class AcceptLanguageValidHeader(AcceptLanguage):
3854 """
3855 Represent a valid ``Accept-Language`` header.
3857 A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.5
3858 <7231#section-5.3.5>`.
3860 We take the reference from the ``language-range`` syntax rule in :rfc:`RFC
3861 7231, section 5.3.5 <7231#section-5.3.5>` to :rfc:`RFC 4647, section 2.1
3862 <4647#section-2.1>` to mean that only basic language ranges (and not
3863 extended language ranges) are expected in the ``Accept-Language`` header.
3865 This object should not be modified. To add to the header, we can use the
3866 addition operators (``+`` and ``+=``), which return a new object (see the
3867 docstring for :meth:`AcceptLanguageValidHeader.__add__`).
3868 """
3870 def __init__(self, header_value):
3871 """
3872 Create an :class:`AcceptLanguageValidHeader` instance.
3874 :param header_value: (``str``) header value.
3875 :raises ValueError: if `header_value` is an invalid value for an
3876 ``Accept-Language`` header.
3877 """
3878 self._header_value = header_value
3879 self._parsed = list(self.parse(header_value))
3880 self._parsed_nonzero = [item for item in self.parsed if item[1]]
3881 # item[1] is the qvalue
3883 def copy(self):
3884 """
3885 Create a copy of the header object.
3887 """
3888 return self.__class__(self._header_value)
3890 @property
3891 def header_value(self):
3892 """(``str`` or ``None``) The header value."""
3893 return self._header_value
3895 @property
3896 def parsed(self):
3897 """
3898 (``list`` or ``None``) Parsed form of the header.
3900 A list of (language range, quality value) tuples.
3901 """
3902 return self._parsed
3904 def __add__(self, other):
3905 """
3906 Add to header, creating a new header object.
3908 `other` can be:
3910 * ``None``
3911 * a ``str``
3912 * a ``dict``, with language ranges as keys and qvalues as values
3913 * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
3914 or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
3915 be mixed within the ``tuple`` or ``list``)
3916 * an :class:`AcceptLanguageValidHeader`,
3917 :class:`AcceptLanguageNoHeader`, or
3918 :class:`AcceptLanguageInvalidHeader` instance
3919 * object of any other type that returns a value for ``__str__``
3921 If `other` is a valid header value or another
3922 :class:`AcceptLanguageValidHeader` instance, the two header values are
3923 joined with ``', '``, and a new :class:`AcceptLanguageValidHeader`
3924 instance with the new header value is returned.
3926 If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
3927 invalid header value, or an :class:`AcceptLanguageInvalidHeader`
3928 instance, a new :class:`AcceptLanguageValidHeader` instance with the
3929 same header value as ``self`` is returned.
3930 """
3931 if isinstance(other, AcceptLanguageValidHeader):
3932 return create_accept_language_header(
3933 header_value=self.header_value + ', ' + other.header_value,
3934 )
3936 if isinstance(
3937 other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
3938 ):
3939 return self.__class__(header_value=self.header_value)
3941 return self._add_instance_and_non_accept_language_type(
3942 instance=self, other=other,
3943 )
3945 def __nonzero__(self):
3946 """
3947 Return whether ``self`` represents a valid ``Accept-Language`` header.
3949 Return ``True`` if ``self`` represents a valid header, and ``False`` if
3950 it represents an invalid header, or the header not being in the
3951 request.
3953 For this class, it always returns ``True``.
3954 """
3955 return True
3956 __bool__ = __nonzero__ # Python 3
3958 def __contains__(self, offer):
3959 """
3960 Return ``bool`` indicating whether `offer` is acceptable.
3962 .. warning::
3964 The behavior of :meth:`AcceptLanguageValidHeader.__contains__` is
3965 currently being maintained for backward compatibility, but it will
3966 change in the future to better conform to the RFC.
3968 What is 'acceptable' depends on the needs of your application.
3969 :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests three
3970 matching schemes from :rfc:`RFC 4647 <4647>`, two of which WebOb
3971 supports with :meth:`AcceptLanguageValidHeader.basic_filtering` and
3972 :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to
3973 mean that Extended Filtering cannot apply for the
3974 ``Accept-Language`` header, as the header only accepts basic
3975 language ranges.) If these are not suitable for the needs of your
3976 application, you may need to write your own matching using
3977 :attr:`AcceptLanguageValidHeader.parsed`.
3979 :param offer: (``str``) language tag offer
3980 :return: (``bool``) Whether ``offer`` is acceptable according to the
3981 header.
3983 This uses the old criterion of a match in
3984 :meth:`AcceptLanguageValidHeader._old_match`, which does not conform to
3985 :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` or any of the
3986 matching schemes suggested there. It also does not properly take into
3987 account ranges with ``q=0`` in the header::
3989 >>> 'en-gb' in AcceptLanguageValidHeader('en, en-gb;q=0')
3990 True
3991 >>> 'en' in AcceptLanguageValidHeader('en;q=0, *')
3992 True
3994 (See the docstring for :meth:`AcceptLanguageValidHeader._old_match` for
3995 other problems with the old criterion for a match.)
3996 """
3997 warnings.warn(
3998 'The behavior of AcceptLanguageValidHeader.__contains__ is '
3999 'currently being maintained for backward compatibility, but it '
4000 'will change in the future to better conform to the RFC.',
4001 DeprecationWarning,
4002 )
4003 for mask, quality in self._parsed_nonzero:
4004 if self._old_match(mask, offer):
4005 return True
4006 return False
4008 def __iter__(self):
4009 """
4010 Return all the ranges with non-0 qvalues, in order of preference.
4012 .. warning::
4014 The behavior of this method is currently maintained for backward
4015 compatibility, but will change in the future.
4017 :return: iterator of all the language ranges in the header with non-0
4018 qvalues, in descending order of qvalue. If two ranges have the
4019 same qvalue, they are returned in the order of their positions
4020 in the header, from left to right.
4022 Please note that this is a simple filter for the ranges in the header
4023 with non-0 qvalues, and is not necessarily the same as what the client
4024 prefers, e.g. ``'en-gb;q=0, *'`` means 'everything but British
4025 English', but ``list(instance)`` would return only ``['*']``.
4026 """
4027 warnings.warn(
4028 'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
4029 'maintained for backward compatibility, but will change in the '
4030 'future.',
4031 DeprecationWarning,
4032 )
4034 for m, q in sorted(
4035 self._parsed_nonzero,
4036 key=lambda i: i[1],
4037 reverse=True
4038 ):
4039 yield m
4041 def __radd__(self, other):
4042 """
4043 Add to header, creating a new header object.
4045 See the docstring for :meth:`AcceptLanguageValidHeader.__add__`.
4046 """
4047 return self._add_instance_and_non_accept_language_type(
4048 instance=self, other=other, instance_on_the_right=True,
4049 )
4051 def __repr__(self):
4052 return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
4054 def __str__(self):
4055 r"""
4056 Return a tidied up version of the header value.
4058 e.g. If the ``header_value`` is ``', \t,de;q=0.000 \t, es;q=1.000, zh,
4059 jp;q=0.210 ,'``, ``str(instance)`` returns ``'de;q=0, es, zh,
4060 jp;q=0.21'``.
4061 """
4062 return ', '.join(
4063 _item_qvalue_pair_to_header_element(pair=tuple_)
4064 for tuple_ in self.parsed
4065 )
4067 def _add_instance_and_non_accept_language_type(
4068 self, instance, other, instance_on_the_right=False,
4069 ):
4070 if not other:
4071 return self.__class__(header_value=instance.header_value)
4073 other_header_value = self._python_value_to_header_str(value=other)
4075 try:
4076 self.parse(value=other_header_value)
4077 except ValueError: # invalid header value
4078 return self.__class__(header_value=instance.header_value)
4080 new_header_value = (
4081 (other_header_value + ', ' + instance.header_value)
4082 if instance_on_the_right
4083 else (instance.header_value + ', ' + other_header_value)
4084 )
4085 return self.__class__(header_value=new_header_value)
4087 def _old_match(self, mask, item):
4088 """
4089 Return whether a language tag matches a language range.
4091 .. warning::
4093 This is maintained for backward compatibility, and will be
4094 deprecated in the future.
4096 This method was WebOb's old criterion for deciding whether a language
4097 tag matches a language range, used in
4099 - :meth:`AcceptLanguageValidHeader.__contains__`
4100 - :meth:`AcceptLanguageValidHeader.best_match`
4101 - :meth:`AcceptLanguageValidHeader.quality`
4103 It does not conform to :rfc:`RFC 7231, section 5.3.5
4104 <7231#section-5.3.5>`, or any of the matching schemes suggested there.
4106 :param mask: (``str``)
4108 | language range
4110 :param item: (``str``)
4112 | language tag. Subtags in language tags are separated by
4113 ``-`` (hyphen). If there are underscores (``_``) in this
4114 argument, they will be converted to hyphens before
4115 checking the match.
4117 :return: (``bool``) whether the tag in `item` matches the range in
4118 `mask`.
4120 `mask` and `item` are a match if:
4122 - ``mask == *``.
4123 - ``mask == item``.
4124 - If the first subtag of `item` equals `mask`, or if the first subtag
4125 of `mask` equals `item`.
4126 This means that::
4128 >>> instance._old_match(mask='en-gb', item='en')
4129 True
4130 >>> instance._old_match(mask='en', item='en-gb')
4131 True
4133 Which is different from any of the matching schemes suggested in
4134 :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, in that none of
4135 those schemes match both more *and* less specific tags.
4137 However, this method appears to be only designed for language tags
4138 and ranges with at most two subtags. So with an `item`/language tag
4139 with more than two subtags like ``zh-Hans-CN``::
4141 >>> instance._old_match(mask='zh', item='zh-Hans-CN')
4142 True
4143 >>> instance._old_match(mask='zh-Hans', item='zh-Hans-CN')
4144 False
4146 From commit history, this does not appear to have been from a
4147 decision to match only the first subtag, but rather because only
4148 language ranges and tags with at most two subtags were expected.
4149 """
4150 item = item.replace('_', '-').lower()
4151 mask = mask.lower()
4152 return (mask == '*'
4153 or item == mask
4154 or item.split('-')[0] == mask
4155 or item == mask.split('-')[0]
4156 )
4158 def basic_filtering(self, language_tags):
4159 """
4160 Return the tags that match the header, using Basic Filtering.
4162 This is an implementation of the Basic Filtering matching scheme,
4163 suggested as a matching scheme for the ``Accept-Language`` header in
4164 :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and defined in
4165 :rfc:`RFC 4647, section 3.3.1 <4647#section-3.3.1>`. It filters the
4166 tags in the `language_tags` argument and returns the ones that match
4167 the header according to the matching scheme.
4169 :param language_tags: (``iterable``) language tags
4170 :return: A list of tuples of the form (language tag, qvalue), in
4171 descending order of qvalue. If two or more tags have the same
4172 qvalue, they are returned in the same order as that in the
4173 header of the ranges they matched. If the matched range is the
4174 same for two or more tags (i.e. their matched ranges have the
4175 same qvalue and the same position in the header), then they
4176 are returned in the same order as that in the `language_tags`
4177 argument. If `language_tags` is unordered, e.g. if it is a set
4178 or a dict, then that order may not be reliable.
4180 For each tag in `language_tags`:
4182 1. If the tag matches a non-``*`` language range in the header with
4183 ``q=0`` (meaning "not acceptable", see :rfc:`RFC 7231, section 5.3.1
4184 <7231#section-5.3.1>`), the tag is filtered out.
4185 2. The non-``*`` language ranges in the header that do not have ``q=0``
4186 are considered in descending order of qvalue; where two or more
4187 language ranges have the same qvalue, they are considered in the
4188 order in which they appear in the header.
4189 3. A language range 'matches a particular language tag if, in a
4190 case-insensitive comparison, it exactly equals the tag, or if it
4191 exactly equals a prefix of the tag such that the first character
4192 following the prefix is "-".' (:rfc:`RFC 4647, section 3.3.1
4193 <4647#section-3.3.1>`)
4194 4. If the tag does not match any of the non-``*`` language ranges, and
4195 there is a ``*`` language range in the header, then if the ``*``
4196 language range has ``q=0``, the language tag is filtered out,
4197 otherwise the tag is considered a match.
4199 (If a range (``*`` or non-``*``) appears in the header more than once
4200 -- this would not make sense, but is nonetheless a valid header
4201 according to the RFC -- the first in the header is used for matching,
4202 and the others are ignored.)
4203 """
4204 # The Basic Filtering matching scheme as applied to the Accept-Language
4205 # header is very under-specified by RFCs 7231 and 4647. This
4206 # implementation combines the description of the matching scheme in RFC
4207 # 4647 and the rules of the Accept-Language header in RFC 7231 to
4208 # arrive at an algorithm for Basic Filtering as applied to the
4209 # Accept-Language header.
4211 lowercased_parsed = [
4212 (range_.lower(), qvalue) for (range_, qvalue) in self.parsed
4213 ]
4214 lowercased_tags = [tag.lower() for tag in language_tags]
4216 not_acceptable_ranges = set()
4217 acceptable_ranges = dict()
4218 asterisk_qvalue = None
4220 for position_in_header, (range_, qvalue) in enumerate(
4221 lowercased_parsed
4222 ):
4223 if range_ == '*':
4224 if asterisk_qvalue is None:
4225 asterisk_qvalue = qvalue
4226 asterisk_position = position_in_header
4227 elif (
4228 range_ not in acceptable_ranges and range_ not in
4229 not_acceptable_ranges
4230 # if we have not already encountered this range in the header
4231 ):
4232 if qvalue == 0.0:
4233 not_acceptable_ranges.add(range_)
4234 else:
4235 acceptable_ranges[range_] = (qvalue, position_in_header)
4236 acceptable_ranges = [
4237 (range_, qvalue, position_in_header)
4238 for range_, (qvalue, position_in_header)
4239 in acceptable_ranges.items()
4240 ]
4241 # Sort acceptable_ranges by position_in_header, ascending order
4242 acceptable_ranges.sort(key=lambda tuple_: tuple_[2])
4243 # Sort acceptable_ranges by qvalue, descending order
4244 acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True)
4245 # Sort guaranteed to be stable with Python >= 2.2, so position in
4246 # header is tiebreaker when two ranges have the same qvalue
4248 def match(tag, range_):
4249 # RFC 4647, section 2.1: 'A language range matches a particular
4250 # language tag if, in a case-insensitive comparison, it exactly
4251 # equals the tag, or if it exactly equals a prefix of the tag such
4252 # that the first character following the prefix is "-".'
4253 return (tag == range_) or tag.startswith(range_ + '-')
4254 # We can assume here that the language tags are valid tags, so we
4255 # do not have to worry about them being malformed and ending with
4256 # '-'.
4258 filtered_tags = []
4259 for index, tag in enumerate(lowercased_tags):
4260 # If tag matches a non-* range with q=0, it is filtered out
4261 if any((
4262 match(tag=tag, range_=range_)
4263 for range_ in not_acceptable_ranges
4264 )):
4265 continue
4267 matched_range_qvalue = None
4268 for range_, qvalue, position_in_header in acceptable_ranges:
4269 # acceptable_ranges is in descending order of qvalue, and tied
4270 # ranges are in ascending order of position_in_header, so the
4271 # first range_ that matches the tag is the best match
4272 if match(tag=tag, range_=range_):
4273 matched_range_qvalue = qvalue
4274 matched_range_position = position_in_header
4275 break
4276 else:
4277 if asterisk_qvalue:
4278 # From RFC 4647, section 3.3.1: '...HTTP/1.1 [RFC2616]
4279 # specifies that the range "*" matches only languages not
4280 # matched by any other range within an "Accept-Language"
4281 # header.' (Though RFC 2616 is obsolete, and there is no
4282 # mention of the meaning of "*" in RFC 7231, as the
4283 # ``language-range`` syntax rule in RFC 7231 section 5.3.1
4284 # directs us to RFC 4647, we can only assume that the
4285 # meaning of "*" in the Accept-Language header remains the
4286 # same).
4287 matched_range_qvalue = asterisk_qvalue
4288 matched_range_position = asterisk_position
4289 if matched_range_qvalue is not None: # if there was a match
4290 filtered_tags.append((
4291 language_tags[index], matched_range_qvalue,
4292 matched_range_position
4293 ))
4295 # sort by matched_range_position, ascending
4296 filtered_tags.sort(key=lambda tuple_: tuple_[2])
4297 # When qvalues are tied, matched range position in the header is the
4298 # tiebreaker.
4300 # sort by qvalue, descending
4301 filtered_tags.sort(key=lambda tuple_: tuple_[1], reverse=True)
4303 return [(item[0], item[1]) for item in filtered_tags]
4304 # (tag, qvalue), dropping the matched_range_position
4306 # We return a list of tuples with qvalues, instead of just a set or
4307 # a list of language tags, because
4308 # RFC 4647 section 3.3: "If the language priority list contains more
4309 # than one range, the content returned is typically ordered in
4310 # descending level of preference, but it MAY be unordered, according to
4311 # the needs of the application or protocol."
4312 # We return the filtered tags in order of preference, each paired with
4313 # the qvalue of the range that was their best match, as the ordering
4314 # and the qvalues may well be needed in some applications, and a simple
4315 # set or list of language tags can always be easily obtained from the
4316 # returned list if the qvalues are not required. One use for qvalues,
4317 # for example, would be to indicate that two tags are equally preferred
4318 # (same qvalue), which we would not be able to do easily with a set or
4319 # a list without e.g. making a member of the set or list a sequence.
4321 def best_match(self, offers, default_match=None):
4322 """
4323 Return the best match from the sequence of language tag `offers`.
4325 .. warning::
4327 This is currently maintained for backward compatibility, and will be
4328 deprecated in the future.
4330 :meth:`AcceptLanguageValidHeader.best_match` uses its own algorithm
4331 (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
4332 best match. The algorithm has many issues, and does not conform to
4333 :rfc:`RFC 7231 <7231>`.
4335 :meth:`AcceptLanguageValidHeader.lookup` is a possible alternative
4336 for finding a best match -- it conforms to, and is suggested as a
4337 matching scheme for the ``Accept-Language`` header in, :rfc:`RFC
4338 7231, section 5.3.5 <7231#section-5.3.5>` -- but please be aware
4339 that there are differences in how it determines what is a best
4340 match. If that is not suitable for the needs of your application,
4341 you may need to write your own matching using
4342 :attr:`AcceptLanguageValidHeader.parsed`.
4344 Each language tag in `offers` is checked against each non-0 range in
4345 the header. If the two are a match according to WebOb's old criterion
4346 for a match, the quality value of the match is the qvalue of the
4347 language range from the header multiplied by the server quality value
4348 of the offer (if the server quality value is not supplied, it is 1).
4350 The offer in the match with the highest quality value is the best
4351 match. If there is more than one match with the highest qvalue, the
4352 match where the language range has a lower number of '*'s is the best
4353 match. If the two have the same number of '*'s, the one that shows up
4354 first in `offers` is the best match.
4356 :param offers: (iterable)
4358 | Each item in the iterable may be a ``str`` language
4359 tag, or a (language tag, server quality value)
4360 ``tuple`` or ``list``. (The two may be mixed in the
4361 iterable.)
4363 :param default_match: (optional, any type) the value to be returned if
4364 there is no match
4366 :return: (``str``, or the type of `default_match`)
4368 | The language tag that is the best match. If there is no
4369 match, the value of `default_match` is returned.
4372 **Issues**:
4374 - Incorrect tiebreaking when quality values of two matches are the same
4375 (https://github.com/Pylons/webob/issues/256)::
4377 >>> header = AcceptLanguageValidHeader(
4378 ... header_value='en-gb;q=1, en;q=0.8'
4379 ... )
4380 >>> header.best_match(offers=['en', 'en-GB'])
4381 'en'
4382 >>> header.best_match(offers=['en-GB', 'en'])
4383 'en-GB'
4385 >>> header = AcceptLanguageValidHeader(header_value='en-gb, en')
4386 >>> header.best_match(offers=['en', 'en-gb'])
4387 'en'
4388 >>> header.best_match(offers=['en-gb', 'en'])
4389 'en-gb'
4391 - Incorrect handling of ``q=0``::
4393 >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *')
4394 >>> header.best_match(offers=['en'])
4395 'en'
4397 >>> header = AcceptLanguageValidHeader(header_value='fr, en;q=0')
4398 >>> header.best_match(offers=['en-gb'], default_match='en')
4399 'en'
4401 - Matching only takes into account the first subtag when matching a
4402 range with more specific or less specific tags::
4404 >>> header = AcceptLanguageValidHeader(header_value='zh')
4405 >>> header.best_match(offers=['zh-Hans-CN'])
4406 'zh-Hans-CN'
4407 >>> header = AcceptLanguageValidHeader(header_value='zh-Hans')
4408 >>> header.best_match(offers=['zh-Hans-CN'])
4409 >>> header.best_match(offers=['zh-Hans-CN']) is None
4410 True
4412 >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN')
4413 >>> header.best_match(offers=['zh'])
4414 'zh'
4415 >>> header.best_match(offers=['zh-Hans'])
4416 >>> header.best_match(offers=['zh-Hans']) is None
4417 True
4419 """
4420 warnings.warn(
4421 'The behavior of AcceptLanguageValidHeader.best_match is '
4422 'currently being maintained for backward compatibility, but it '
4423 'will be deprecated in the future as it does not conform to the '
4424 'RFC.',
4425 DeprecationWarning,
4426 )
4427 best_quality = -1
4428 best_offer = default_match
4429 matched_by = '*/*'
4430 # [We can see that this was written for the ``Accept`` header and not
4431 # the ``Accept-Language`` header, as there are no '/'s in a valid
4432 # ``Accept-Language`` header.]
4433 for offer in offers:
4434 if isinstance(offer, (tuple, list)):
4435 offer, server_quality = offer
4436 else:
4437 server_quality = 1
4438 for mask, quality in self._parsed_nonzero:
4439 possible_quality = server_quality * quality
4440 if possible_quality < best_quality:
4441 continue
4442 elif possible_quality == best_quality:
4443 # 'text/plain' overrides 'message/*' overrides '*/*'
4444 # (if all match w/ the same q=)
4445 if matched_by.count('*') <= mask.count('*'):
4446 continue
4447 # [This tiebreaking was written for the `Accept` header. A
4448 # basic language range in a valid ``Accept-Language``
4449 # header can only be either '*' or a range with no '*' in
4450 # it. This happens to work here, but is not sufficient as a
4451 # tiebreaker.
4452 #
4453 # A best match here, given this algorithm uses
4454 # self._old_match() which matches both more *and* less
4455 # specific tags, should be the match where the absolute
4456 # value of the difference between the subtag counts of
4457 # `mask` and `offer` is the lowest.]
4458 if self._old_match(mask, offer):
4459 best_quality = possible_quality
4460 best_offer = offer
4461 matched_by = mask
4462 return best_offer
4464 def lookup(
4465 self, language_tags, default_range=None, default_tag=None,
4466 default=None,
4467 ):
4468 """
4469 Return the language tag that best matches the header, using Lookup.
4471 This is an implementation of the Lookup matching scheme,
4472 suggested as a matching scheme for the ``Accept-Language`` header in
4473 :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and described in
4474 :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`.
4476 Each language range in the header is considered in turn, by descending
4477 order of qvalue; where qvalues are tied, ranges are considered from
4478 left to right.
4480 Each language range in the header represents the most specific tag that
4481 is an acceptable match: Lookup progressively truncates subtags from the
4482 end of the range until a matching language tag is found. An example is
4483 given in :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, under
4484 "Example of a Lookup Fallback Pattern":
4486 ::
4488 Range to match: zh-Hant-CN-x-private1-private2
4489 1. zh-Hant-CN-x-private1-private2
4490 2. zh-Hant-CN-x-private1
4491 3. zh-Hant-CN
4492 4. zh-Hant
4493 5. zh
4494 6. (default)
4496 :param language_tags: (``iterable``) language tags
4498 :param default_range: (optional, ``None`` or ``str``)
4500 | If Lookup finds no match using the ranges in
4501 the header, and this argument is not None,
4502 Lookup will next attempt to match the range in
4503 this argument, using the same subtag
4504 truncation.
4506 | `default_range` cannot be '*', as '*' is
4507 skipped in Lookup. See :ref:`note
4508 <acceptparse-lookup-asterisk-note>`.
4510 | This parameter corresponds to the functionality
4511 described in :rfc:`RFC 4647, section 3.4.1
4512 <4647#section-3.4.1>`, in the paragraph
4513 starting with "One common way to provide for a
4514 default is to allow a specific language range
4515 to be set as the default..."
4517 :param default_tag: (optional, ``None`` or ``str``)
4519 | At least one of `default_tag` or `default` must
4520 be supplied as an argument to the method, to
4521 define the defaulting behaviour.
4523 | If Lookup finds no match using the ranges in the
4524 header and `default_range`, this argument is not
4525 ``None``, and it does not match any range in the
4526 header with ``q=0`` (exactly, with no subtag
4527 truncation), then this value is returned.
4529 | This parameter corresponds to "return a
4530 particular language tag designated for the
4531 operation", one of the examples of "defaulting
4532 behavior" described in :rfc:`RFC 4647, section
4533 3.4.1 <4647#section-3.4.1>`.
4535 :param default: (optional, ``None`` or any type, including a callable)
4537 | At least one of `default_tag` or `default` must be
4538 supplied as an argument to the method, to define the
4539 defaulting behaviour.
4541 | If Lookup finds no match using the ranges in the
4542 header and `default_range`, and `default_tag` is
4543 ``None`` or not acceptable because it matches a
4544 ``q=0`` range in the header, then Lookup will next
4545 examine the `default` argument.
4547 | If `default` is a callable, it will be called, and
4548 the callable's return value will be returned.
4550 | If `default` is not a callable, the value itself will
4551 be returned.
4553 | The difference between supplying a ``str`` to
4554 `default_tag` and `default` is that `default_tag` is
4555 checked against ``q=0`` ranges in the header to see
4556 if it matches one of the ranges specified as not
4557 acceptable, whereas a ``str`` for the `default`
4558 argument is simply returned.
4560 | This parameter corresponds to the "defaulting
4561 behavior" described in :rfc:`RFC 4647, section 3.4.1
4562 <4647#section-3.4.1>`
4564 :return: (``str``, ``None``, or any type)
4566 | The best match according to the Lookup matching scheme, or a
4567 return value from one of the default arguments.
4569 **Notes**:
4571 .. _acceptparse-lookup-asterisk-note:
4573 - Lookup's behaviour with '*' language ranges in the header may be
4574 surprising. From :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`:
4576 In the lookup scheme, this range does not convey enough
4577 information by itself to determine which language tag is most
4578 appropriate, since it matches everything. If the language range
4579 "*" is followed by other language ranges, it is skipped. If the
4580 language range "*" is the only one in the language priority list
4581 or if no other language range follows, the default value is
4582 computed and returned.
4584 So
4586 ::
4588 >>> header = AcceptLanguageValidHeader('de, zh, *')
4589 >>> header.lookup(language_tags=['ja', 'en'], default='default')
4590 'default'
4592 - Any tags in `language_tags` and `default_tag` and any tag matched
4593 during the subtag truncation search for `default_range`, that are an
4594 exact match for a non-``*`` range with ``q=0`` in the header, are
4595 considered not acceptable and ruled out.
4597 - If there is a ``*;q=0`` in the header, then `default_range` and
4598 `default_tag` have no effect, as ``*;q=0`` means that all languages
4599 not already matched by other ranges within the header are
4600 unacceptable.
4601 """
4602 if default_tag is None and default is None:
4603 raise TypeError(
4604 '`default_tag` and `default` arguments cannot both be None.'
4605 )
4607 # We need separate `default_tag` and `default` arguments because if we
4608 # only had the `default` argument, there would be no way to tell
4609 # whether a str is a language tag (in which case we have to check
4610 # whether it has been specified as not acceptable with a q=0 range in
4611 # the header) or not (in which case we can just return the value).
4613 if default_range == '*':
4614 raise ValueError('default_range cannot be *.')
4616 parsed = list(self.parsed)
4618 tags = language_tags
4619 not_acceptable_ranges = []
4620 acceptable_ranges = []
4622 asterisk_non0_found = False
4623 # Whether there is a '*' range in the header with q={not 0}
4625 asterisk_q0_found = False
4626 # Whether there is a '*' range in the header with q=0
4627 # While '*' is skipped in Lookup because it "does not convey enough
4628 # information by itself to determine which language tag is most
4629 # appropriate" (RFC 4647, section 3.4), '*;q=0' is clear in meaning:
4630 # languages not matched by any other range within the header are not
4631 # acceptable.
4633 for range_, qvalue in parsed:
4634 if qvalue == 0.0:
4635 if range_ == '*': # *;q=0
4636 asterisk_q0_found = True
4637 else: # {non-* range};q=0
4638 not_acceptable_ranges.append(range_.lower())
4639 elif not asterisk_q0_found and range_ == '*': # *;q={not 0}
4640 asterisk_non0_found = True
4641 # if asterisk_q0_found, then it does not matter whether
4642 # asterisk_non0_found
4643 else: # {non-* range};q={not 0}
4644 acceptable_ranges.append((range_, qvalue))
4645 # Sort acceptable_ranges by qvalue, descending order
4646 acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True)
4647 # Sort guaranteed to be stable with Python >= 2.2, so position in
4648 # header is tiebreaker when two ranges have the same qvalue
4650 acceptable_ranges = [tuple_[0] for tuple_ in acceptable_ranges]
4651 lowered_tags = [tag.lower() for tag in tags]
4653 def best_match(range_):
4654 subtags = range_.split('-')
4655 while True:
4656 for index, tag in enumerate(lowered_tags):
4657 if tag in not_acceptable_ranges:
4658 continue
4659 # We think a non-'*' range with q=0 represents only
4660 # itself as a tag, and there should be no falling back
4661 # with subtag truncation. For example, with
4662 # 'en-gb;q=0', it should not mean 'en;q=0': the client
4663 # is unlikely to expect that specifying 'en-gb' as not
4664 # acceptable would mean that 'en' is also not
4665 # acceptable. There is no guidance on this at all in
4666 # the RFCs, so it is left to us to decide how it should
4667 # work.
4669 if tag == range_:
4670 return tags[index] # return the pre-lowered tag
4672 try:
4673 subtag_before_this = subtags[-2]
4674 except IndexError: # len(subtags) == 1
4675 break
4676 # len(subtags) >= 2
4677 if len(subtag_before_this) == 1 and (
4678 subtag_before_this.isdigit() or
4679 subtag_before_this.isalpha()
4680 ): # if subtag_before_this is a single-letter or -digit subtag
4681 subtags.pop(-1) # pop twice instead of once
4682 subtags.pop(-1)
4683 range_ = '-'.join(subtags)
4685 for range_ in acceptable_ranges:
4686 match = best_match(range_=range_.lower())
4687 if match is not None:
4688 return match
4690 if not asterisk_q0_found:
4691 if default_range is not None:
4692 lowered_default_range = default_range.lower()
4693 match = best_match(range_=lowered_default_range)
4694 if match is not None:
4695 return match
4697 if default_tag is not None:
4698 lowered_default_tag = default_tag.lower()
4699 if lowered_default_tag not in not_acceptable_ranges:
4700 return default_tag
4702 try:
4703 return default()
4704 except TypeError: # default is not a callable
4705 return default
4707 def quality(self, offer):
4708 """
4709 Return quality value of given offer, or ``None`` if there is no match.
4711 .. warning::
4713 This is currently maintained for backward compatibility, and will be
4714 deprecated in the future.
4716 :meth:`AcceptLanguageValidHeader.quality` uses its own algorithm
4717 (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
4718 best match. The algorithm has many issues, and does not conform to
4719 :rfc:`RFC 7231 <7231>`.
4721 What should be considered a match depends on the needs of your
4722 application (for example, should a language range in the header
4723 match a more specific language tag offer, or a less specific tag
4724 offer?) :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests
4725 three matching schemes from :rfc:`RFC 4647 <4647>`, two of which
4726 WebOb supports with
4727 :meth:`AcceptLanguageValidHeader.basic_filtering` and
4728 :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to
4729 mean that Extended Filtering cannot apply for the
4730 ``Accept-Language`` header, as the header only accepts basic
4731 language ranges.) :meth:`AcceptLanguageValidHeader.basic_filtering`
4732 returns quality values with the matched language tags.
4733 :meth:`AcceptLanguageValidHeader.lookup` returns a language tag
4734 without the quality value, but the quality value is less likely to
4735 be useful when we are looking for a best match.
4737 If these are not suitable or sufficient for the needs of your
4738 application, you may need to write your own matching using
4739 :attr:`AcceptLanguageValidHeader.parsed`.
4741 :param offer: (``str``) language tag offer
4742 :return: (``float`` or ``None``)
4744 | The highest quality value from the language range(s) that
4745 match the `offer`, or ``None`` if there is no match.
4748 **Issues**:
4750 - Incorrect handling of ``q=0`` and ``*``::
4752 >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *')
4753 >>> header.quality(offer='en')
4754 1.0
4756 - Matching only takes into account the first subtag when matching a
4757 range with more specific or less specific tags::
4759 >>> header = AcceptLanguageValidHeader(header_value='zh')
4760 >>> header.quality(offer='zh-Hans-CN')
4761 1.0
4762 >>> header = AcceptLanguageValidHeader(header_value='zh-Hans')
4763 >>> header.quality(offer='zh-Hans-CN')
4764 >>> header.quality(offer='zh-Hans-CN') is None
4765 True
4767 >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN')
4768 >>> header.quality(offer='zh')
4769 1.0
4770 >>> header.quality(offer='zh-Hans')
4771 >>> header.quality(offer='zh-Hans') is None
4772 True
4774 """
4775 warnings.warn(
4776 'The behavior of AcceptLanguageValidHeader.quality is'
4777 'currently being maintained for backward compatibility, but it '
4778 'will be deprecated in the future as it does not conform to the '
4779 'RFC.',
4780 DeprecationWarning,
4781 )
4782 bestq = 0
4783 for mask, q in self.parsed:
4784 if self._old_match(mask, offer):
4785 bestq = max(bestq, q)
4786 return bestq or None
4789class _AcceptLanguageInvalidOrNoHeader(AcceptLanguage):
4790 """
4791 Represent when an ``Accept-Language`` header is invalid or not in request.
4793 This is the base class for the behaviour that
4794 :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader`
4795 have in common.
4797 :rfc:`7231` does not provide any guidance on what should happen if the
4798 ``Accept-Language`` header has an invalid value. This implementation
4799 disregards the header when the header is invalid, so
4800 :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader`
4801 have much behaviour in common.
4802 """
4804 def __nonzero__(self):
4805 """
4806 Return whether ``self`` represents a valid ``Accept-Language`` header.
4808 Return ``True`` if ``self`` represents a valid header, and ``False`` if
4809 it represents an invalid header, or the header not being in the
4810 request.
4812 For this class, it always returns ``False``.
4813 """
4814 return False
4815 __bool__ = __nonzero__ # Python 3
4817 def __contains__(self, offer):
4818 """
4819 Return ``bool`` indicating whether `offer` is acceptable.
4821 .. warning::
4823 The behavior of ``.__contains__`` for the ``AcceptLanguage`` classes
4824 is currently being maintained for backward compatibility, but it
4825 will change in the future to better conform to the RFC.
4827 :param offer: (``str``) language tag offer
4828 :return: (``bool``) Whether ``offer`` is acceptable according to the
4829 header.
4831 For this class, either there is no ``Accept-Language`` header in the
4832 request, or the header is invalid, so any language tag is acceptable,
4833 and this always returns ``True``.
4834 """
4835 warnings.warn(
4836 'The behavior of .__contains__ for the AcceptLanguage classes is '
4837 'currently being maintained for backward compatibility, but it '
4838 'will change in the future to better conform to the RFC.',
4839 DeprecationWarning,
4840 )
4841 return True
4843 def __iter__(self):
4844 """
4845 Return all the ranges with non-0 qvalues, in order of preference.
4847 .. warning::
4849 The behavior of this method is currently maintained for backward
4850 compatibility, but will change in the future.
4852 :return: iterator of all the language ranges in the header with non-0
4853 qvalues, in descending order of qvalue. If two ranges have the
4854 same qvalue, they are returned in the order of their positions
4855 in the header, from left to right.
4857 For this class, either there is no ``Accept-Language`` header in the
4858 request, or the header is invalid, so there are no language ranges, and
4859 this always returns an empty iterator.
4860 """
4861 warnings.warn(
4862 'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
4863 'maintained for backward compatibility, but will change in the '
4864 'future.',
4865 DeprecationWarning,
4866 )
4867 return iter(())
4869 def basic_filtering(self, language_tags):
4870 """
4871 Return the tags that match the header, using Basic Filtering.
4873 :param language_tags: (``iterable``) language tags
4874 :return: A list of tuples of the form (language tag, qvalue), in
4875 descending order of preference.
4877 When the header is invalid and when the header is not in the request,
4878 there are no matches, so this method always returns an empty list.
4879 """
4880 return []
4882 def best_match(self, offers, default_match=None):
4883 """
4884 Return the best match from the sequence of language tag `offers`.
4886 This is the ``.best_match()`` method for when the header is invalid or
4887 not found in the request, corresponding to
4888 :meth:`AcceptLanguageValidHeader.best_match`.
4890 .. warning::
4892 This is currently maintained for backward compatibility, and will be
4893 deprecated in the future (see the documentation for
4894 :meth:`AcceptLanguageValidHeader.best_match`).
4896 When the header is invalid, or there is no `Accept-Language` header in
4897 the request, any of the language tags in `offers` are considered
4898 acceptable, so the best match is the tag in `offers` with the highest
4899 server quality value (if the server quality value is not supplied, it
4900 is 1).
4902 If more than one language tags in `offers` have the same highest server
4903 quality value, then the one that shows up first in `offers` is the best
4904 match.
4906 :param offers: (iterable)
4908 | Each item in the iterable may be a ``str`` language
4909 tag, or a (language tag, server quality value)
4910 ``tuple`` or ``list``. (The two may be mixed in the
4911 iterable.)
4913 :param default_match: (optional, any type) the value to be returned if
4914 `offers` is empty.
4916 :return: (``str``, or the type of `default_match`)
4918 | The language tag that has the highest server quality value.
4919 If `offers` is empty, the value of `default_match` is
4920 returned.
4921 """
4922 warnings.warn(
4923 'The behavior of .best_match for the AcceptLanguage classes is '
4924 'currently being maintained for backward compatibility, but the '
4925 'method will be deprecated in the future, as its behavior is not '
4926 'specified in (and currently does not conform to) RFC 7231.',
4927 DeprecationWarning,
4928 )
4929 best_quality = -1
4930 best_offer = default_match
4931 for offer in offers:
4932 if isinstance(offer, (list, tuple)):
4933 offer, quality = offer
4934 else:
4935 quality = 1
4936 if quality > best_quality:
4937 best_offer = offer
4938 best_quality = quality
4939 return best_offer
4941 def lookup(
4942 self, language_tags=None, default_range=None, default_tag=None,
4943 default=None,
4944 ):
4945 """
4946 Return the language tag that best matches the header, using Lookup.
4948 When the header is invalid, or there is no ``Accept-Language`` header
4949 in the request, all language tags are considered acceptable, so it is
4950 as if the header is '*'. As specified for the Lookup matching scheme in
4951 :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, when the header is
4952 '*', the default value is to be computed and returned. So this method
4953 will ignore the `language_tags` and `default_range` arguments, and
4954 proceed to `default_tag`, then `default`.
4956 :param language_tags: (optional, any type)
4958 | This argument is ignored, and is only used as a
4959 placeholder so that the method signature
4960 corresponds to that of
4961 :meth:`AcceptLanguageValidHeader.lookup`.
4963 :param default_range: (optional, any type)
4965 | This argument is ignored, and is only used as a
4966 placeholder so that the method signature
4967 corresponds to that of
4968 :meth:`AcceptLanguageValidHeader.lookup`.
4970 :param default_tag: (optional, ``None`` or ``str``)
4972 | At least one of `default_tag` or `default` must
4973 be supplied as an argument to the method, to
4974 define the defaulting behaviour.
4976 | If this argument is not ``None``, then it is
4977 returned.
4979 | This parameter corresponds to "return a
4980 particular language tag designated for the
4981 operation", one of the examples of "defaulting
4982 behavior" described in :rfc:`RFC 4647, section
4983 3.4.1 <4647#section-3.4.1>`.
4985 :param default: (optional, ``None`` or any type, including a callable)
4987 | At least one of `default_tag` or `default` must be
4988 supplied as an argument to the method, to define the
4989 defaulting behaviour.
4991 | If `default_tag` is ``None``, then Lookup will next
4992 examine the `default` argument.
4994 | If `default` is a callable, it will be called, and
4995 the callable's return value will be returned.
4997 | If `default` is not a callable, the value itself will
4998 be returned.
5000 | This parameter corresponds to the "defaulting
5001 behavior" described in :rfc:`RFC 4647, section 3.4.1
5002 <4647#section-3.4.1>`
5004 :return: (``str``, or any type)
5006 | the return value from `default_tag` or `default`.
5007 """
5008 if default_tag is None and default is None:
5009 raise TypeError(
5010 '`default_tag` and `default` arguments cannot both be None.'
5011 )
5013 if default_tag is not None:
5014 return default_tag
5016 try:
5017 return default()
5018 except TypeError: # default is not a callable
5019 return default
5021 def quality(self, offer):
5022 """
5023 Return quality value of given offer, or ``None`` if there is no match.
5025 This is the ``.quality()`` method for when the header is invalid or not
5026 found in the request, corresponding to
5027 :meth:`AcceptLanguageValidHeader.quality`.
5029 .. warning::
5031 This is currently maintained for backward compatibility, and will be
5032 deprecated in the future (see the documentation for
5033 :meth:`AcceptLanguageValidHeader.quality`).
5035 :param offer: (``str``) language tag offer
5036 :return: (``float``) ``1.0``.
5038 When the ``Accept-Language`` header is invalid or not in the request,
5039 all offers are equally acceptable, so 1.0 is always returned.
5040 """
5041 warnings.warn(
5042 'The behavior of .quality for the AcceptLanguage classes is '
5043 'currently being maintained for backward compatibility, but the '
5044 'method will be deprecated in the future, as its behavior is not '
5045 'specified in (and currently does not conform to) RFC 7231.',
5046 DeprecationWarning,
5047 )
5048 return 1.0
5051class AcceptLanguageNoHeader(_AcceptLanguageInvalidOrNoHeader):
5052 """
5053 Represent when there is no ``Accept-Language`` header in the request.
5055 This object should not be modified. To add to the header, we can use the
5056 addition operators (``+`` and ``+=``), which return a new object (see the
5057 docstring for :meth:`AcceptLanguageNoHeader.__add__`).
5058 """
5060 def __init__(self):
5061 """
5062 Create an :class:`AcceptLanguageNoHeader` instance.
5063 """
5064 self._header_value = None
5065 self._parsed = None
5066 self._parsed_nonzero = None
5068 def copy(self):
5069 """
5070 Create a copy of the header object.
5072 """
5073 return self.__class__()
5075 @property
5076 def header_value(self):
5077 """
5078 (``str`` or ``None``) The header value.
5080 As there is no header in the request, this is ``None``.
5081 """
5082 return self._header_value
5084 @property
5085 def parsed(self):
5086 """
5087 (``list`` or ``None``) Parsed form of the header.
5089 As there is no header in the request, this is ``None``.
5090 """
5091 return self._parsed
5093 def __add__(self, other):
5094 """
5095 Add to header, creating a new header object.
5097 `other` can be:
5099 * ``None``
5100 * a ``str``
5101 * a ``dict``, with language ranges as keys and qvalues as values
5102 * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
5103 or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be
5104 mixed within the ``tuple`` or ``list``)
5105 * an :class:`AcceptLanguageValidHeader`,
5106 :class:`AcceptLanguageNoHeader`, or
5107 :class:`AcceptLanguageInvalidHeader` instance
5108 * object of any other type that returns a value for ``__str__``
5110 If `other` is a valid header value or an
5111 :class:`AcceptLanguageValidHeader` instance, a new
5112 :class:`AcceptLanguageValidHeader` instance with the valid header value
5113 is returned.
5115 If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
5116 invalid header value, or an :class:`AcceptLanguageInvalidHeader`
5117 instance, a new :class:`AcceptLanguageNoHeader` instance is returned.
5118 """
5119 if isinstance(other, AcceptLanguageValidHeader):
5120 return AcceptLanguageValidHeader(header_value=other.header_value)
5122 if isinstance(
5123 other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
5124 ):
5125 return self.__class__()
5127 return self._add_instance_and_non_accept_language_type(
5128 instance=self, other=other,
5129 )
5131 def __radd__(self, other):
5132 """
5133 Add to header, creating a new header object.
5135 See the docstring for :meth:`AcceptLanguageNoHeader.__add__`.
5136 """
5137 return self.__add__(other=other)
5139 def __repr__(self):
5140 return '<{}>'.format(self.__class__.__name__)
5142 def __str__(self):
5143 """Return the ``str`` ``'<no header in request>'``."""
5144 return '<no header in request>'
5146 def _add_instance_and_non_accept_language_type(self, instance, other):
5147 if not other:
5148 return self.__class__()
5150 other_header_value = self._python_value_to_header_str(value=other)
5152 try:
5153 return AcceptLanguageValidHeader(header_value=other_header_value)
5154 except ValueError: # invalid header value
5155 return self.__class__()
5158class AcceptLanguageInvalidHeader(_AcceptLanguageInvalidOrNoHeader):
5159 """
5160 Represent an invalid ``Accept-Language`` header.
5162 An invalid header is one that does not conform to
5163 :rfc:`7231#section-5.3.5`. As specified in the RFC, an empty header is an
5164 invalid ``Accept-Language`` header.
5166 :rfc:`7231` does not provide any guidance on what should happen if the
5167 ``Accept-Language`` header has an invalid value. This implementation
5168 disregards the header, and treats it as if there is no ``Accept-Language``
5169 header in the request.
5171 This object should not be modified. To add to the header, we can use the
5172 addition operators (``+`` and ``+=``), which return a new object (see the
5173 docstring for :meth:`AcceptLanguageInvalidHeader.__add__`).
5174 """
5176 def __init__(self, header_value):
5177 """
5178 Create an :class:`AcceptLanguageInvalidHeader` instance.
5179 """
5180 self._header_value = header_value
5181 self._parsed = None
5182 self._parsed_nonzero = None
5184 def copy(self):
5185 """
5186 Create a copy of the header object.
5188 """
5189 return self.__class__(self._header_value)
5191 @property
5192 def header_value(self):
5193 """(``str`` or ``None``) The header value."""
5194 return self._header_value
5196 @property
5197 def parsed(self):
5198 """
5199 (``list`` or ``None``) Parsed form of the header.
5201 As the header is invalid and cannot be parsed, this is ``None``.
5202 """
5203 return self._parsed
5205 def __add__(self, other):
5206 """
5207 Add to header, creating a new header object.
5209 `other` can be:
5211 * ``None``
5212 * a ``str``
5213 * a ``dict``, with language ranges as keys and qvalues as values
5214 * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
5215 or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
5216 be mixed within the ``tuple`` or ``list``)
5217 * an :class:`AcceptLanguageValidHeader`,
5218 :class:`AcceptLanguageNoHeader`, or
5219 :class:`AcceptLanguageInvalidHeader` instance
5220 * object of any other type that returns a value for ``__str__``
5222 If `other` is a valid header value or an
5223 :class:`AcceptLanguageValidHeader` instance, a new
5224 :class:`AcceptLanguageValidHeader` instance with the valid header value
5225 is returned.
5227 If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
5228 invalid header value, or an :class:`AcceptLanguageInvalidHeader`
5229 instance, a new :class:`AcceptLanguageNoHeader` instance is returned.
5230 """
5231 if isinstance(other, AcceptLanguageValidHeader):
5232 return AcceptLanguageValidHeader(header_value=other.header_value)
5234 if isinstance(
5235 other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
5236 ):
5237 return AcceptLanguageNoHeader()
5239 return self._add_instance_and_non_accept_language_type(
5240 instance=self, other=other,
5241 )
5243 def __radd__(self, other):
5244 """
5245 Add to header, creating a new header object.
5247 See the docstring for :meth:`AcceptLanguageValidHeader.__add__`.
5248 """
5249 return self._add_instance_and_non_accept_language_type(
5250 instance=self, other=other, instance_on_the_right=True,
5251 )
5253 def __repr__(self):
5254 return '<{}>'.format(self.__class__.__name__)
5255 # We do not display the header_value, as it is untrusted input. The
5256 # header_value could always be easily obtained from the .header_value
5257 # property.
5259 def __str__(self):
5260 """Return the ``str`` ``'<invalid header value>'``."""
5261 return '<invalid header value>'
5263 def _add_instance_and_non_accept_language_type(
5264 self, instance, other, instance_on_the_right=False,
5265 ):
5266 if not other:
5267 return AcceptLanguageNoHeader()
5269 other_header_value = self._python_value_to_header_str(value=other)
5271 try:
5272 return AcceptLanguageValidHeader(header_value=other_header_value)
5273 except ValueError: # invalid header value
5274 return AcceptLanguageNoHeader()
5277def create_accept_language_header(header_value):
5278 """
5279 Create an object representing the ``Accept-Language`` header in a request.
5281 :param header_value: (``str``) header value
5282 :return: If `header_value` is ``None``, an :class:`AcceptLanguageNoHeader`
5283 instance.
5285 | If `header_value` is a valid ``Accept-Language`` header, an
5286 :class:`AcceptLanguageValidHeader` instance.
5288 | If `header_value` is an invalid ``Accept-Language`` header, an
5289 :class:`AcceptLanguageInvalidHeader` instance.
5290 """
5291 if header_value is None:
5292 return AcceptLanguageNoHeader()
5293 if isinstance(header_value, AcceptLanguage):
5294 return header_value.copy()
5295 try:
5296 return AcceptLanguageValidHeader(header_value=header_value)
5297 except ValueError:
5298 return AcceptLanguageInvalidHeader(header_value=header_value)
5301def accept_language_property():
5302 doc = """
5303 Property representing the ``Accept-Language`` header.
5305 (:rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`)
5307 The header value in the request environ is parsed and a new object
5308 representing the header is created every time we *get* the value of the
5309 property. (*set* and *del* change the header value in the request
5310 environ, and do not involve parsing.)
5311 """
5313 ENVIRON_KEY = 'HTTP_ACCEPT_LANGUAGE'
5315 def fget(request):
5316 """Get an object representing the header in the request."""
5317 return create_accept_language_header(
5318 header_value=request.environ.get(ENVIRON_KEY)
5319 )
5321 def fset(request, value):
5322 """
5323 Set the corresponding key in the request environ.
5325 `value` can be:
5327 * ``None``
5328 * a ``str``
5329 * a ``dict``, with language ranges as keys and qvalues as values
5330 * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
5331 or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
5332 be mixed within the ``tuple`` or ``list``)
5333 * an :class:`AcceptLanguageValidHeader`,
5334 :class:`AcceptLanguageNoHeader`, or
5335 :class:`AcceptLanguageInvalidHeader` instance
5336 * object of any other type that returns a value for ``__str__``
5337 """
5338 if value is None or isinstance(value, AcceptLanguageNoHeader):
5339 fdel(request=request)
5340 else:
5341 if isinstance(
5342 value, (AcceptLanguageValidHeader, AcceptLanguageInvalidHeader)
5343 ):
5344 header_value = value.header_value
5345 else:
5346 header_value = AcceptLanguage._python_value_to_header_str(
5347 value=value,
5348 )
5349 request.environ[ENVIRON_KEY] = header_value
5351 def fdel(request):
5352 """Delete the corresponding key from the request environ."""
5353 try:
5354 del request.environ[ENVIRON_KEY]
5355 except KeyError:
5356 pass
5358 return property(fget, fset, fdel, textwrap.dedent(doc))