--- /dev/null
+"""
+Parse four ``Accept*`` headers used in server-driven content negotiation.
+
+The four headers are ``Accept``, ``Accept-Charset``, ``Accept-Encoding`` and
+``Accept-Language``.
+
+This module was taken from upstream PIP module 'webob', version 1.8.7
+"""
+
+from collections import namedtuple
+import re
+import textwrap
+import warnings
+
+
+# RFC 7230 Section 3.2.3 "Whitespace"
+# OWS = *( SP / HTAB )
+# ; optional whitespace
+OWS_re = '[ \t]*'
+
+# RFC 7230 Section 3.2.6 "Field Value Components":
+# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+# / DIGIT / ALPHA
+tchar_re = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]"
+
+# token = 1*tchar
+token_re = tchar_re + '+'
+token_compiled_re = re.compile('^' + token_re + '$')
+
+# RFC 7231 Section 5.3.1 "Quality Values"
+# qvalue = ( "0" [ "." 0*3DIGIT ] )
+# / ( "1" [ "." 0*3("0") ] )
+qvalue_re = (
+ r'(?:0(?:\.[0-9]{0,3})?)'
+ '|'
+ r'(?:1(?:\.0{0,3})?)'
+)
+# weight = OWS ";" OWS "q=" qvalue
+weight_re = OWS_re + ';' + OWS_re + '[qQ]=(' + qvalue_re + ')'
+
+
+def _item_n_weight_re(item_re):
+ return '(' + item_re + ')(?:' + weight_re + ')?'
+
+
+def _item_qvalue_pair_to_header_element(pair):
+ item, qvalue = pair
+ if qvalue == 1.0:
+ element = item
+ elif qvalue == 0.0:
+ element = '{};q=0'.format(item)
+ else:
+ element = '{};q={}'.format(item, qvalue)
+ return element
+
+
+def _list_0_or_more__compiled_re(element_re):
+ # RFC 7230 Section 7 "ABNF List Extension: #rule":
+ # #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ]
+ return re.compile(
+ '^(?:$)|' +
+ '(?:' +
+ '(?:,|(?:' + element_re + '))' +
+ '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*' +
+ ')$',
+ )
+
+
+def _list_1_or_more__compiled_re(element_re):
+ # RFC 7230 Section 7 "ABNF List Extension: #rule":
+ # 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] )
+ # and RFC 7230 Errata ID: 4169
+ return re.compile(
+ '^(?:,' + OWS_re + ')*' + element_re +
+ '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*$',
+ )
+
+
+class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])):
+ """
+ A pre-parsed offer tuple represeting a value in the format
+ ``type/subtype;param0=value0;param1=value1``.
+
+ :ivar type: The media type's root category.
+ :ivar subtype: The media type's subtype.
+ :ivar params: A tuple of 2-tuples containing parameter names and values.
+
+ """
+ __slots__ = ()
+
+ def __str__(self):
+ """
+ Return the properly quoted media type string.
+
+ """
+ value = self.type + '/' + self.subtype
+ return Accept._form_media_range(value, self.params)
+
+
+class Accept(object):
+ """
+ Represent an ``Accept`` header.
+
+ Base class for :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, and
+ :class:`AcceptInvalidHeader`.
+ """
+
+ # RFC 6838 describes syntax rules for media types that are different to
+ # (and stricter than) those in RFC 7231, but if RFC 7231 intended us to
+ # follow the rules in RFC 6838 for media ranges, it would not have
+ # specified its own syntax rules for media ranges, so it appears we should
+ # use the rules in RFC 7231 for now.
+
+ # RFC 5234 Appendix B.1 "Core Rules":
+ # VCHAR = %x21-7E
+ # ; visible (printing) characters
+ vchar_re = '\x21-\x7e'
+ # RFC 7230 Section 3.2.6 "Field Value Components":
+ # quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+ # qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
+ # obs-text = %x80-FF
+ # quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+ obs_text_re = '\x80-\xff'
+ qdtext_re = '[\t \x21\x23-\x5b\\\x5d-\x7e' + obs_text_re + ']'
+ # The '\\' between \x5b and \x5d is needed to escape \x5d (']')
+ quoted_pair_re = r'\\' + '[\t ' + vchar_re + obs_text_re + ']'
+ quoted_string_re = \
+ '"(?:(?:' + qdtext_re + ')|(?:' + quoted_pair_re + '))*"'
+
+ # RFC 7231 Section 3.1.1.1 "Media Type":
+ # type = token
+ # subtype = token
+ # parameter = token "=" ( token / quoted-string )
+ type_re = token_re
+ subtype_re = token_re
+ parameter_re = token_re + '=' + \
+ '(?:(?:' + token_re + ')|(?:' + quoted_string_re + '))'
+
+ # Section 5.3.2 "Accept":
+ # media-range = ( "*/*"
+ # / ( type "/" "*" )
+ # / ( type "/" subtype )
+ # ) *( OWS ";" OWS parameter )
+ media_range_re = (
+ '(' +
+ '(?:' + type_re + '/' + subtype_re + ')' +
+ # '*' is included through type_re and subtype_re, so this covers */*
+ # and type/*
+ ')' +
+ '(' +
+ '(?:' + OWS_re + ';' + OWS_re +
+ '(?![qQ]=)' + # media type parameter cannot be named "q"
+ parameter_re + ')*' +
+ ')'
+ )
+ # accept-params = weight *( accept-ext )
+ # accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
+ accept_ext_re = (
+ OWS_re + ';' + OWS_re + token_re + '(?:' +
+ '=(?:' +
+ '(?:' + token_re + ')|(?:' + quoted_string_re + ')' +
+ ')' +
+ ')?'
+ )
+ accept_params_re = weight_re + '((?:' + accept_ext_re + ')*)'
+
+ media_range_n_accept_params_re = media_range_re + '(?:' + \
+ accept_params_re + ')?'
+ media_range_n_accept_params_compiled_re = re.compile(
+ media_range_n_accept_params_re,
+ )
+
+ accept_compiled_re = _list_0_or_more__compiled_re(
+ element_re=media_range_n_accept_params_re,
+ )
+
+ # For parsing repeated groups within the media type parameters and
+ # extension parameters segments
+ parameters_compiled_re = re.compile(
+ OWS_re + ';' + OWS_re + '(' + token_re + ')=(' + token_re + '|' +
+ quoted_string_re + ')',
+ )
+ accept_ext_compiled_re = re.compile(
+ OWS_re + ';' + OWS_re + '(' + token_re + ')' +
+ '(?:' +
+ '=(' +
+ '(?:' +
+ '(?:' + token_re + ')|(?:' + quoted_string_re + ')' +
+ ')' +
+ ')' +
+ ')?',
+ )
+
+ # For parsing the media types in the `offers` argument to
+ # .acceptable_offers(), we re-use the media range regex for media types.
+ # This is not intended to be a validation of the offers; its main purpose
+ # is to extract the media type and any media type parameters.
+ media_type_re = media_range_re
+ media_type_compiled_re = re.compile('^' + media_type_re + '$')
+
+ @classmethod
+ def _escape_and_quote_parameter_value(cls, param_value):
+ """
+ Escape and quote parameter value where necessary.
+
+ For media type and extension parameter values.
+ """
+ if param_value == '':
+ param_value = '""'
+ else:
+ param_value = param_value.replace('\\', '\\\\').replace(
+ '"', r'\"',
+ )
+ if not token_compiled_re.match(param_value):
+ param_value = '"' + param_value + '"'
+ return param_value
+
+ @classmethod
+ def _form_extension_params_segment(cls, extension_params):
+ """
+ Convert iterable of extension parameters to str segment for header.
+
+ `extension_params` is an iterable where each item is either a parameter
+ string or a (name, value) tuple.
+ """
+ extension_params_segment = ''
+ for item in extension_params:
+ try:
+ extension_params_segment += (';' + item)
+ except TypeError:
+ param_name, param_value = item
+ param_value = cls._escape_and_quote_parameter_value(
+ param_value=param_value,
+ )
+ extension_params_segment += (
+ ';' + param_name + '=' + param_value
+ )
+ return extension_params_segment
+
+ @classmethod
+ def _form_media_range(cls, type_subtype, media_type_params):
+ """
+ Combine `type_subtype` and `media_type_params` to form a media range.
+
+ `type_subtype` is a ``str``, and `media_type_params` is an iterable of
+ (parameter name, parameter value) tuples.
+ """
+ media_type_params_segment = ''
+ for param_name, param_value in media_type_params:
+ param_value = cls._escape_and_quote_parameter_value(
+ param_value=param_value,
+ )
+ media_type_params_segment += (';' + param_name + '=' + param_value)
+ return type_subtype + media_type_params_segment
+
+ @classmethod
+ def _iterable_to_header_element(cls, iterable):
+ """
+ Convert iterable of tuples into header element ``str``.
+
+ Each tuple is expected to be in one of two forms: (media_range, qvalue,
+ extension_params_segment), or (media_range, qvalue).
+ """
+ try:
+ media_range, qvalue, extension_params_segment = iterable
+ except ValueError:
+ media_range, qvalue = iterable
+ extension_params_segment = ''
+
+ if qvalue == 1.0:
+ if extension_params_segment:
+ element = '{};q=1{}'.format(
+ media_range, extension_params_segment,
+ )
+ else:
+ element = media_range
+ elif qvalue == 0.0:
+ element = '{};q=0{}'.format(media_range, extension_params_segment)
+ else:
+ element = '{};q={}{}'.format(
+ media_range, qvalue, extension_params_segment,
+ )
+ return element
+
+ @classmethod
+ def _parse_media_type_params(cls, media_type_params_segment):
+ """
+ Parse media type parameters segment into list of (name, value) tuples.
+ """
+ media_type_params = cls.parameters_compiled_re.findall(
+ media_type_params_segment,
+ )
+ for index, (name, value) in enumerate(media_type_params):
+ if value.startswith('"') and value.endswith('"'):
+ value = cls._process_quoted_string_token(token=value)
+ media_type_params[index] = (name, value)
+ return media_type_params
+
+ @classmethod
+ def _process_quoted_string_token(cls, token):
+ """
+ Return unescaped and unquoted value from quoted token.
+ """
+ # RFC 7230, section 3.2.6 "Field Value Components": "Recipients that
+ # process the value of a quoted-string MUST handle a quoted-pair as if
+ # it were replaced by the octet following the backslash."
+ return re.sub(r'\\(?![\\])', '', token[1:-1]).replace('\\\\', '\\')
+
+ @classmethod
+ def _python_value_to_header_str(cls, value):
+ """
+ Convert Python value to header string for __add__/__radd__.
+ """
+ if isinstance(value, str):
+ return value
+ if hasattr(value, 'items'):
+ if value == {}:
+ value = []
+ else:
+ value_list = []
+ for media_range, item in value.items():
+ # item is either (media range, (qvalue, extension
+ # parameters segment)), or (media range, qvalue) (supported
+ # for backward compatibility)
+ if isinstance(item, (float, int)):
+ value_list.append((media_range, item, ''))
+ else:
+ value_list.append((media_range, item[0], item[1]))
+ value = sorted(
+ value_list,
+ key=lambda item: item[1], # qvalue
+ reverse=True,
+ )
+ if isinstance(value, (tuple, list)):
+ header_elements = []
+ for item in value:
+ if isinstance(item, (tuple, list)):
+ item = cls._iterable_to_header_element(iterable=item)
+ header_elements.append(item)
+ header_str = ', '.join(header_elements)
+ else:
+ header_str = str(value)
+ return header_str
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse an ``Accept`` header.
+
+ :param value: (``str``) header value
+ :return: If `value` is a valid ``Accept`` header, returns an iterator
+ of (*media_range*, *qvalue*, *media_type_params*,
+ *extension_params*) tuples, as parsed from the header from
+ left to right.
+
+ | *media_range* is the media range, including any media type
+ parameters. The media range is returned in a canonicalised
+ form (except the case of the characters are unchanged):
+ unnecessary spaces around the semicolons before media type
+ parameters are removed; the parameter values are returned in
+ a form where only the '``\\``' and '``"``' characters are
+ escaped, and the values are quoted with double quotes only
+ if they need to be quoted.
+
+ | *qvalue* is the quality value of the media range.
+
+ | *media_type_params* is the media type parameters, as a list
+ of (parameter name, value) tuples.
+
+ | *extension_params* is the extension parameters, as a list
+ where each item is either a parameter string or a (parameter
+ name, value) tuple.
+ :raises ValueError: if `value` is an invalid header
+ """
+ # Check if header is valid
+ # Using Python stdlib's `re` module, there is currently no way to check
+ # the match *and* get all the groups using the same regex, so we have
+ # to do this in steps using multiple regexes.
+ if cls.accept_compiled_re.match(value) is None:
+ raise ValueError('Invalid value for an Accept header.')
+ def generator(value):
+ for match in (
+ cls.media_range_n_accept_params_compiled_re.finditer(value)
+ ):
+ groups = match.groups()
+
+ type_subtype = groups[0]
+
+ media_type_params = cls._parse_media_type_params(
+ media_type_params_segment=groups[1],
+ )
+
+ media_range = cls._form_media_range(
+ type_subtype=type_subtype,
+ media_type_params=media_type_params,
+ )
+
+ # qvalue (groups[2]) and extension_params (groups[3]) are both
+ # None if neither qvalue or extension parameters are found in
+ # the match.
+
+ qvalue = groups[2]
+ qvalue = float(qvalue) if qvalue else 1.0
+
+ extension_params = groups[3]
+ if extension_params:
+ extension_params = cls.accept_ext_compiled_re.findall(
+ extension_params,
+ )
+ for index, (token_key, token_value) in enumerate(
+ extension_params
+ ):
+ if token_value:
+ if (
+ token_value.startswith('"') and
+ token_value.endswith('"')
+ ):
+ token_value = cls._process_quoted_string_token(
+ token=token_value,
+ )
+ extension_params[index] = (
+ token_key, token_value,
+ )
+ else:
+ extension_params[index] = token_key
+ else:
+ extension_params = []
+
+ yield (
+ media_range, qvalue, media_type_params, extension_params,
+ )
+ return generator(value=value)
+
+ @classmethod
+ def parse_offer(cls, offer):
+ """
+ Parse an offer into its component parts.
+
+ :param offer: A media type or range in the format
+ ``type/subtype[;params]``.
+ :return: A named tuple containing ``(*type*, *subtype*, *params*)``.
+
+ | *params* is a list containing ``(*parameter name*, *value*)``
+ values.
+
+ :raises ValueError: If the offer does not match the required format.
+
+ """
+ if isinstance(offer, AcceptOffer):
+ return offer
+ match = cls.media_type_compiled_re.match(offer)
+ if not match:
+ raise ValueError('Invalid value for an Accept offer.')
+
+ groups = match.groups()
+ offer_type, offer_subtype = groups[0].split('/')
+ offer_params = cls._parse_media_type_params(
+ media_type_params_segment=groups[1],
+ )
+ if offer_type == '*' or offer_subtype == '*':
+ raise ValueError('Invalid value for an Accept offer.')
+ return AcceptOffer(
+ offer_type.lower(),
+ offer_subtype.lower(),
+ tuple((name.lower(), value) for name, value in offer_params),
+ )
+
+ @classmethod
+ def _parse_and_normalize_offers(cls, offers):
+ """
+ Throw out any offers that do not match the media range ABNF.
+
+ :return: A list of offers split into the format ``[offer_index,
+ parsed_offer]``.
+
+ """
+ parsed_offers = []
+ for index, offer in enumerate(offers):
+ try:
+ parsed_offer = cls.parse_offer(offer)
+ except ValueError:
+ continue
+ parsed_offers.append([index, parsed_offer])
+ return parsed_offers
+
+
+class AcceptValidHeader(Accept):
+ """
+ Represent a valid ``Accept`` header.
+
+ A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.2
+ <7231#section-5.3.2>`.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptValidHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ A list of (*media_range*, *qvalue*, *media_type_params*,
+ *extension_params*) tuples, where
+
+ *media_range* is the media range, including any media type parameters.
+ The media range is returned in a canonicalised form (except the case of
+ the characters are unchanged): unnecessary spaces around the semicolons
+ before media type parameters are removed; the parameter values are
+ returned in a form where only the '``\\``' and '``"``' characters are
+ escaped, and the values are quoted with double quotes only if they need
+ to be quoted.
+
+ *qvalue* is the quality value of the media range.
+
+ *media_type_params* is the media type parameters, as a list of
+ (parameter name, value) tuples.
+
+ *extension_params* is the extension parameters, as a list where each
+ item is either a parameter string or a (parameter name, value) tuple.
+ """
+ return self._parsed
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptValidHeader` instance.
+
+ :param header_value: (``str``) header value.
+ :raises ValueError: if `header_value` is an invalid value for an
+ ``Accept`` header.
+ """
+ self._header_value = header_value
+ self._parsed = list(self.parse(header_value))
+ self._parsed_nonzero = [item for item in self.parsed if item[1]]
+ # item[1] is the qvalue
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with media ranges ``str``'s (including any media type
+ parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
+ *extension_params*) tuples as values, where *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
+ or ``list`` where *media_range* is a ``str`` of the media range
+ including any media type parameters, and *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
+ :class:`AcceptInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or another
+ :class:`AcceptValidHeader` instance, and the header value it represents
+ is not `''`, then the two header values are joined with ``', '``, and a
+ new :class:`AcceptValidHeader` instance with the new header value is
+ returned.
+
+ If `other` is a valid header value or another
+ :class:`AcceptValidHeader` instance representing a header value of
+ `''`; or if it is ``None`` or an :class:`AcceptNoHeader` instance; or
+ if it is an invalid header value, or an :class:`AcceptInvalidHeader`
+ instance, then a new :class:`AcceptValidHeader` instance with the same
+ header value as ``self`` is returned.
+ """
+ if isinstance(other, AcceptValidHeader):
+ if other.header_value == '':
+ return self.__class__(header_value=self.header_value)
+ else:
+ return create_accept_header(
+ header_value=self.header_value + ', ' + other.header_value,
+ )
+
+ if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
+ return self.__class__(header_value=self.header_value)
+
+ return self._add_instance_and_non_accept_type(
+ instance=self, other=other,
+ )
+
+ def __bool__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``True``.
+ """
+ return True
+ __nonzero__ = __bool__ # Python 2
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of :meth:`AcceptValidHeader.__contains__` is currently
+ being maintained for backward compatibility, but it will change in
+ the future to better conform to the RFC.
+
+ :param offer: (``str``) media type offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ This uses the old criterion of a match in
+ :meth:`AcceptValidHeader._old_match`, which is not as specified in
+ :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
+ correctly take into account media type parameters:
+
+ >>> 'text/html;p=1' in AcceptValidHeader('text/html')
+ False
+
+ or media ranges with ``q=0`` in the header::
+
+ >>> 'text/html' in AcceptValidHeader('text/*, text/html;q=0')
+ True
+ >>> 'text/html' in AcceptValidHeader('text/html;q=0, */*')
+ True
+
+ (See the docstring for :meth:`AcceptValidHeader._old_match` for other
+ problems with the old criterion for matching.)
+ """
+ warnings.warn(
+ 'The behavior of AcceptValidHeader.__contains__ is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ for (
+ media_range, quality, media_type_params, extension_params
+ ) in self._parsed_nonzero:
+ if self._old_match(media_range, offer):
+ return True
+ return False
+
+ def __iter__(self):
+ """
+ Return all the ranges with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the media ranges in the header with non-0
+ qvalues, in descending order of qvalue. If two ranges have the
+ same qvalue, they are returned in the order of their positions
+ in the header, from left to right.
+
+ Please note that this is a simple filter for the ranges in the header
+ with non-0 qvalues, and is not necessarily the same as what the client
+ prefers, e.g. ``'audio/basic;q=0, */*'`` means 'everything but
+ audio/basic', but ``list(instance)`` would return only ``['*/*']``.
+ """
+ warnings.warn(
+ 'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+
+ for media_range, qvalue, media_type_params, extension_params in sorted(
+ self._parsed_nonzero,
+ key=lambda i: i[1],
+ reverse=True
+ ):
+ yield media_range
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
+
+ def __str__(self):
+ r"""
+ Return a tidied up version of the header value.
+
+ e.g. If ``self.header_value`` is ``r',,text/html ; p1="\"\1\"" ;
+ q=0.50; e1=1 ;e2 , text/plain ,'``, ``str(instance)`` returns
+ ``r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain'``.
+ """
+ # self.parsed tuples are in the form: (media_range, qvalue,
+ # media_type_params, extension_params)
+ # self._iterable_to_header_element() requires iterable to be in the
+ # form: (media_range, qvalue, extension_params_segment).
+ return ', '.join(
+ self._iterable_to_header_element(
+ iterable=(
+ tuple_[0], # media_range
+ tuple_[1], # qvalue
+ self._form_extension_params_segment(
+ extension_params=tuple_[3], # extension_params
+ )
+ ),
+ ) for tuple_ in self.parsed
+ )
+
+ def _add_instance_and_non_accept_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if not other:
+ return self.__class__(header_value=instance.header_value)
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ if other_header_value == '':
+ # if ``other`` is an object whose type we don't recognise, and
+ # str(other) returns ''
+ return self.__class__(header_value=instance.header_value)
+
+ try:
+ self.parse(value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__(header_value=instance.header_value)
+
+ new_header_value = (
+ (other_header_value + ', ' + instance.header_value)
+ if instance_on_the_right
+ else (instance.header_value + ', ' + other_header_value)
+ )
+ return self.__class__(header_value=new_header_value)
+
+ def _old_match(self, mask, offer):
+ """
+ Check if the offer is covered by the mask
+
+ ``offer`` may contain wildcards to facilitate checking if a ``mask``
+ would match a 'permissive' offer.
+
+ Wildcard matching forces the match to take place against the type or
+ subtype of the mask and offer (depending on where the wildcard matches)
+
+ .. warning::
+
+ This is maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ This method was WebOb's old criterion for deciding whether a media type
+ matches a media range, used in
+
+ - :meth:`AcceptValidHeader.__contains__`
+ - :meth:`AcceptValidHeader.best_match`
+ - :meth:`AcceptValidHeader.quality`
+
+ It allows offers of *, */*, type/*, */subtype and types with no
+ subtypes, which are not media types as specified in :rfc:`RFC 7231,
+ section 5.3.2 <7231#section-5.3.2>`. This is also undocumented in any
+ of the public APIs that uses this method.
+ """
+ # Match if comparisons are the same or either is a complete wildcard
+ if (mask.lower() == offer.lower() or
+ '*/*' in (mask, offer) or
+ '*' == offer):
+ return True
+
+ # Set mask type with wildcard subtype for malformed masks
+ try:
+ mask_type, mask_subtype = [x.lower() for x in mask.split('/')]
+ except ValueError:
+ mask_type = mask
+ mask_subtype = '*'
+
+ # Set offer type with wildcard subtype for malformed offers
+ try:
+ offer_type, offer_subtype = [x.lower() for x in offer.split('/')]
+ except ValueError:
+ offer_type = offer
+ offer_subtype = '*'
+
+ if mask_subtype == '*':
+ # match on type only
+ if offer_type == '*':
+ return True
+ else:
+ return mask_type.lower() == offer_type.lower()
+
+ if mask_type == '*':
+ # match on subtype only
+ if offer_subtype == '*':
+ return True
+ else:
+ return mask_subtype.lower() == offer_subtype.lower()
+
+ if offer_subtype == '*':
+ # match on type only
+ return mask_type.lower() == offer_type.lower()
+
+ if offer_type == '*':
+ # match on subtype only
+ return mask_subtype.lower() == offer_subtype.lower()
+
+ return offer.lower() == mask.lower()
+
+ def accept_html(self):
+ """
+ Return ``True`` if any HTML-like type is accepted.
+
+ The HTML-like types are 'text/html', 'application/xhtml+xml',
+ 'application/xml' and 'text/xml'.
+ """
+ return bool(
+ self.acceptable_offers(
+ offers=[
+ 'text/html',
+ 'application/xhtml+xml',
+ 'application/xml',
+ 'text/xml',
+ ],
+ )
+ )
+ accepts_html = property(fget=accept_html, doc=accept_html.__doc__)
+ # note the plural
+
+ def acceptable_offers(self, offers):
+ """
+ Return the offers that are acceptable according to the header.
+
+ The offers are returned in descending order of preference, where
+ preference is indicated by the qvalue of the media range in the header
+ that best matches the offer.
+
+ This uses the matching rules described in :rfc:`RFC 7231, section 5.3.2
+ <7231#section-5.3.2>`.
+
+ Any offers that cannot be parsed via
+ :meth:`.Accept.parse_offer` will be ignored.
+
+ :param offers: ``iterable`` of ``str`` media types (media types can
+ include media type parameters) or pre-parsed instances
+ of :class:`.AcceptOffer`.
+ :return: A list of tuples of the form (media type, qvalue), in
+ descending order of qvalue. Where two offers have the same
+ qvalue, they are returned in the same order as their order in
+ `offers`.
+ """
+ parsed = self.parsed
+
+ # RFC 7231, section 3.1.1.1 "Media Type":
+ # "The type, subtype, and parameter name tokens are case-insensitive.
+ # Parameter values might or might not be case-sensitive, depending on
+ # the semantics of the parameter name."
+ lowercased_ranges = [
+ (
+ media_range.partition(';')[0].lower(),
+ qvalue,
+ tuple(
+ (name.lower(), value)
+ for name, value in media_type_params
+ ),
+ )
+ for media_range, qvalue, media_type_params, __ in
+ parsed
+ ]
+ lowercased_offers_parsed = self._parse_and_normalize_offers(offers)
+
+ acceptable_offers_n_quality_factors = {}
+ for offer_index, parsed_offer in lowercased_offers_parsed:
+ offer = offers[offer_index]
+ offer_type, offer_subtype, offer_media_type_params = parsed_offer
+ for (
+ range_type_subtype, range_qvalue, range_media_type_params,
+ ) in lowercased_ranges:
+ range_type, range_subtype = range_type_subtype.split('/', 1)
+
+ # The specificity values below are based on the list in the
+ # example in RFC 7231 section 5.3.2 explaining how "media
+ # ranges can be overridden by more specific media ranges or
+ # specific media types". We assign specificity to the list
+ # items in reverse order, so specificity 4, 3, 2, 1 correspond
+ # to 1, 2, 3, 4 in the list, respectively (so that higher
+ # specificity has higher precedence).
+ if (
+ offer_type == range_type
+ and offer_subtype == range_subtype
+ ):
+ if range_media_type_params == ():
+ # If offer_media_type_params == () the offer and the
+ # range match exactly, with neither having media type
+ # parameters.
+ # If offer_media_type_params is not (), the offer and
+ # the range are a match. See the table towards the end
+ # of RFC 7231 section 5.3.2, where the media type
+ # 'text/html;level=3' matches the range 'text/html' in
+ # the header.
+ # Both cases are a match with a specificity of 3.
+ specificity = 3
+ elif offer_media_type_params == range_media_type_params:
+ specificity = 4
+ else: # pragma: no cover
+ # no cover because of
+ # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement
+ continue
+ else:
+ if range_subtype == '*' and offer_type == range_type:
+ specificity = 2
+ elif range_type_subtype == '*/*':
+ specificity = 1
+ else: # pragma: no cover
+ # no cover because of
+ # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement
+ continue
+ try:
+ if specificity <= (
+ acceptable_offers_n_quality_factors[offer][2]
+ ):
+ continue
+ except KeyError:
+ # the entry for the offer is not already in
+ # acceptable_offers_n_quality_factors
+ pass
+ acceptable_offers_n_quality_factors[offer] = (
+ range_qvalue, # qvalue of matched range
+ offer_index,
+ specificity, # specifity of matched range
+ )
+
+ acceptable_offers_n_quality_factors = [
+ # key is offer, value[0] is qvalue, value[1] is offer_index
+ (key, value[0], value[1])
+ for key, value in acceptable_offers_n_quality_factors.items()
+ if value[0] # != 0.0
+ # We have to filter out the offers with qvalues of 0 here instead
+ # of just skipping them early in the large ``for`` loop because
+ # that would not work for e.g. when the header is 'text/html;q=0,
+ # text/html' (which does not make sense, but is nonetheless valid),
+ # and offers is ['text/html']
+ ]
+ # sort by offer_index, ascending
+ acceptable_offers_n_quality_factors.sort(key=lambda tuple_: tuple_[2])
+ # (stable) sort by qvalue, descending
+ acceptable_offers_n_quality_factors.sort(
+ key=lambda tuple_: tuple_[1], reverse=True,
+ )
+ # drop offer_index
+ acceptable_offers_n_quality_factors = [
+ (item[0], item[1]) for item in acceptable_offers_n_quality_factors
+ ]
+ return acceptable_offers_n_quality_factors
+ # If a media range is repeated in the header (which would not make
+ # sense, but would be valid according to the rules in the RFC), an
+ # offer for which the media range is the most specific match would take
+ # its qvalue from the first appearance of the range in the header.
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of media type `offers`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :meth:`AcceptValidHeader.best_match` uses its own algorithm (one not
+ specified in :rfc:`RFC 7231 <7231>`) to determine what is a best
+ match. The algorithm has many issues, and does not conform to
+ :rfc:`RFC 7231 <7231>`.
+
+ Each media type in `offers` is checked against each non-``q=0`` range
+ in the header. If the two are a match according to WebOb's old
+ criterion for a match, the quality value of the match is the qvalue of
+ the media range from the header multiplied by the server quality value
+ of the offer (if the server quality value is not supplied, it is 1).
+
+ The offer in the match with the highest quality value is the best
+ match. If there is more than one match with the highest qvalue, the
+ match where the media range has a lower number of '*'s is the best
+ match. If the two have the same number of '*'s, the one that shows up
+ first in `offers` is the best match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` media type,
+ or a (media type, server quality value) ``tuple`` or
+ ``list``. (The two may be mixed in the iterable.)
+
+ :param default_match: (optional, any type) the value to be returned if
+ there is no match
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The offer that is the best match. If there is no match, the
+ value of `default_match` is returned.
+
+ This uses the old criterion of a match in
+ :meth:`AcceptValidHeader._old_match`, which is not as specified in
+ :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
+ correctly take into account media type parameters:
+
+ >>> instance = AcceptValidHeader('text/html')
+ >>> instance.best_match(offers=['text/html;p=1']) is None
+ True
+
+ or media ranges with ``q=0`` in the header::
+
+ >>> instance = AcceptValidHeader('text/*, text/html;q=0')
+ >>> instance.best_match(offers=['text/html'])
+ 'text/html'
+
+ >>> instance = AcceptValidHeader('text/html;q=0, */*')
+ >>> instance.best_match(offers=['text/html'])
+ 'text/html'
+
+ (See the docstring for :meth:`AcceptValidHeader._old_match` for other
+ problems with the old criterion for matching.)
+
+ Another issue is that this method considers the best matching range for
+ an offer to be the matching range with the highest quality value,
+ (where quality values are tied, the most specific media range is
+ chosen); whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`
+ specifies that we should consider the best matching range for a media
+ type offer to be the most specific matching range.::
+
+ >>> instance = AcceptValidHeader('text/html;q=0.5, text/*')
+ >>> instance.best_match(offers=['text/html', 'text/plain'])
+ 'text/html'
+ """
+ warnings.warn(
+ 'The behavior of AcceptValidHeader.best_match is currently being '
+ 'maintained for backward compatibility, but it will be deprecated'
+ ' in the future, as it does not conform to the RFC.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ matched_by = '*/*'
+ for offer in offers:
+ if isinstance(offer, (tuple, list)):
+ offer, server_quality = offer
+ else:
+ server_quality = 1
+ for item in self._parsed_nonzero:
+ mask = item[0]
+ quality = item[1]
+ possible_quality = server_quality * quality
+ if possible_quality < best_quality:
+ continue
+ elif possible_quality == best_quality:
+ # 'text/plain' overrides 'message/*' overrides '*/*'
+ # (if all match w/ the same q=)
+ if matched_by.count('*') <= mask.count('*'):
+ continue
+ if self._old_match(mask, offer):
+ best_quality = possible_quality
+ best_offer = offer
+ matched_by = mask
+ return best_offer
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :param offer: (``str``) media type offer
+ :return: (``float`` or ``None``)
+
+ | The highest quality value from the media range(s) that match
+ the `offer`, or ``None`` if there is no match.
+
+ This uses the old criterion of a match in
+ :meth:`AcceptValidHeader._old_match`, which is not as specified in
+ :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
+ correctly take into account media type parameters:
+
+ >>> instance = AcceptValidHeader('text/html')
+ >>> instance.quality('text/html;p=1') is None
+ True
+
+ or media ranges with ``q=0`` in the header::
+
+ >>> instance = AcceptValidHeader('text/*, text/html;q=0')
+ >>> instance.quality('text/html')
+ 1.0
+ >>> AcceptValidHeader('text/html;q=0, */*').quality('text/html')
+ 1.0
+
+ (See the docstring for :meth:`AcceptValidHeader._old_match` for other
+ problems with the old criterion for matching.)
+
+ Another issue is that this method considers the best matching range for
+ an offer to be the matching range with the highest quality value,
+ whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` specifies
+ that we should consider the best matching range for a media type offer
+ to be the most specific matching range.::
+
+ >>> instance = AcceptValidHeader('text/html;q=0.5, text/*')
+ >>> instance.quality('text/html')
+ 1.0
+ """
+ warnings.warn(
+ 'The behavior of AcceptValidHeader.quality is currently being '
+ 'maintained for backward compatibility, but it will be deprecated '
+ 'in the future, as it does not conform to the RFC.',
+ DeprecationWarning,
+ )
+ bestq = 0
+ for item in self.parsed:
+ media_range = item[0]
+ qvalue = item[1]
+ if self._old_match(media_range, offer):
+ bestq = max(bestq, qvalue)
+ return bestq or None
+
+
+class MIMEAccept(Accept):
+ """
+ Backwards compatibility shim for the new functionality provided by
+ AcceptValidHeader, AcceptInvalidHeader, or AcceptNoHeader, that acts like
+ the old MIMEAccept from WebOb version 1.7 or lower.
+
+ This shim does use the newer Accept header parsing, which will mean your
+ application may be less liberal in what Accept headers are correctly
+ parsed. It is recommended that user agents be updated to send appropriate
+ Accept headers that are valid according to rfc:`RFC 7231, section 5.3.2
+ <7231#section-5.3.2>`
+
+ .. deprecated:: 1.8
+
+ Instead of directly creating the Accept object, please see:
+ :func:`create_accept_header(header_value)
+ <webob.acceptparse.create_accept_header>`, which will create the
+ appropriate object.
+
+ This shim has an extended deprecation period to allow for application
+ developers to switch the to new API.
+
+ """
+
+ def __init__(self, header_value):
+ warnings.warn(
+ 'The MIMEAccept class has been replaced by '
+ 'webob.acceptparse.create_accept_header. This compatibility shim '
+ 'will be deprecated in a future version of WebOb.',
+ DeprecationWarning
+ )
+ self._accept = create_accept_header(header_value)
+ if self._accept.parsed:
+ self._parsed = [(media, q) for (media, q, _, _) in self._accept.parsed]
+ self._parsed_nonzero = [(m, q) for (m, q) in self._parsed if q]
+ else:
+ self._parsed = []
+ self._parsed_nonzero = []
+
+ @staticmethod
+ def parse(value):
+ try:
+ parsed_accepted = Accept.parse(value)
+
+ for (media, q, _, _) in parsed_accepted:
+ yield (media, q)
+ except ValueError:
+ pass
+
+ def __repr__(self):
+ return self._accept.__repr__()
+
+ def __iter__(self):
+ return self._accept.__iter__()
+
+ def __str__(self):
+ return self._accept.__str__()
+
+ def __add__(self, other):
+ if isinstance(other, self.__class__):
+ return self.__class__(str(self._accept.__add__(other._accept)))
+ else:
+ return self.__class__(str(self._accept.__add__(other)))
+
+ def __radd__(self, other):
+ return self.__class__(str(self._accept.__radd__(other)))
+
+ def __contains__(self, offer):
+ return offer in self._accept
+
+ def quality(self, offer):
+ return self._accept.quality(offer)
+
+ def best_match(self, offers, default_match=None):
+ return self._accept.best_match(offers, default_match=default_match)
+
+ def accept_html(self):
+ return self._accept.accept_html()
+
+
+class _AcceptInvalidOrNoHeader(Accept):
+ """
+ Represent when an ``Accept`` header is invalid or not in request.
+
+ This is the base class for the behaviour that :class:`.AcceptInvalidHeader`
+ and :class:`.AcceptNoHeader` have in common.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept`` header has an invalid value. This implementation disregards the
+ header when the header is invalid, so :class:`.AcceptInvalidHeader` and
+ :class:`.AcceptNoHeader` have much behaviour in common.
+ """
+
+ def __bool__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``False``.
+ """
+ return False
+ __nonzero__ = __bool__ # Python 2
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of ``.__contains__`` for the ``Accept`` classes is
+ currently being maintained for backward compatibility, but it will
+ change in the future to better conform to the RFC.
+
+ :param offer: (``str``) media type offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ For this class, either there is no ``Accept`` header in the request, or
+ the header is invalid, so any media type is acceptable, and this always
+ returns ``True``.
+ """
+ warnings.warn(
+ 'The behavior of .__contains__ for the Accept classes is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ return True
+
+ def __iter__(self):
+ """
+ Return all the ranges with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the media ranges in the header with non-0
+ qvalues, in descending order of qvalue. If two ranges have the
+ same qvalue, they are returned in the order of their positions
+ in the header, from left to right.
+
+ When there is no ``Accept`` header in the request or the header is
+ invalid, there are no media ranges, so this always returns an empty
+ iterator.
+ """
+ warnings.warn(
+ 'The behavior of AcceptValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+ return iter(())
+
+ def accept_html(self):
+ """
+ Return ``True`` if any HTML-like type is accepted.
+
+ The HTML-like types are 'text/html', 'application/xhtml+xml',
+ 'application/xml' and 'text/xml'.
+
+ When the header is invalid, or there is no `Accept` header in the
+ request, all `offers` are considered acceptable, so this always returns
+ ``True``.
+ """
+ return bool(
+ self.acceptable_offers(
+ offers=[
+ 'text/html',
+ 'application/xhtml+xml',
+ 'application/xml',
+ 'text/xml',
+ ],
+ )
+ )
+ accepts_html = property(fget=accept_html, doc=accept_html.__doc__)
+ # note the plural
+
+ def acceptable_offers(self, offers):
+ """
+ Return the offers that are acceptable according to the header.
+
+ Any offers that cannot be parsed via
+ :meth:`.Accept.parse_offer` will be ignored.
+
+ :param offers: ``iterable`` of ``str`` media types (media types can
+ include media type parameters)
+ :return: When the header is invalid, or there is no ``Accept`` header
+ in the request, all `offers` are considered acceptable, so
+ this method returns a list of (media type, qvalue) tuples
+ where each offer in `offers` is paired with the qvalue of 1.0,
+ in the same order as in `offers`.
+ """
+ return [
+ (offers[offer_index], 1.0)
+ for offer_index, _
+ # avoid returning any offers that don't match the grammar so
+ # that the return values here are consistent with what would be
+ # returned in AcceptValidHeader
+ in self._parse_and_normalize_offers(offers)
+ ]
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of language tag `offers`.
+
+ This is the ``.best_match()`` method for when the header is invalid or
+ not found in the request, corresponding to
+ :meth:`AcceptValidHeader.best_match`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptValidHeader.best_match`).
+
+ When the header is invalid, or there is no `Accept` header in the
+ request, all `offers` are considered acceptable, so the best match is
+ the media type in `offers` with the highest server quality value (if
+ the server quality value is not supplied for a media type, it is 1).
+
+ If more than one media type in `offers` have the same highest server
+ quality value, then the one that shows up first in `offers` is the best
+ match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` media type,
+ or a (media type, server quality value) ``tuple`` or
+ ``list``. (The two may be mixed in the iterable.)
+
+ :param default_match: (optional, any type) the value to be returned if
+ `offers` is empty.
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The offer that has the highest server quality value. If
+ `offers` is empty, the value of `default_match` is returned.
+ """
+ warnings.warn(
+ 'The behavior of .best_match for the Accept classes is currently '
+ 'being maintained for backward compatibility, but the method will'
+ ' be deprecated in the future, as its behavior is not specified '
+ 'in (and currently does not conform to) RFC 7231.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ for offer in offers:
+ if isinstance(offer, (list, tuple)):
+ offer, quality = offer
+ else:
+ quality = 1
+ if quality > best_quality:
+ best_offer = offer
+ best_quality = quality
+ return best_offer
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ This is the ``.quality()`` method for when the header is invalid or not
+ found in the request, corresponding to
+ :meth:`AcceptValidHeader.quality`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptValidHeader.quality`).
+
+ :param offer: (``str``) media type offer
+ :return: (``float``) ``1.0``.
+
+ When the ``Accept`` header is invalid or not in the request, all offers
+ are equally acceptable, so 1.0 is always returned.
+ """
+ warnings.warn(
+ 'The behavior of .quality for the Accept classes is currently '
+ 'being maintained for backward compatibility, but the method will'
+ ' be deprecated in the future, as its behavior does not conform to'
+ 'RFC 7231.',
+ DeprecationWarning,
+ )
+ return 1.0
+
+
+class AcceptNoHeader(_AcceptInvalidOrNoHeader):
+ """
+ Represent when there is no ``Accept`` header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptNoHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """
+ (``str`` or ``None``) The header value.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._parsed
+
+ def __init__(self):
+ """
+ Create an :class:`AcceptNoHeader` instance.
+ """
+ self._header_value = None
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__()
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with media ranges ``str``'s (including any media type
+ parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
+ *extension_params*) tuples as values, where *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
+ or ``list`` where *media_range* is a ``str`` of the media range
+ including any media type parameters, and *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
+ :class:`AcceptInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an :class:`AcceptValidHeader`
+ instance, a new :class:`AcceptValidHeader` instance with the valid
+ header value is returned.
+
+ If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid
+ header value, or an :class:`AcceptInvalidHeader` instance, a new
+ :class:`AcceptNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptValidHeader):
+ return AcceptValidHeader(header_value=other.header_value)
+
+ if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
+ return self.__class__()
+
+ return self._add_instance_and_non_accept_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptNoHeader.__add__`.
+ """
+ return self.__add__(other=other)
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+
+ def __str__(self):
+ """Return the ``str`` ``'<no header in request>'``."""
+ return '<no header in request>'
+
+ def _add_instance_and_non_accept_type(self, instance, other):
+ if other is None:
+ return self.__class__()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__()
+
+
+class AcceptInvalidHeader(_AcceptInvalidOrNoHeader):
+ """
+ Represent an invalid ``Accept`` header.
+
+ An invalid header is one that does not conform to
+ :rfc:`7231#section-5.3.2`.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept`` header has an invalid value. This implementation disregards the
+ header, and treats it as if there is no ``Accept`` header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptInvalidHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As the header is invalid and cannot be parsed, this is ``None``.
+ """
+ return self._parsed
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptInvalidHeader` instance.
+ """
+ self._header_value = header_value
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with media ranges ``str``'s (including any media type
+ parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
+ *extension_params*) tuples as values, where *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
+ or ``list`` where *media_range* is a ``str`` of the media range
+ including any media type parameters, and *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
+ :class:`AcceptInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an :class:`AcceptValidHeader`
+ instance, then a new :class:`AcceptValidHeader` instance with the valid
+ header value is returned.
+
+ If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid
+ header value, or an :class:`AcceptInvalidHeader` instance, a new
+ :class:`AcceptNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptValidHeader):
+ return AcceptValidHeader(header_value=other.header_value)
+
+ if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
+ return AcceptNoHeader()
+
+ return self._add_instance_and_non_accept_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+ # We do not display the header_value, as it is untrusted input. The
+ # header_value could always be easily obtained from the .header_value
+ # property.
+
+ def __str__(self):
+ """Return the ``str`` ``'<invalid header value>'``."""
+ return '<invalid header value>'
+
+ def _add_instance_and_non_accept_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if other is None:
+ return AcceptNoHeader()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return AcceptNoHeader()
+
+
+def create_accept_header(header_value):
+ """
+ Create an object representing the ``Accept`` header in a request.
+
+ :param header_value: (``str``) header value
+ :return: If `header_value` is ``None``, an :class:`AcceptNoHeader`
+ instance.
+
+ | If `header_value` is a valid ``Accept`` header, an
+ :class:`AcceptValidHeader` instance.
+
+ | If `header_value` is an invalid ``Accept`` header, an
+ :class:`AcceptInvalidHeader` instance.
+ """
+ if header_value is None:
+ return AcceptNoHeader()
+ if isinstance(header_value, Accept):
+ return header_value.copy()
+ try:
+ return AcceptValidHeader(header_value=header_value)
+ except ValueError:
+ return AcceptInvalidHeader(header_value=header_value)
+
+
+def accept_property():
+ doc = """
+ Property representing the ``Accept`` header.
+
+ (:rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`)
+
+ The header value in the request environ is parsed and a new object
+ representing the header is created every time we *get* the value of the
+ property. (*set* and *del* change the header value in the request
+ environ, and do not involve parsing.)
+ """
+
+ ENVIRON_KEY = 'HTTP_ACCEPT'
+
+ def fget(request):
+ """Get an object representing the header in the request."""
+ return create_accept_header(
+ header_value=request.environ.get(ENVIRON_KEY)
+ )
+
+ def fset(request, value):
+ """
+ Set the corresponding key in the request environ.
+
+ `value` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with media ranges ``str``'s (including any media type
+ parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
+ *extension_params*) tuples as values, where *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
+ or ``list`` where *media_range* is a ``str`` of the media range
+ including any media type parameters, and *extension_params* is a
+ ``str`` of the extension parameters segment of the header element,
+ starting with the first '``;``'
+ * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
+ :class:`AcceptInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+ """
+ if value is None or isinstance(value, AcceptNoHeader):
+ fdel(request=request)
+ else:
+ if isinstance(value, (AcceptValidHeader, AcceptInvalidHeader)):
+ header_value = value.header_value
+ else:
+ header_value = Accept._python_value_to_header_str(value=value)
+ request.environ[ENVIRON_KEY] = header_value
+
+ def fdel(request):
+ """Delete the corresponding key from the request environ."""
+ try:
+ del request.environ[ENVIRON_KEY]
+ except KeyError:
+ pass
+
+ return property(fget, fset, fdel, textwrap.dedent(doc))
+
+
+class AcceptCharset(object):
+ """
+ Represent an ``Accept-Charset`` header.
+
+ Base class for :class:`AcceptCharsetValidHeader`,
+ :class:`AcceptCharsetNoHeader`, and :class:`AcceptCharsetInvalidHeader`.
+ """
+
+ # RFC 7231 Section 3.1.1.2 "Charset":
+ # charset = token
+ charset_re = token_re
+ # RFC 7231 Section 5.3.3 "Accept-Charset":
+ # Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
+ charset_n_weight_re = _item_n_weight_re(item_re=charset_re)
+ charset_n_weight_compiled_re = re.compile(charset_n_weight_re)
+ accept_charset_compiled_re = _list_1_or_more__compiled_re(
+ element_re=charset_n_weight_re,
+ )
+
+ @classmethod
+ def _python_value_to_header_str(cls, value):
+ if isinstance(value, str):
+ header_str = value
+ else:
+ if hasattr(value, 'items'):
+ value = sorted(
+ value.items(),
+ key=lambda item: item[1],
+ reverse=True,
+ )
+ if isinstance(value, (tuple, list)):
+ result = []
+ for item in value:
+ if isinstance(item, (tuple, list)):
+ item = _item_qvalue_pair_to_header_element(pair=item)
+ result.append(item)
+ header_str = ', '.join(result)
+ else:
+ header_str = str(value)
+ return header_str
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse an ``Accept-Charset`` header.
+
+ :param value: (``str``) header value
+ :return: If `value` is a valid ``Accept-Charset`` header, returns an
+ iterator of (charset, quality value) tuples, as parsed from
+ the header from left to right.
+ :raises ValueError: if `value` is an invalid header
+ """
+ # Check if header is valid
+ # Using Python stdlib's `re` module, there is currently no way to check
+ # the match *and* get all the groups using the same regex, so we have
+ # to use one regex to check the match, and another to get the groups.
+ if cls.accept_charset_compiled_re.match(value) is None:
+ raise ValueError('Invalid value for an Accept-Charset header.')
+ def generator(value):
+ for match in (cls.charset_n_weight_compiled_re.finditer(value)):
+ charset = match.group(1)
+ qvalue = match.group(2)
+ qvalue = float(qvalue) if qvalue else 1.0
+ yield (charset, qvalue)
+ return generator(value=value)
+
+
+class AcceptCharsetValidHeader(AcceptCharset):
+ """
+ Represent a valid ``Accept-Charset`` header.
+
+ A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.3
+ <7231#section-5.3.3>`.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptCharsetValidHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """(``str``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list``) Parsed form of the header.
+
+ A list of (charset, quality value) tuples.
+ """
+ return self._parsed
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptCharsetValidHeader` instance.
+
+ :param header_value: (``str``) header value.
+ :raises ValueError: if `header_value` is an invalid value for an
+ ``Accept-Charset`` header.
+ """
+ self._header_value = header_value
+ self._parsed = list(self.parse(header_value))
+ self._parsed_nonzero = [
+ item for item in self.parsed if item[1] # item[1] is the qvalue
+ ]
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, where keys are charsets and values are qvalues
+ * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
+ ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
+ can be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
+ or :class:`AcceptCharsetInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or another
+ :class:`AcceptCharsetValidHeader` instance, the two header values are
+ joined with ``', '``, and a new :class:`AcceptCharsetValidHeader`
+ instance with the new header value is returned.
+
+ If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
+ invalid header value, or an :class:`AcceptCharsetInvalidHeader`
+ instance, a new :class:`AcceptCharsetValidHeader` instance with the
+ same header value as ``self`` is returned.
+ """
+ if isinstance(other, AcceptCharsetValidHeader):
+ return create_accept_charset_header(
+ header_value=self.header_value + ', ' + other.header_value,
+ )
+
+ if isinstance(
+ other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
+ ):
+ return self.__class__(header_value=self.header_value)
+
+ return self._add_instance_and_non_accept_charset_type(
+ instance=self, other=other,
+ )
+
+ def __bool__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept-Charset`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``True``.
+ """
+ return True
+ __nonzero__ = __bool__ # Python 2
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of :meth:`AcceptCharsetValidHeader.__contains__` is
+ currently being maintained for backward compatibility, but it will
+ change in the future to better conform to the RFC.
+
+ :param offer: (``str``) charset offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ This does not fully conform to :rfc:`RFC 7231, section 5.3.3
+ <7231#section-5.3.3>`: it incorrect interprets ``*`` to mean 'match any
+ charset in the header', rather than 'match any charset that is not
+ mentioned elsewhere in the header'::
+
+ >>> 'UTF-8' in AcceptCharsetValidHeader('UTF-8;q=0, *')
+ True
+ """
+ warnings.warn(
+ 'The behavior of AcceptCharsetValidHeader.__contains__ is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ for mask, quality in self._parsed_nonzero:
+ if self._old_match(mask, offer):
+ return True
+ return False
+
+ def __iter__(self):
+ """
+ Return all the items with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the items (charset or ``*``) in the header
+ with non-0 qvalues, in descending order of qvalue. If two
+ items have the same qvalue, they are returned in the order of
+ their positions in the header, from left to right.
+
+ Please note that this is a simple filter for the items in the header
+ with non-0 qvalues, and is not necessarily the same as what the client
+ prefers, e.g. ``'utf-7;q=0, *'`` means 'everything but utf-7', but
+ ``list(instance)`` would return only ``['*']``.
+ """
+ warnings.warn(
+ 'The behavior of AcceptCharsetValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+ for m,q in sorted(
+ self._parsed_nonzero,
+ key=lambda i: i[1],
+ reverse=True
+ ):
+ yield m
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptCharsetValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_charset_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
+
+ def __str__(self):
+ r"""
+ Return a tidied up version of the header value.
+
+ e.g. If the ``header_value`` is ``', \t,iso-8859-5;q=0.000 \t,
+ utf-8;q=1.000, UTF-7, unicode-1-1;q=0.210 ,'``, ``str(instance)``
+ returns ``'iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21'``.
+ """
+ return ', '.join(
+ _item_qvalue_pair_to_header_element(pair=tuple_)
+ for tuple_ in self.parsed
+ )
+
+ def _add_instance_and_non_accept_charset_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if not other:
+ return self.__class__(header_value=instance.header_value)
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ self.parse(value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__(header_value=instance.header_value)
+
+ new_header_value = (
+ (other_header_value + ', ' + instance.header_value)
+ if instance_on_the_right
+ else (instance.header_value + ', ' + other_header_value)
+ )
+ return self.__class__(header_value=new_header_value)
+
+ def _old_match(self, mask, offer):
+ """
+ Return whether charset offer matches header item (charset or ``*``).
+
+ .. warning::
+
+ This is maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ This method was WebOb's old criterion for deciding whether a charset
+ matches a header item (charset or ``*``), used in
+
+ - :meth:`AcceptCharsetValidHeader.__contains__`
+ - :meth:`AcceptCharsetValidHeader.best_match`
+ - :meth:`AcceptCharsetValidHeader.quality`
+
+ It does not conform to :rfc:`RFC 7231, section 5.3.3
+ <7231#section-5.3.3>` in that it does not interpret ``*`` values in the
+ header correctly: ``*`` should only match charsets not mentioned
+ elsewhere in the header.
+ """
+ return mask == '*' or offer.lower() == mask.lower()
+
+ def acceptable_offers(self, offers):
+ """
+ Return the offers that are acceptable according to the header.
+
+ The offers are returned in descending order of preference, where
+ preference is indicated by the qvalue of the charset or ``*`` in the
+ header matching the offer.
+
+ This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3
+ <7231#section-5.3.3>`.
+
+ :param offers: ``iterable`` of ``str`` charsets
+ :return: A list of tuples of the form (charset, qvalue), in descending
+ order of qvalue. Where two offers have the same qvalue, they
+ are returned in the same order as their order in `offers`.
+ """
+ lowercased_parsed = [
+ (charset.lower(), qvalue) for (charset, qvalue) in self.parsed
+ ]
+ lowercased_offers = [offer.lower() for offer in offers]
+
+ not_acceptable_charsets = set()
+ acceptable_charsets = dict()
+ asterisk_qvalue = None
+
+ for charset, qvalue in lowercased_parsed:
+ if charset == '*':
+ if asterisk_qvalue is None:
+ asterisk_qvalue = qvalue
+ elif (
+ charset not in acceptable_charsets and charset not in
+ not_acceptable_charsets
+ # if we have not already encountered this charset in the header
+ ):
+ if qvalue == 0.0:
+ not_acceptable_charsets.add(charset)
+ else:
+ acceptable_charsets[charset] = qvalue
+ acceptable_charsets = list(acceptable_charsets.items())
+ # Sort acceptable_charsets by qvalue, descending order
+ acceptable_charsets.sort(key=lambda tuple_: tuple_[1], reverse=True)
+
+ filtered_offers = []
+ for index, offer in enumerate(lowercased_offers):
+ # If offer matches a non-* charset with q=0, it is filtered out
+ if any((
+ (offer == charset) for charset in not_acceptable_charsets
+ )):
+ continue
+
+ matched_charset_qvalue = None
+ for charset, qvalue in acceptable_charsets:
+ if offer == charset:
+ matched_charset_qvalue = qvalue
+ break
+ else:
+ if asterisk_qvalue:
+ matched_charset_qvalue = asterisk_qvalue
+ if matched_charset_qvalue is not None: # if there was a match
+ filtered_offers.append((
+ offers[index], matched_charset_qvalue, index
+ ))
+
+ # sort by position in `offers` argument, ascending
+ filtered_offers.sort(key=lambda tuple_: tuple_[2])
+ # When qvalues are tied, position in `offers` is the tiebreaker.
+
+ # sort by qvalue, descending
+ filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True)
+
+ return [(item[0], item[1]) for item in filtered_offers]
+ # (offer, qvalue), dropping the position
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of charset `offers`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :meth:`AcceptCharsetValidHeader.best_match` has many issues, and
+ does not conform to :rfc:`RFC 7231 <7231>`.
+
+ Each charset in `offers` is checked against each non-``q=0`` item
+ (charset or ``*``) in the header. If the two are a match according to
+ WebOb's old criterion for a match, the quality value of the match is
+ the qvalue of the item from the header multiplied by the server quality
+ value of the offer (if the server quality value is not supplied, it is
+ 1).
+
+ The offer in the match with the highest quality value is the best
+ match. If there is more than one match with the highest qvalue, the one
+ that shows up first in `offers` is the best match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` charset, or
+ a (charset, server quality value) ``tuple`` or
+ ``list``. (The two may be mixed in the iterable.)
+
+ :param default_match: (optional, any type) the value to be returned if
+ there is no match
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The offer that is the best match. If there is no match, the
+ value of `default_match` is returned.
+
+ The algorithm behind this method was written for the ``Accept`` header
+ rather than the ``Accept-Charset`` header. It uses the old criterion of
+ a match in :meth:`AcceptCharsetValidHeader._old_match`, which does not
+ conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that
+ it does not interpret ``*`` values in the header correctly: ``*``
+ should only match charsets not mentioned elsewhere in the header::
+
+ >>> AcceptCharsetValidHeader('utf-8;q=0, *').best_match(['utf-8'])
+ 'utf-8'
+ """
+ warnings.warn(
+ 'The behavior of AcceptCharsetValidHeader.best_match is currently'
+ ' being maintained for backward compatibility, but it will be '
+ 'deprecated in the future, as it does not conform to the RFC.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ matched_by = '*/*'
+ for offer in offers:
+ if isinstance(offer, (tuple, list)):
+ offer, server_quality = offer
+ else:
+ server_quality = 1
+ for mask, quality in self._parsed_nonzero:
+ possible_quality = server_quality * quality
+ if possible_quality < best_quality:
+ continue
+ elif possible_quality == best_quality:
+ # 'text/plain' overrides 'message/*' overrides '*/*'
+ # (if all match w/ the same q=)
+ # [We can see that this was written for the Accept header,
+ # not the Accept-Charset header.]
+ if matched_by.count('*') <= mask.count('*'):
+ continue
+ if self._old_match(mask, offer):
+ best_quality = possible_quality
+ best_offer = offer
+ matched_by = mask
+ return best_offer
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :param offer: (``str``) charset offer
+ :return: (``float`` or ``None``)
+
+ | The quality value from the charset that matches the `offer`,
+ or ``None`` if there is no match.
+
+ This uses the old criterion of a match in
+ :meth:`AcceptCharsetValidHeader._old_match`, which does not conform to
+ :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that it does
+ not interpret ``*`` values in the header correctly: ``*`` should only
+ match charsets not mentioned elsewhere in the header::
+
+ >>> AcceptCharsetValidHeader('utf-8;q=0, *').quality('utf-8')
+ 1.0
+ >>> AcceptCharsetValidHeader('utf-8;q=0.9, *').quality('utf-8')
+ 1.0
+ """
+ warnings.warn(
+ 'The behavior of AcceptCharsetValidHeader.quality is currently '
+ 'being maintained for backward compatibility, but it will be '
+ 'deprecated in the future, as it does not conform to the RFC.',
+ DeprecationWarning,
+ )
+ bestq = 0
+ for mask, q in self.parsed:
+ if self._old_match(mask, offer):
+ bestq = max(bestq, q)
+ return bestq or None
+
+
+class _AcceptCharsetInvalidOrNoHeader(AcceptCharset):
+ """
+ Represent when an ``Accept-Charset`` header is invalid or not in request.
+
+ This is the base class for the behaviour that
+ :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader`
+ have in common.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept-Charset`` header has an invalid value. This implementation
+ disregards the header when the header is invalid, so
+ :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader`
+ have much behaviour in common.
+ """
+
+ def __bool__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept-Charset`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``False``.
+ """
+ return False
+ __nonzero__ = __bool__ # Python 2
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of ``.__contains__`` for the ``AcceptCharset`` classes
+ is currently being maintained for backward compatibility, but it
+ will change in the future to better conform to the RFC.
+
+ :param offer: (``str``) charset offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ For this class, either there is no ``Accept-Charset`` header in the
+ request, or the header is invalid, so any charset is acceptable, and
+ this always returns ``True``.
+ """
+ warnings.warn(
+ 'The behavior of .__contains__ for the AcceptCharset classes is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ return True
+
+ def __iter__(self):
+ """
+ Return all the items with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the items (charset or ``*``) in the header
+ with non-0 qvalues, in descending order of qvalue. If two
+ items have the same qvalue, they are returned in the order of
+ their positions in the header, from left to right.
+
+ When there is no ``Accept-Charset`` header in the request or the header
+ is invalid, there are no items, and this always returns an empty
+ iterator.
+ """
+ warnings.warn(
+ 'The behavior of AcceptCharsetValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+ return iter(())
+
+ def acceptable_offers(self, offers):
+ """
+ Return the offers that are acceptable according to the header.
+
+ The offers are returned in descending order of preference, where
+ preference is indicated by the qvalue of the charset or ``*`` in the
+ header matching the offer.
+
+ This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3
+ <7231#section-5.3.3>`.
+
+ :param offers: ``iterable`` of ``str`` charsets
+ :return: A list of tuples of the form (charset, qvalue), in descending
+ order of qvalue. Where two offers have the same qvalue, they
+ are returned in the same order as their order in `offers`.
+
+ | When the header is invalid or there is no ``Accept-Charset``
+ header in the request, all `offers` are considered
+ acceptable, so this method returns a list of (charset,
+ qvalue) tuples where each offer in `offers` is paired with
+ the qvalue of 1.0, in the same order as `offers`.
+ """
+ return [(offer, 1.0) for offer in offers]
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of charset `offers`.
+
+ This is the ``.best_match()`` method for when the header is invalid or
+ not found in the request, corresponding to
+ :meth:`AcceptCharsetValidHeader.best_match`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptCharsetValidHeader.best_match`).
+
+ When the header is invalid, or there is no `Accept-Charset` header in
+ the request, all the charsets in `offers` are considered acceptable, so
+ the best match is the charset in `offers` with the highest server
+ quality value (if the server quality value is not supplied, it is 1).
+
+ If more than one charsets in `offers` have the same highest server
+ quality value, then the one that shows up first in `offers` is the best
+ match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` charset, or
+ a (charset, server quality value) ``tuple`` or
+ ``list``. (The two may be mixed in the iterable.)
+
+ :param default_match: (optional, any type) the value to be returned if
+ `offers` is empty.
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The charset that has the highest server quality value. If
+ `offers` is empty, the value of `default_match` is returned.
+ """
+ warnings.warn(
+ 'The behavior of .best_match for the AcceptCharset classes is '
+ 'currently being maintained for backward compatibility, but the '
+ 'method will be deprecated in the future, as its behavior is not '
+ 'specified in (and currently does not conform to) RFC 7231.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ for offer in offers:
+ if isinstance(offer, (list, tuple)):
+ offer, quality = offer
+ else:
+ quality = 1
+ if quality > best_quality:
+ best_offer = offer
+ best_quality = quality
+ return best_offer
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ This is the ``.quality()`` method for when the header is invalid or not
+ found in the request, corresponding to
+ :meth:`AcceptCharsetValidHeader.quality`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptCharsetValidHeader.quality`).
+
+ :param offer: (``str``) charset offer
+ :return: (``float``) ``1.0``.
+
+ When the ``Accept-Charset`` header is invalid or not in the request,
+ all offers are equally acceptable, so 1.0 is always returned.
+ """
+ warnings.warn(
+ 'The behavior of .quality for the Accept-Charset classes is '
+ 'currently being maintained for backward compatibility, but the '
+ 'method will be deprecated in the future, as its behavior does not'
+ ' conform to RFC 7231.',
+ DeprecationWarning,
+ )
+ return 1.0
+
+
+class AcceptCharsetNoHeader(_AcceptCharsetInvalidOrNoHeader):
+ """
+ Represent when there is no ``Accept-Charset`` header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptCharsetNoHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """
+ (``str`` or ``None``) The header value.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._parsed
+
+ def __init__(self):
+ """
+ Create an :class:`AcceptCharsetNoHeader` instance.
+ """
+ self._header_value = None
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__()
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, where keys are charsets and values are qvalues
+ * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
+ ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
+ can be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
+ or :class:`AcceptCharsetInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an
+ :class:`AcceptCharsetValidHeader` instance, a new
+ :class:`AcceptCharsetValidHeader` instance with the valid header value
+ is returned.
+
+ If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
+ invalid header value, or an :class:`AcceptCharsetInvalidHeader`
+ instance, a new :class:`AcceptCharsetNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptCharsetValidHeader):
+ return AcceptCharsetValidHeader(header_value=other.header_value)
+
+ if isinstance(
+ other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
+ ):
+ return self.__class__()
+
+ return self._add_instance_and_non_accept_charset_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptCharsetNoHeader.__add__`.
+ """
+ return self.__add__(other=other)
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+
+ def __str__(self):
+ """Return the ``str`` ``'<no header in request>'``."""
+ return '<no header in request>'
+
+ def _add_instance_and_non_accept_charset_type(self, instance, other):
+ if not other:
+ return self.__class__()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptCharsetValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__()
+
+
+class AcceptCharsetInvalidHeader(_AcceptCharsetInvalidOrNoHeader):
+ """
+ Represent an invalid ``Accept-Charset`` header.
+
+ An invalid header is one that does not conform to
+ :rfc:`7231#section-5.3.3`. As specified in the RFC, an empty header is an
+ invalid ``Accept-Charset`` header.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept-Charset`` header has an invalid value. This implementation
+ disregards the header, and treats it as if there is no ``Accept-Charset``
+ header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptCharsetInvalidHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As the header is invalid and cannot be parsed, this is ``None``.
+ """
+ return self._parsed
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptCharsetInvalidHeader` instance.
+ """
+ self._header_value = header_value
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, where keys are charsets and values are qvalues
+ * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
+ ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
+ can be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
+ or :class:`AcceptCharsetInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an
+ :class:`AcceptCharsetValidHeader` instance, a new
+ :class:`AcceptCharsetValidHeader` instance with the valid header value
+ is returned.
+
+ If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
+ invalid header value, or an :class:`AcceptCharsetInvalidHeader`
+ instance, a new :class:`AcceptCharsetNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptCharsetValidHeader):
+ return AcceptCharsetValidHeader(header_value=other.header_value)
+
+ if isinstance(
+ other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
+ ):
+ return AcceptCharsetNoHeader()
+
+ return self._add_instance_and_non_accept_charset_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptCharsetValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_charset_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+ # We do not display the header_value, as it is untrusted input. The
+ # header_value could always be easily obtained from the .header_value
+ # property.
+
+ def __str__(self):
+ """Return the ``str`` ``'<invalid header value>'``."""
+ return '<invalid header value>'
+
+ def _add_instance_and_non_accept_charset_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if not other:
+ return AcceptCharsetNoHeader()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptCharsetValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return AcceptCharsetNoHeader()
+
+
+def create_accept_charset_header(header_value):
+ """
+ Create an object representing the ``Accept-Charset`` header in a request.
+
+ :param header_value: (``str``) header value
+ :return: If `header_value` is ``None``, an :class:`AcceptCharsetNoHeader`
+ instance.
+
+ | If `header_value` is a valid ``Accept-Charset`` header, an
+ :class:`AcceptCharsetValidHeader` instance.
+
+ | If `header_value` is an invalid ``Accept-Charset`` header, an
+ :class:`AcceptCharsetInvalidHeader` instance.
+ """
+ if header_value is None:
+ return AcceptCharsetNoHeader()
+ if isinstance(header_value, AcceptCharset):
+ return header_value.copy()
+ try:
+ return AcceptCharsetValidHeader(header_value=header_value)
+ except ValueError:
+ return AcceptCharsetInvalidHeader(header_value=header_value)
+
+
+def accept_charset_property():
+ doc = """
+ Property representing the ``Accept-Charset`` header.
+
+ (:rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`)
+
+ The header value in the request environ is parsed and a new object
+ representing the header is created every time we *get* the value of the
+ property. (*set* and *del* change the header value in the request
+ environ, and do not involve parsing.)
+ """
+
+ ENVIRON_KEY = 'HTTP_ACCEPT_CHARSET'
+
+ def fget(request):
+ """Get an object representing the header in the request."""
+ return create_accept_charset_header(
+ header_value=request.environ.get(ENVIRON_KEY)
+ )
+
+ def fset(request, value):
+ """
+ Set the corresponding key in the request environ.
+
+ `value` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, where keys are charsets and values are qvalues
+ * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
+ ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
+ can be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
+ or :class:`AcceptCharsetInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+ """
+ if value is None or isinstance(value, AcceptCharsetNoHeader):
+ fdel(request=request)
+ else:
+ if isinstance(
+ value, (AcceptCharsetValidHeader, AcceptCharsetInvalidHeader)
+ ):
+ header_value = value.header_value
+ else:
+ header_value = AcceptCharset._python_value_to_header_str(
+ value=value,
+ )
+ request.environ[ENVIRON_KEY] = header_value
+
+ def fdel(request):
+ """Delete the corresponding key from the request environ."""
+ try:
+ del request.environ[ENVIRON_KEY]
+ except KeyError:
+ pass
+
+ return property(fget, fset, fdel, textwrap.dedent(doc))
+
+
+class AcceptEncoding(object):
+ """
+ Represent an ``Accept-Encoding`` header.
+
+ Base class for :class:`AcceptEncodingValidHeader`,
+ :class:`AcceptEncodingNoHeader`, and :class:`AcceptEncodingInvalidHeader`.
+ """
+
+ # RFC 7231 Section 3.1.2.1 "Content Codings":
+ # content-coding = token
+ # Section 5.3.4 "Accept-Encoding":
+ # Accept-Encoding = #( codings [ weight ] )
+ # codings = content-coding / "identity" / "*"
+ codings_re = token_re
+ # "identity" (case-insensitive) and "*" are both already included in token
+ # rule
+ codings_n_weight_re = _item_n_weight_re(item_re=codings_re)
+ codings_n_weight_compiled_re = re.compile(codings_n_weight_re)
+ accept_encoding_compiled_re = _list_0_or_more__compiled_re(
+ element_re=codings_n_weight_re,
+ )
+
+ @classmethod
+ def _python_value_to_header_str(cls, value):
+ if isinstance(value, str):
+ header_str = value
+ else:
+ if hasattr(value, 'items'):
+ value = sorted(
+ value.items(),
+ key=lambda item: item[1],
+ reverse=True,
+ )
+ if isinstance(value, (tuple, list)):
+ result = []
+ for item in value:
+ if isinstance(item, (tuple, list)):
+ item = _item_qvalue_pair_to_header_element(pair=item)
+ result.append(item)
+ header_str = ', '.join(result)
+ else:
+ header_str = str(value)
+ return header_str
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse an ``Accept-Encoding`` header.
+
+ :param value: (``str``) header value
+ :return: If `value` is a valid ``Accept-Encoding`` header, returns an
+ iterator of (codings, quality value) tuples, as parsed from
+ the header from left to right.
+ :raises ValueError: if `value` is an invalid header
+ """
+ # Check if header is valid
+ # Using Python stdlib's `re` module, there is currently no way to check
+ # the match *and* get all the groups using the same regex, so we have
+ # to use one regex to check the match, and another to get the groups.
+ if cls.accept_encoding_compiled_re.match(value) is None:
+ raise ValueError('Invalid value for an Accept-Encoding header.')
+ def generator(value):
+ for match in (cls.codings_n_weight_compiled_re.finditer(value)):
+ codings = match.group(1)
+ qvalue = match.group(2)
+ qvalue = float(qvalue) if qvalue else 1.0
+ yield (codings, qvalue)
+ return generator(value=value)
+
+
+class AcceptEncodingValidHeader(AcceptEncoding):
+ """
+ Represent a valid ``Accept-Encoding`` header.
+
+ A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.4
+ <7231#section-5.3.4>`.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptEncodingValidHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ A list of (*codings*, *qvalue*) tuples, where
+
+ *codings* (``str``) is a content-coding, the string "``identity``", or
+ "``*``"; and
+
+ *qvalue* (``float``) is the quality value of the codings.
+ """
+ return self._parsed
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptEncodingValidHeader` instance.
+
+ :param header_value: (``str``) header value.
+ :raises ValueError: if `header_value` is an invalid value for an
+ ``Accept-Encoding`` header.
+ """
+ self._header_value = header_value
+ self._parsed = list(self.parse(header_value))
+ self._parsed_nonzero = [item for item in self.parsed if item[1]]
+ # item[1] is the qvalue
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
+ keys, and qvalue ``float``'s as values
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
+ or ``list``
+ * an :class:`AcceptEncodingValidHeader`,
+ :class:`AcceptEncodingNoHeader`, or
+ :class:`AcceptEncodingInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or another
+ :class:`AcceptEncodingValidHeader` instance, and the header value it
+ represents is not ``''``, then the two header values are joined with
+ ``', '``, and a new :class:`AcceptEncodingValidHeader` instance with
+ the new header value is returned.
+
+ If `other` is a valid header value or another
+ :class:`AcceptEncodingValidHeader` instance representing a header value
+ of ``''``; or if it is ``None`` or an :class:`AcceptEncodingNoHeader`
+ instance; or if it is an invalid header value, or an
+ :class:`AcceptEncodingInvalidHeader` instance, then a new
+ :class:`AcceptEncodingValidHeader` instance with the same header value
+ as ``self`` is returned.
+ """
+ if isinstance(other, AcceptEncodingValidHeader):
+ if other.header_value == '':
+ return self.__class__(header_value=self.header_value)
+ else:
+ return create_accept_encoding_header(
+ header_value=self.header_value + ', ' + other.header_value,
+ )
+
+ if isinstance(
+ other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
+ ):
+ return self.__class__(header_value=self.header_value)
+
+ return self._add_instance_and_non_accept_encoding_type(
+ instance=self, other=other,
+ )
+
+ def __bool__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept-Encoding`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``True``.
+ """
+ return True
+ __nonzero__ = __bool__ # Python 2
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of :meth:`AcceptEncodingValidHeader.__contains__` is
+ currently being maintained for backward compatibility, but it will
+ change in the future to better conform to the RFC.
+
+ :param offer: (``str``) a content-coding or ``identity`` offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ The behavior of this method does not fully conform to :rfc:`7231`.
+ It does not correctly interpret ``*``::
+
+ >>> 'gzip' in AcceptEncodingValidHeader('gzip;q=0, *')
+ True
+
+ and does not handle the ``identity`` token correctly::
+
+ >>> 'identity' in AcceptEncodingValidHeader('gzip')
+ False
+ """
+ warnings.warn(
+ 'The behavior of AcceptEncodingValidHeader.__contains__ is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ for mask, quality in self._parsed_nonzero:
+ if self._old_match(mask, offer):
+ return True
+
+ def __iter__(self):
+ """
+ Return all the ranges with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the (content-coding/``identity``/``*``) items
+ in the header with non-0 qvalues, in descending order of
+ qvalue. If two items have the same qvalue, they are returned
+ in the order of their positions in the header, from left to
+ right.
+
+ Please note that this is a simple filter for the items in the header
+ with non-0 qvalues, and is not necessarily the same as what the client
+ prefers, e.g. ``'gzip;q=0, *'`` means 'everything but gzip', but
+ ``list(instance)`` would return only ``['*']``.
+ """
+ warnings.warn(
+ 'The behavior of AcceptEncodingLanguageValidHeader.__iter__ is '
+ 'currently maintained for backward compatibility, but will change'
+ ' in the future.',
+ DeprecationWarning,
+ )
+
+ for m,q in sorted(
+ self._parsed_nonzero,
+ key=lambda i: i[1],
+ reverse=True
+ ):
+ yield m
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptEncodingValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_encoding_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
+
+ def __str__(self):
+ r"""
+ Return a tidied up version of the header value.
+
+ e.g. If the ``header_value`` is ``",\t, a ;\t q=0.20 , b ,',"``,
+ ``str(instance)`` returns ``"a;q=0.2, b, '"``.
+ """
+ return ', '.join(
+ _item_qvalue_pair_to_header_element(pair=tuple_)
+ for tuple_ in self.parsed
+ )
+
+ def _add_instance_and_non_accept_encoding_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if not other:
+ return self.__class__(header_value=instance.header_value)
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ if other_header_value == '':
+ # if ``other`` is an object whose type we don't recognise, and
+ # str(other) returns ''
+ return self.__class__(header_value=instance.header_value)
+
+ try:
+ self.parse(value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__(header_value=instance.header_value)
+
+ new_header_value = (
+ (other_header_value + ', ' + instance.header_value)
+ if instance_on_the_right
+ else (instance.header_value + ', ' + other_header_value)
+ )
+ return self.__class__(header_value=new_header_value)
+
+ def _old_match(self, mask, offer):
+ """
+ Return whether content-coding offer matches codings header item.
+
+ .. warning::
+
+ This is maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ This method was WebOb's old criterion for deciding whether a
+ content-coding offer matches a header item (content-coding,
+ ``identity`` or ``*``), used in
+
+ - :meth:`AcceptCharsetValidHeader.__contains__`
+ - :meth:`AcceptCharsetValidHeader.best_match`
+ - :meth:`AcceptCharsetValidHeader.quality`
+
+ It does not conform to :rfc:`RFC 7231, section 5.3.4
+ <7231#section-5.3.4>` in that it does not interpret ``*`` values in the
+ header correctly: ``*`` should only match content-codings not mentioned
+ elsewhere in the header.
+ """
+ return mask == '*' or offer.lower() == mask.lower()
+
+ def acceptable_offers(self, offers):
+ """
+ Return the offers that are acceptable according to the header.
+
+ The offers are returned in descending order of preference, where
+ preference is indicated by the qvalue of the item (content-coding,
+ "identity" or "*") in the header that matches the offer.
+
+ This uses the matching rules described in :rfc:`RFC 7231, section 5.3.4
+ <7231#section-5.3.4>`.
+
+ :param offers: ``iterable`` of ``str``s, where each ``str`` is a
+ content-coding or the string ``identity`` (the token
+ used to represent "no encoding")
+ :return: A list of tuples of the form (content-coding or "identity",
+ qvalue), in descending order of qvalue. Where two offers have
+ the same qvalue, they are returned in the same order as their
+ order in `offers`.
+
+ Use the string ``'identity'`` (without the quotes) in `offers` to
+ indicate an offer with no content-coding. From the RFC: 'If the
+ representation has no content-coding, then it is acceptable by default
+ unless specifically excluded by the Accept-Encoding field stating
+ either "identity;q=0" or "\\*;q=0" without a more specific entry for
+ "identity".' The RFC does not specify the qvalue that should be
+ assigned to the representation/offer with no content-coding; this
+ implementation assigns it a qvalue of 1.0.
+ """
+ lowercased_parsed = [
+ (codings.lower(), qvalue) for (codings, qvalue) in self.parsed
+ ]
+ lowercased_offers = [offer.lower() for offer in offers]
+
+ not_acceptable_codingss = set()
+ acceptable_codingss = dict()
+ asterisk_qvalue = None
+
+ for codings, qvalue in lowercased_parsed:
+ if codings == '*':
+ if asterisk_qvalue is None:
+ asterisk_qvalue = qvalue
+ elif (
+ codings not in acceptable_codingss and codings not in
+ not_acceptable_codingss
+ # if we have not already encountered this codings in the header
+ ):
+ if qvalue == 0.0:
+ not_acceptable_codingss.add(codings)
+ else:
+ acceptable_codingss[codings] = qvalue
+ acceptable_codingss = list(acceptable_codingss.items())
+ # Sort acceptable_codingss by qvalue, descending order
+ acceptable_codingss.sort(key=lambda tuple_: tuple_[1], reverse=True)
+
+ filtered_offers = []
+ for index, offer in enumerate(lowercased_offers):
+ # If offer matches a non-* codings with q=0, it is filtered out
+ if any((
+ (offer == codings) for codings in not_acceptable_codingss
+ )):
+ continue
+
+ matched_codings_qvalue = None
+ for codings, qvalue in acceptable_codingss:
+ if offer == codings:
+ matched_codings_qvalue = qvalue
+ break
+ else:
+ if asterisk_qvalue:
+ matched_codings_qvalue = asterisk_qvalue
+ elif asterisk_qvalue != 0.0 and offer == 'identity':
+ matched_codings_qvalue = 1.0
+ if matched_codings_qvalue is not None: # if there was a match
+ filtered_offers.append((
+ offers[index], matched_codings_qvalue, index
+ ))
+
+ # sort by position in `offers` argument, ascending
+ filtered_offers.sort(key=lambda tuple_: tuple_[2])
+ # When qvalues are tied, position in `offers` is the tiebreaker.
+
+ # sort by qvalue, descending
+ filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True)
+
+ return [(item[0], item[1]) for item in filtered_offers]
+ # (offer, qvalue), dropping the position
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of `offers`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :meth:`AcceptEncodingValidHeader.best_match` uses its own algorithm
+ (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
+ best match. The algorithm has many issues, and does not conform to
+ the RFC.
+
+ Each offer in `offers` is checked against each non-``q=0`` item
+ (content-coding/``identity``/``*``) in the header. If the two are a
+ match according to WebOb's old criterion for a match, the quality value
+ of the match is the qvalue of the item from the header multiplied by
+ the server quality value of the offer (if the server quality value is
+ not supplied, it is 1).
+
+ The offer in the match with the highest quality value is the best
+ match. If there is more than one match with the highest qvalue, the one
+ that shows up first in `offers` is the best match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` *codings*,
+ or a (*codings*, server quality value) ``tuple`` or
+ ``list``, where *codings* is either a content-coding,
+ or the string ``identity`` (which represents *no
+ encoding*). ``str`` and ``tuple``/``list`` elements
+ may be mixed within the iterable.
+
+ :param default_match: (optional, any type) the value to be returned if
+ there is no match
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The offer that is the best match. If there is no match, the
+ value of `default_match` is returned.
+
+ This method does not conform to :rfc:`RFC 7231, section 5.3.4
+ <7231#section-5.3.4>`, in that it does not correctly interpret ``*``::
+
+ >>> AcceptEncodingValidHeader('gzip;q=0, *').best_match(['gzip'])
+ 'gzip'
+
+ and does not handle the ``identity`` token correctly::
+
+ >>> instance = AcceptEncodingValidHeader('gzip')
+ >>> instance.best_match(['identity']) is None
+ True
+ """
+ warnings.warn(
+ 'The behavior of AcceptEncodingValidHeader.best_match is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will be deprecated in the future, as it does not conform to the'
+ ' RFC.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ matched_by = '*/*'
+ for offer in offers:
+ if isinstance(offer, (tuple, list)):
+ offer, server_quality = offer
+ else:
+ server_quality = 1
+ for item in self._parsed_nonzero:
+ mask = item[0]
+ quality = item[1]
+ possible_quality = server_quality * quality
+ if possible_quality < best_quality:
+ continue
+ elif possible_quality == best_quality:
+ # 'text/plain' overrides 'message/*' overrides '*/*'
+ # (if all match w/ the same q=)
+ # [We can see that this was written for the Accept header,
+ # not the Accept-Encoding header.]
+ if matched_by.count('*') <= mask.count('*'):
+ continue
+ if self._old_match(mask, offer):
+ best_quality = possible_quality
+ best_offer = offer
+ matched_by = mask
+ return best_offer
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :param offer: (``str``) A content-coding, or ``identity``.
+ :return: (``float`` or ``None``)
+
+ | The quality value from the header item
+ (content-coding/``identity``/``*``) that matches the
+ `offer`, or ``None`` if there is no match.
+
+ The behavior of this method does not conform to :rfc:`RFC 7231, section
+ 5.3.4<7231#section-5.3.4>`, in that it does not correctly interpret
+ ``*``::
+
+ >>> AcceptEncodingValidHeader('gzip;q=0, *').quality('gzip')
+ 1.0
+
+ and does not handle the ``identity`` token correctly::
+
+ >>> AcceptEncodingValidHeader('gzip').quality('identity') is None
+ True
+ """
+ warnings.warn(
+ 'The behavior of AcceptEncodingValidHeader.quality is currently '
+ 'being maintained for backward compatibility, but it will be '
+ 'deprecated in the future, as it does not conform to the RFC.',
+ DeprecationWarning,
+ )
+ bestq = 0
+ for mask, q in self.parsed:
+ if self._old_match(mask, offer):
+ bestq = max(bestq, q)
+ return bestq or None
+
+
+class _AcceptEncodingInvalidOrNoHeader(AcceptEncoding):
+ """
+ Represent when an ``Accept-Encoding`` header is invalid or not in request.
+
+ This is the base class for the behaviour that
+ :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader`
+ have in common.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``AcceptEncoding`` header has an invalid value. This implementation
+ disregards the header when the header is invalid, so
+ :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader`
+ have much behaviour in common.
+ """
+
+ def __bool__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept-Encoding`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``False``.
+ """
+ return False
+ __nonzero__ = __bool__ # Python 2
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of ``.__contains__`` for the ``Accept-Encoding``
+ classes is currently being maintained for backward compatibility,
+ but it will change in the future to better conform to the RFC.
+
+ :param offer: (``str``) a content-coding or ``identity`` offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ For this class, either there is no ``Accept-Encoding`` header in the
+ request, or the header is invalid, so any content-coding is acceptable,
+ and this always returns ``True``.
+ """
+ warnings.warn(
+ 'The behavior of .__contains__ for the Accept-Encoding classes is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ return True
+
+ def __iter__(self):
+ """
+ Return all the header items with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the (content-coding/``identity``/``*``) items
+ in the header with non-0 qvalues, in descending order of
+ qvalue. If two items have the same qvalue, they are returned
+ in the order of their positions in the header, from left to
+ right.
+
+ When there is no ``Accept-Encoding`` header in the request or the
+ header is invalid, there are no items in the header, so this always
+ returns an empty iterator.
+ """
+ warnings.warn(
+ 'The behavior of AcceptEncodingValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+ return iter(())
+
+ def acceptable_offers(self, offers):
+ """
+ Return the offers that are acceptable according to the header.
+
+ :param offers: ``iterable`` of ``str``s, where each ``str`` is a
+ content-coding or the string ``identity`` (the token
+ used to represent "no encoding")
+ :return: When the header is invalid, or there is no ``Accept-Encoding``
+ header in the request, all `offers` are considered acceptable,
+ so this method returns a list of (content-coding or
+ "identity", qvalue) tuples where each offer in `offers` is
+ paired with the qvalue of 1.0, in the same order as in
+ `offers`.
+ """
+ return [(offer, 1.0) for offer in offers]
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of `offers`.
+
+ This is the ``.best_match()`` method for when the header is invalid or
+ not found in the request, corresponding to
+ :meth:`AcceptEncodingValidHeader.best_match`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptEncodingValidHeader.best_match`).
+
+ When the header is invalid, or there is no `Accept-Encoding` header in
+ the request, all `offers` are considered acceptable, so the best match
+ is the offer in `offers` with the highest server quality value (if the
+ server quality value is not supplied for a media type, it is 1).
+
+ If more than one offer in `offers` have the same highest server quality
+ value, then the one that shows up first in `offers` is the best match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` *codings*,
+ or a (*codings*, server quality value) ``tuple`` or
+ ``list``, where *codings* is either a content-coding,
+ or the string ``identity`` (which represents *no
+ encoding*). ``str`` and ``tuple``/``list`` elements
+ may be mixed within the iterable.
+
+ :param default_match: (optional, any type) the value to be returned if
+ `offers` is empty.
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The offer that has the highest server quality value. If
+ `offers` is empty, the value of `default_match` is returned.
+ """
+ warnings.warn(
+ 'The behavior of .best_match for the Accept-Encoding classes is '
+ 'currently being maintained for backward compatibility, but the '
+ 'method will be deprecated in the future, as its behavior is not '
+ 'specified in (and currently does not conform to) RFC 7231.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ for offer in offers:
+ if isinstance(offer, (list, tuple)):
+ offer, quality = offer
+ else:
+ quality = 1
+ if quality > best_quality:
+ best_offer = offer
+ best_quality = quality
+ return best_offer
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ This is the ``.quality()`` method for when the header is invalid or not
+ found in the request, corresponding to
+ :meth:`AcceptEncodingValidHeader.quality`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptEncodingValidHeader.quality`).
+
+ :param offer: (``str``) A content-coding, or ``identity``.
+ :return: (``float``) ``1.0``.
+
+ When the ``Accept-Encoding`` header is invalid or not in the request,
+ all offers are equally acceptable, so 1.0 is always returned.
+ """
+ warnings.warn(
+ 'The behavior of .quality for the Accept-Encoding classes is '
+ 'currently being maintained for backward compatibility, but the '
+ 'method will be deprecated in the future, as its behavior does '
+ 'not conform to RFC 7231.',
+ DeprecationWarning,
+ )
+ return 1.0
+
+
+class AcceptEncodingNoHeader(_AcceptEncodingInvalidOrNoHeader):
+ """
+ Represent when there is no ``Accept-Encoding`` header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptEncodingNoHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """
+ (``str`` or ``None``) The header value.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._parsed
+
+ def __init__(self):
+ """
+ Create an :class:`AcceptEncodingNoHeader` instance.
+ """
+ self._header_value = None
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__()
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
+ keys, and qvalue ``float``'s as values
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
+ or ``list``
+ * an :class:`AcceptEncodingValidHeader`,
+ :class:`AcceptEncodingNoHeader`, or
+ :class:`AcceptEncodingInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an
+ :class:`AcceptEncodingValidHeader` instance, a new
+ :class:`AcceptEncodingValidHeader` instance with the valid header value
+ is returned.
+
+ If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an
+ invalid header value, or an :class:`AcceptEncodingInvalidHeader`
+ instance, a new :class:`AcceptEncodingNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptEncodingValidHeader):
+ return AcceptEncodingValidHeader(header_value=other.header_value)
+
+ if isinstance(
+ other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
+ ):
+ return self.__class__()
+
+ return self._add_instance_and_non_accept_encoding_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptEncodingNoHeader.__add__`.
+ """
+ return self.__add__(other=other)
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+
+ def __str__(self):
+ """Return the ``str`` ``'<no header in request>'``."""
+ return '<no header in request>'
+
+ def _add_instance_and_non_accept_encoding_type(self, instance, other):
+ if other is None:
+ return self.__class__()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptEncodingValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__()
+
+
+class AcceptEncodingInvalidHeader(_AcceptEncodingInvalidOrNoHeader):
+ """
+ Represent an invalid ``Accept-Encoding`` header.
+
+ An invalid header is one that does not conform to
+ :rfc:`7231#section-5.3.4`.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept-Encoding`` header has an invalid value. This implementation
+ disregards the header, and treats it as if there is no ``Accept-Encoding``
+ header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptEncodingInvalidHeader.__add__`).
+ """
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As the header is invalid and cannot be parsed, this is ``None``.
+ """
+ return self._parsed
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptEncodingInvalidHeader` instance.
+ """
+ self._header_value = header_value
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
+ keys, and qvalue ``float``'s as values
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
+ or ``list``
+ * an :class:`AcceptEncodingValidHeader`,
+ :class:`AcceptEncodingNoHeader`, or
+ :class:`AcceptEncodingInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an
+ :class:`AcceptEncodingValidHeader` instance, then a new
+ :class:`AcceptEncodingValidHeader` instance with the valid header value
+ is returned.
+
+ If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an
+ invalid header value, or an :class:`AcceptEncodingInvalidHeader`
+ instance, a new :class:`AcceptEncodingNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptEncodingValidHeader):
+ return AcceptEncodingValidHeader(header_value=other.header_value)
+
+ if isinstance(
+ other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
+ ):
+ return AcceptEncodingNoHeader()
+
+ return self._add_instance_and_non_accept_encoding_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptEncodingValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_encoding_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+ # We do not display the header_value, as it is untrusted input. The
+ # header_value could always be easily obtained from the .header_value
+ # property.
+
+ def __str__(self):
+ """Return the ``str`` ``'<invalid header value>'``."""
+ return '<invalid header value>'
+
+ def _add_instance_and_non_accept_encoding_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if other is None:
+ return AcceptEncodingNoHeader()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptEncodingValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return AcceptEncodingNoHeader()
+
+
+def create_accept_encoding_header(header_value):
+ """
+ Create an object representing the ``Accept-Encoding`` header in a request.
+
+ :param header_value: (``str``) header value
+ :return: If `header_value` is ``None``, an :class:`AcceptEncodingNoHeader`
+ instance.
+
+ | If `header_value` is a valid ``Accept-Encoding`` header, an
+ :class:`AcceptEncodingValidHeader` instance.
+
+ | If `header_value` is an invalid ``Accept-Encoding`` header, an
+ :class:`AcceptEncodingInvalidHeader` instance.
+ """
+ if header_value is None:
+ return AcceptEncodingNoHeader()
+ if isinstance(header_value, AcceptEncoding):
+ return header_value.copy()
+ try:
+ return AcceptEncodingValidHeader(header_value=header_value)
+ except ValueError:
+ return AcceptEncodingInvalidHeader(header_value=header_value)
+
+
+def accept_encoding_property():
+ doc = """
+ Property representing the ``Accept-Encoding`` header.
+
+ (:rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`)
+
+ The header value in the request environ is parsed and a new object
+ representing the header is created every time we *get* the value of the
+ property. (*set* and *del* change the header value in the request
+ environ, and do not involve parsing.)
+ """
+
+ ENVIRON_KEY = 'HTTP_ACCEPT_ENCODING'
+
+ def fget(request):
+ """Get an object representing the header in the request."""
+ return create_accept_encoding_header(
+ header_value=request.environ.get(ENVIRON_KEY)
+ )
+
+ def fset(request, value):
+ """
+ Set the corresponding key in the request environ.
+
+ `value` can be:
+
+ * ``None``
+ * a ``str`` header value
+ * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
+ keys, and qvalue ``float``'s as values
+ * a ``tuple`` or ``list``, where each item is either a header element
+ ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
+ or ``list``
+ * an :class:`AcceptEncodingValidHeader`,
+ :class:`AcceptEncodingNoHeader`, or
+ :class:`AcceptEncodingInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+ """
+ if value is None or isinstance(value, AcceptEncodingNoHeader):
+ fdel(request=request)
+ else:
+ if isinstance(
+ value, (AcceptEncodingValidHeader, AcceptEncodingInvalidHeader)
+ ):
+ header_value = value.header_value
+ else:
+ header_value = AcceptEncoding._python_value_to_header_str(
+ value=value,
+ )
+ request.environ[ENVIRON_KEY] = header_value
+
+ def fdel(request):
+ """Delete the corresponding key from the request environ."""
+ try:
+ del request.environ[ENVIRON_KEY]
+ except KeyError:
+ pass
+
+ return property(fget, fset, fdel, textwrap.dedent(doc))
+
+
+class AcceptLanguage(object):
+ """
+ Represent an ``Accept-Language`` header.
+
+ Base class for :class:`AcceptLanguageValidHeader`,
+ :class:`AcceptLanguageNoHeader`, and :class:`AcceptLanguageInvalidHeader`.
+ """
+
+ # RFC 7231 Section 5.3.5 "Accept-Language":
+ # Accept-Language = 1#( language-range [ weight ] )
+ # language-range =
+ # <language-range, see [RFC4647], Section 2.1>
+ # RFC 4647 Section 2.1 "Basic Language Range":
+ # language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
+ # alphanum = ALPHA / DIGIT
+ lang_range_re = (
+ r'\*|'
+ '(?:'
+ '[A-Za-z]{1,8}'
+ '(?:-[A-Za-z0-9]{1,8})*'
+ ')'
+ )
+ lang_range_n_weight_re = _item_n_weight_re(item_re=lang_range_re)
+ lang_range_n_weight_compiled_re = re.compile(lang_range_n_weight_re)
+ accept_language_compiled_re = _list_1_or_more__compiled_re(
+ element_re=lang_range_n_weight_re,
+ )
+
+ @classmethod
+ def _python_value_to_header_str(cls, value):
+ if isinstance(value, str):
+ header_str = value
+ else:
+ if hasattr(value, 'items'):
+ value = sorted(
+ value.items(),
+ key=lambda item: item[1],
+ reverse=True,
+ )
+ if isinstance(value, (tuple, list)):
+ result = []
+ for element in value:
+ if isinstance(element, (tuple, list)):
+ element = _item_qvalue_pair_to_header_element(
+ pair=element
+ )
+ result.append(element)
+ header_str = ', '.join(result)
+ else:
+ header_str = str(value)
+ return header_str
+
+ @classmethod
+ def parse(cls, value):
+ """
+ Parse an ``Accept-Language`` header.
+
+ :param value: (``str``) header value
+ :return: If `value` is a valid ``Accept-Language`` header, returns an
+ iterator of (language range, quality value) tuples, as parsed
+ from the header from left to right.
+ :raises ValueError: if `value` is an invalid header
+ """
+ # Check if header is valid
+ # Using Python stdlib's `re` module, there is currently no way to check
+ # the match *and* get all the groups using the same regex, so we have
+ # to use one regex to check the match, and another to get the groups.
+ if cls.accept_language_compiled_re.match(value) is None:
+ raise ValueError('Invalid value for an Accept-Language header.')
+ def generator(value):
+ for match in (
+ cls.lang_range_n_weight_compiled_re.finditer(value)
+ ):
+ lang_range = match.group(1)
+ qvalue = match.group(2)
+ qvalue = float(qvalue) if qvalue else 1.0
+ yield (lang_range, qvalue)
+ return generator(value=value)
+
+
+class AcceptLanguageValidHeader(AcceptLanguage):
+ """
+ Represent a valid ``Accept-Language`` header.
+
+ A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.5
+ <7231#section-5.3.5>`.
+
+ We take the reference from the ``language-range`` syntax rule in :rfc:`RFC
+ 7231, section 5.3.5 <7231#section-5.3.5>` to :rfc:`RFC 4647, section 2.1
+ <4647#section-2.1>` to mean that only basic language ranges (and not
+ extended language ranges) are expected in the ``Accept-Language`` header.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptLanguageValidHeader.__add__`).
+ """
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptLanguageValidHeader` instance.
+
+ :param header_value: (``str``) header value.
+ :raises ValueError: if `header_value` is an invalid value for an
+ ``Accept-Language`` header.
+ """
+ self._header_value = header_value
+ self._parsed = list(self.parse(header_value))
+ self._parsed_nonzero = [item for item in self.parsed if item[1]]
+ # item[1] is the qvalue
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ A list of (language range, quality value) tuples.
+ """
+ return self._parsed
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str``
+ * a ``dict``, with language ranges as keys and qvalues as values
+ * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
+ or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
+ be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptLanguageValidHeader`,
+ :class:`AcceptLanguageNoHeader`, or
+ :class:`AcceptLanguageInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or another
+ :class:`AcceptLanguageValidHeader` instance, the two header values are
+ joined with ``', '``, and a new :class:`AcceptLanguageValidHeader`
+ instance with the new header value is returned.
+
+ If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
+ invalid header value, or an :class:`AcceptLanguageInvalidHeader`
+ instance, a new :class:`AcceptLanguageValidHeader` instance with the
+ same header value as ``self`` is returned.
+ """
+ if isinstance(other, AcceptLanguageValidHeader):
+ return create_accept_language_header(
+ header_value=self.header_value + ', ' + other.header_value,
+ )
+
+ if isinstance(
+ other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
+ ):
+ return self.__class__(header_value=self.header_value)
+
+ return self._add_instance_and_non_accept_language_type(
+ instance=self, other=other,
+ )
+
+ def __nonzero__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept-Language`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``True``.
+ """
+ return True
+ __bool__ = __nonzero__ # Python 3
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of :meth:`AcceptLanguageValidHeader.__contains__` is
+ currently being maintained for backward compatibility, but it will
+ change in the future to better conform to the RFC.
+
+ What is 'acceptable' depends on the needs of your application.
+ :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests three
+ matching schemes from :rfc:`RFC 4647 <4647>`, two of which WebOb
+ supports with :meth:`AcceptLanguageValidHeader.basic_filtering` and
+ :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to
+ mean that Extended Filtering cannot apply for the
+ ``Accept-Language`` header, as the header only accepts basic
+ language ranges.) If these are not suitable for the needs of your
+ application, you may need to write your own matching using
+ :attr:`AcceptLanguageValidHeader.parsed`.
+
+ :param offer: (``str``) language tag offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ This uses the old criterion of a match in
+ :meth:`AcceptLanguageValidHeader._old_match`, which does not conform to
+ :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` or any of the
+ matching schemes suggested there. It also does not properly take into
+ account ranges with ``q=0`` in the header::
+
+ >>> 'en-gb' in AcceptLanguageValidHeader('en, en-gb;q=0')
+ True
+ >>> 'en' in AcceptLanguageValidHeader('en;q=0, *')
+ True
+
+ (See the docstring for :meth:`AcceptLanguageValidHeader._old_match` for
+ other problems with the old criterion for a match.)
+ """
+ warnings.warn(
+ 'The behavior of AcceptLanguageValidHeader.__contains__ is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ for mask, quality in self._parsed_nonzero:
+ if self._old_match(mask, offer):
+ return True
+ return False
+
+ def __iter__(self):
+ """
+ Return all the ranges with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the language ranges in the header with non-0
+ qvalues, in descending order of qvalue. If two ranges have the
+ same qvalue, they are returned in the order of their positions
+ in the header, from left to right.
+
+ Please note that this is a simple filter for the ranges in the header
+ with non-0 qvalues, and is not necessarily the same as what the client
+ prefers, e.g. ``'en-gb;q=0, *'`` means 'everything but British
+ English', but ``list(instance)`` would return only ``['*']``.
+ """
+ warnings.warn(
+ 'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+
+ for m, q in sorted(
+ self._parsed_nonzero,
+ key=lambda i: i[1],
+ reverse=True
+ ):
+ yield m
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptLanguageValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_language_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{} ({!r})>'.format(self.__class__.__name__, str(self))
+
+ def __str__(self):
+ r"""
+ Return a tidied up version of the header value.
+
+ e.g. If the ``header_value`` is ``', \t,de;q=0.000 \t, es;q=1.000, zh,
+ jp;q=0.210 ,'``, ``str(instance)`` returns ``'de;q=0, es, zh,
+ jp;q=0.21'``.
+ """
+ return ', '.join(
+ _item_qvalue_pair_to_header_element(pair=tuple_)
+ for tuple_ in self.parsed
+ )
+
+ def _add_instance_and_non_accept_language_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if not other:
+ return self.__class__(header_value=instance.header_value)
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ self.parse(value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__(header_value=instance.header_value)
+
+ new_header_value = (
+ (other_header_value + ', ' + instance.header_value)
+ if instance_on_the_right
+ else (instance.header_value + ', ' + other_header_value)
+ )
+ return self.__class__(header_value=new_header_value)
+
+ def _old_match(self, mask, item):
+ """
+ Return whether a language tag matches a language range.
+
+ .. warning::
+
+ This is maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ This method was WebOb's old criterion for deciding whether a language
+ tag matches a language range, used in
+
+ - :meth:`AcceptLanguageValidHeader.__contains__`
+ - :meth:`AcceptLanguageValidHeader.best_match`
+ - :meth:`AcceptLanguageValidHeader.quality`
+
+ It does not conform to :rfc:`RFC 7231, section 5.3.5
+ <7231#section-5.3.5>`, or any of the matching schemes suggested there.
+
+ :param mask: (``str``)
+
+ | language range
+
+ :param item: (``str``)
+
+ | language tag. Subtags in language tags are separated by
+ ``-`` (hyphen). If there are underscores (``_``) in this
+ argument, they will be converted to hyphens before
+ checking the match.
+
+ :return: (``bool``) whether the tag in `item` matches the range in
+ `mask`.
+
+ `mask` and `item` are a match if:
+
+ - ``mask == *``.
+ - ``mask == item``.
+ - If the first subtag of `item` equals `mask`, or if the first subtag
+ of `mask` equals `item`.
+ This means that::
+
+ >>> instance._old_match(mask='en-gb', item='en')
+ True
+ >>> instance._old_match(mask='en', item='en-gb')
+ True
+
+ Which is different from any of the matching schemes suggested in
+ :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, in that none of
+ those schemes match both more *and* less specific tags.
+
+ However, this method appears to be only designed for language tags
+ and ranges with at most two subtags. So with an `item`/language tag
+ with more than two subtags like ``zh-Hans-CN``::
+
+ >>> instance._old_match(mask='zh', item='zh-Hans-CN')
+ True
+ >>> instance._old_match(mask='zh-Hans', item='zh-Hans-CN')
+ False
+
+ From commit history, this does not appear to have been from a
+ decision to match only the first subtag, but rather because only
+ language ranges and tags with at most two subtags were expected.
+ """
+ item = item.replace('_', '-').lower()
+ mask = mask.lower()
+ return (mask == '*'
+ or item == mask
+ or item.split('-')[0] == mask
+ or item == mask.split('-')[0]
+ )
+
+ def basic_filtering(self, language_tags):
+ """
+ Return the tags that match the header, using Basic Filtering.
+
+ This is an implementation of the Basic Filtering matching scheme,
+ suggested as a matching scheme for the ``Accept-Language`` header in
+ :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and defined in
+ :rfc:`RFC 4647, section 3.3.1 <4647#section-3.3.1>`. It filters the
+ tags in the `language_tags` argument and returns the ones that match
+ the header according to the matching scheme.
+
+ :param language_tags: (``iterable``) language tags
+ :return: A list of tuples of the form (language tag, qvalue), in
+ descending order of qvalue. If two or more tags have the same
+ qvalue, they are returned in the same order as that in the
+ header of the ranges they matched. If the matched range is the
+ same for two or more tags (i.e. their matched ranges have the
+ same qvalue and the same position in the header), then they
+ are returned in the same order as that in the `language_tags`
+ argument. If `language_tags` is unordered, e.g. if it is a set
+ or a dict, then that order may not be reliable.
+
+ For each tag in `language_tags`:
+
+ 1. If the tag matches a non-``*`` language range in the header with
+ ``q=0`` (meaning "not acceptable", see :rfc:`RFC 7231, section 5.3.1
+ <7231#section-5.3.1>`), the tag is filtered out.
+ 2. The non-``*`` language ranges in the header that do not have ``q=0``
+ are considered in descending order of qvalue; where two or more
+ language ranges have the same qvalue, they are considered in the
+ order in which they appear in the header.
+ 3. A language range 'matches a particular language tag if, in a
+ case-insensitive comparison, it exactly equals the tag, or if it
+ exactly equals a prefix of the tag such that the first character
+ following the prefix is "-".' (:rfc:`RFC 4647, section 3.3.1
+ <4647#section-3.3.1>`)
+ 4. If the tag does not match any of the non-``*`` language ranges, and
+ there is a ``*`` language range in the header, then if the ``*``
+ language range has ``q=0``, the language tag is filtered out,
+ otherwise the tag is considered a match.
+
+ (If a range (``*`` or non-``*``) appears in the header more than once
+ -- this would not make sense, but is nonetheless a valid header
+ according to the RFC -- the first in the header is used for matching,
+ and the others are ignored.)
+ """
+ # The Basic Filtering matching scheme as applied to the Accept-Language
+ # header is very under-specified by RFCs 7231 and 4647. This
+ # implementation combines the description of the matching scheme in RFC
+ # 4647 and the rules of the Accept-Language header in RFC 7231 to
+ # arrive at an algorithm for Basic Filtering as applied to the
+ # Accept-Language header.
+
+ lowercased_parsed = [
+ (range_.lower(), qvalue) for (range_, qvalue) in self.parsed
+ ]
+ lowercased_tags = [tag.lower() for tag in language_tags]
+
+ not_acceptable_ranges = set()
+ acceptable_ranges = dict()
+ asterisk_qvalue = None
+
+ for position_in_header, (range_, qvalue) in enumerate(
+ lowercased_parsed
+ ):
+ if range_ == '*':
+ if asterisk_qvalue is None:
+ asterisk_qvalue = qvalue
+ asterisk_position = position_in_header
+ elif (
+ range_ not in acceptable_ranges and range_ not in
+ not_acceptable_ranges
+ # if we have not already encountered this range in the header
+ ):
+ if qvalue == 0.0:
+ not_acceptable_ranges.add(range_)
+ else:
+ acceptable_ranges[range_] = (qvalue, position_in_header)
+ acceptable_ranges = [
+ (range_, qvalue, position_in_header)
+ for range_, (qvalue, position_in_header)
+ in acceptable_ranges.items()
+ ]
+ # Sort acceptable_ranges by position_in_header, ascending order
+ acceptable_ranges.sort(key=lambda tuple_: tuple_[2])
+ # Sort acceptable_ranges by qvalue, descending order
+ acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True)
+ # Sort guaranteed to be stable with Python >= 2.2, so position in
+ # header is tiebreaker when two ranges have the same qvalue
+
+ def match(tag, range_):
+ # RFC 4647, section 2.1: 'A language range matches a particular
+ # language tag if, in a case-insensitive comparison, it exactly
+ # equals the tag, or if it exactly equals a prefix of the tag such
+ # that the first character following the prefix is "-".'
+ return (tag == range_) or tag.startswith(range_ + '-')
+ # We can assume here that the language tags are valid tags, so we
+ # do not have to worry about them being malformed and ending with
+ # '-'.
+
+ filtered_tags = []
+ for index, tag in enumerate(lowercased_tags):
+ # If tag matches a non-* range with q=0, it is filtered out
+ if any((
+ match(tag=tag, range_=range_)
+ for range_ in not_acceptable_ranges
+ )):
+ continue
+
+ matched_range_qvalue = None
+ for range_, qvalue, position_in_header in acceptable_ranges:
+ # acceptable_ranges is in descending order of qvalue, and tied
+ # ranges are in ascending order of position_in_header, so the
+ # first range_ that matches the tag is the best match
+ if match(tag=tag, range_=range_):
+ matched_range_qvalue = qvalue
+ matched_range_position = position_in_header
+ break
+ else:
+ if asterisk_qvalue:
+ # From RFC 4647, section 3.3.1: '...HTTP/1.1 [RFC2616]
+ # specifies that the range "*" matches only languages not
+ # matched by any other range within an "Accept-Language"
+ # header.' (Though RFC 2616 is obsolete, and there is no
+ # mention of the meaning of "*" in RFC 7231, as the
+ # ``language-range`` syntax rule in RFC 7231 section 5.3.1
+ # directs us to RFC 4647, we can only assume that the
+ # meaning of "*" in the Accept-Language header remains the
+ # same).
+ matched_range_qvalue = asterisk_qvalue
+ matched_range_position = asterisk_position
+ if matched_range_qvalue is not None: # if there was a match
+ filtered_tags.append((
+ language_tags[index], matched_range_qvalue,
+ matched_range_position
+ ))
+
+ # sort by matched_range_position, ascending
+ filtered_tags.sort(key=lambda tuple_: tuple_[2])
+ # When qvalues are tied, matched range position in the header is the
+ # tiebreaker.
+
+ # sort by qvalue, descending
+ filtered_tags.sort(key=lambda tuple_: tuple_[1], reverse=True)
+
+ return [(item[0], item[1]) for item in filtered_tags]
+ # (tag, qvalue), dropping the matched_range_position
+
+ # We return a list of tuples with qvalues, instead of just a set or
+ # a list of language tags, because
+ # RFC 4647 section 3.3: "If the language priority list contains more
+ # than one range, the content returned is typically ordered in
+ # descending level of preference, but it MAY be unordered, according to
+ # the needs of the application or protocol."
+ # We return the filtered tags in order of preference, each paired with
+ # the qvalue of the range that was their best match, as the ordering
+ # and the qvalues may well be needed in some applications, and a simple
+ # set or list of language tags can always be easily obtained from the
+ # returned list if the qvalues are not required. One use for qvalues,
+ # for example, would be to indicate that two tags are equally preferred
+ # (same qvalue), which we would not be able to do easily with a set or
+ # a list without e.g. making a member of the set or list a sequence.
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of language tag `offers`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :meth:`AcceptLanguageValidHeader.best_match` uses its own algorithm
+ (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
+ best match. The algorithm has many issues, and does not conform to
+ :rfc:`RFC 7231 <7231>`.
+
+ :meth:`AcceptLanguageValidHeader.lookup` is a possible alternative
+ for finding a best match -- it conforms to, and is suggested as a
+ matching scheme for the ``Accept-Language`` header in, :rfc:`RFC
+ 7231, section 5.3.5 <7231#section-5.3.5>` -- but please be aware
+ that there are differences in how it determines what is a best
+ match. If that is not suitable for the needs of your application,
+ you may need to write your own matching using
+ :attr:`AcceptLanguageValidHeader.parsed`.
+
+ Each language tag in `offers` is checked against each non-0 range in
+ the header. If the two are a match according to WebOb's old criterion
+ for a match, the quality value of the match is the qvalue of the
+ language range from the header multiplied by the server quality value
+ of the offer (if the server quality value is not supplied, it is 1).
+
+ The offer in the match with the highest quality value is the best
+ match. If there is more than one match with the highest qvalue, the
+ match where the language range has a lower number of '*'s is the best
+ match. If the two have the same number of '*'s, the one that shows up
+ first in `offers` is the best match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` language
+ tag, or a (language tag, server quality value)
+ ``tuple`` or ``list``. (The two may be mixed in the
+ iterable.)
+
+ :param default_match: (optional, any type) the value to be returned if
+ there is no match
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The language tag that is the best match. If there is no
+ match, the value of `default_match` is returned.
+
+
+ **Issues**:
+
+ - Incorrect tiebreaking when quality values of two matches are the same
+ (https://github.com/Pylons/webob/issues/256)::
+
+ >>> header = AcceptLanguageValidHeader(
+ ... header_value='en-gb;q=1, en;q=0.8'
+ ... )
+ >>> header.best_match(offers=['en', 'en-GB'])
+ 'en'
+ >>> header.best_match(offers=['en-GB', 'en'])
+ 'en-GB'
+
+ >>> header = AcceptLanguageValidHeader(header_value='en-gb, en')
+ >>> header.best_match(offers=['en', 'en-gb'])
+ 'en'
+ >>> header.best_match(offers=['en-gb', 'en'])
+ 'en-gb'
+
+ - Incorrect handling of ``q=0``::
+
+ >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *')
+ >>> header.best_match(offers=['en'])
+ 'en'
+
+ >>> header = AcceptLanguageValidHeader(header_value='fr, en;q=0')
+ >>> header.best_match(offers=['en-gb'], default_match='en')
+ 'en'
+
+ - Matching only takes into account the first subtag when matching a
+ range with more specific or less specific tags::
+
+ >>> header = AcceptLanguageValidHeader(header_value='zh')
+ >>> header.best_match(offers=['zh-Hans-CN'])
+ 'zh-Hans-CN'
+ >>> header = AcceptLanguageValidHeader(header_value='zh-Hans')
+ >>> header.best_match(offers=['zh-Hans-CN'])
+ >>> header.best_match(offers=['zh-Hans-CN']) is None
+ True
+
+ >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN')
+ >>> header.best_match(offers=['zh'])
+ 'zh'
+ >>> header.best_match(offers=['zh-Hans'])
+ >>> header.best_match(offers=['zh-Hans']) is None
+ True
+
+ """
+ warnings.warn(
+ 'The behavior of AcceptLanguageValidHeader.best_match is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will be deprecated in the future as it does not conform to the '
+ 'RFC.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ matched_by = '*/*'
+ # [We can see that this was written for the ``Accept`` header and not
+ # the ``Accept-Language`` header, as there are no '/'s in a valid
+ # ``Accept-Language`` header.]
+ for offer in offers:
+ if isinstance(offer, (tuple, list)):
+ offer, server_quality = offer
+ else:
+ server_quality = 1
+ for mask, quality in self._parsed_nonzero:
+ possible_quality = server_quality * quality
+ if possible_quality < best_quality:
+ continue
+ elif possible_quality == best_quality:
+ # 'text/plain' overrides 'message/*' overrides '*/*'
+ # (if all match w/ the same q=)
+ if matched_by.count('*') <= mask.count('*'):
+ continue
+ # [This tiebreaking was written for the `Accept` header. A
+ # basic language range in a valid ``Accept-Language``
+ # header can only be either '*' or a range with no '*' in
+ # it. This happens to work here, but is not sufficient as a
+ # tiebreaker.
+ #
+ # A best match here, given this algorithm uses
+ # self._old_match() which matches both more *and* less
+ # specific tags, should be the match where the absolute
+ # value of the difference between the subtag counts of
+ # `mask` and `offer` is the lowest.]
+ if self._old_match(mask, offer):
+ best_quality = possible_quality
+ best_offer = offer
+ matched_by = mask
+ return best_offer
+
+ def lookup(
+ self, language_tags, default_range=None, default_tag=None,
+ default=None,
+ ):
+ """
+ Return the language tag that best matches the header, using Lookup.
+
+ This is an implementation of the Lookup matching scheme,
+ suggested as a matching scheme for the ``Accept-Language`` header in
+ :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and described in
+ :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`.
+
+ Each language range in the header is considered in turn, by descending
+ order of qvalue; where qvalues are tied, ranges are considered from
+ left to right.
+
+ Each language range in the header represents the most specific tag that
+ is an acceptable match: Lookup progressively truncates subtags from the
+ end of the range until a matching language tag is found. An example is
+ given in :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, under
+ "Example of a Lookup Fallback Pattern":
+
+ ::
+
+ Range to match: zh-Hant-CN-x-private1-private2
+ 1. zh-Hant-CN-x-private1-private2
+ 2. zh-Hant-CN-x-private1
+ 3. zh-Hant-CN
+ 4. zh-Hant
+ 5. zh
+ 6. (default)
+
+ :param language_tags: (``iterable``) language tags
+
+ :param default_range: (optional, ``None`` or ``str``)
+
+ | If Lookup finds no match using the ranges in
+ the header, and this argument is not None,
+ Lookup will next attempt to match the range in
+ this argument, using the same subtag
+ truncation.
+
+ | `default_range` cannot be '*', as '*' is
+ skipped in Lookup. See :ref:`note
+ <acceptparse-lookup-asterisk-note>`.
+
+ | This parameter corresponds to the functionality
+ described in :rfc:`RFC 4647, section 3.4.1
+ <4647#section-3.4.1>`, in the paragraph
+ starting with "One common way to provide for a
+ default is to allow a specific language range
+ to be set as the default..."
+
+ :param default_tag: (optional, ``None`` or ``str``)
+
+ | At least one of `default_tag` or `default` must
+ be supplied as an argument to the method, to
+ define the defaulting behaviour.
+
+ | If Lookup finds no match using the ranges in the
+ header and `default_range`, this argument is not
+ ``None``, and it does not match any range in the
+ header with ``q=0`` (exactly, with no subtag
+ truncation), then this value is returned.
+
+ | This parameter corresponds to "return a
+ particular language tag designated for the
+ operation", one of the examples of "defaulting
+ behavior" described in :rfc:`RFC 4647, section
+ 3.4.1 <4647#section-3.4.1>`.
+
+ :param default: (optional, ``None`` or any type, including a callable)
+
+ | At least one of `default_tag` or `default` must be
+ supplied as an argument to the method, to define the
+ defaulting behaviour.
+
+ | If Lookup finds no match using the ranges in the
+ header and `default_range`, and `default_tag` is
+ ``None`` or not acceptable because it matches a
+ ``q=0`` range in the header, then Lookup will next
+ examine the `default` argument.
+
+ | If `default` is a callable, it will be called, and
+ the callable's return value will be returned.
+
+ | If `default` is not a callable, the value itself will
+ be returned.
+
+ | The difference between supplying a ``str`` to
+ `default_tag` and `default` is that `default_tag` is
+ checked against ``q=0`` ranges in the header to see
+ if it matches one of the ranges specified as not
+ acceptable, whereas a ``str`` for the `default`
+ argument is simply returned.
+
+ | This parameter corresponds to the "defaulting
+ behavior" described in :rfc:`RFC 4647, section 3.4.1
+ <4647#section-3.4.1>`
+
+ :return: (``str``, ``None``, or any type)
+
+ | The best match according to the Lookup matching scheme, or a
+ return value from one of the default arguments.
+
+ **Notes**:
+
+ .. _acceptparse-lookup-asterisk-note:
+
+ - Lookup's behaviour with '*' language ranges in the header may be
+ surprising. From :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`:
+
+ In the lookup scheme, this range does not convey enough
+ information by itself to determine which language tag is most
+ appropriate, since it matches everything. If the language range
+ "*" is followed by other language ranges, it is skipped. If the
+ language range "*" is the only one in the language priority list
+ or if no other language range follows, the default value is
+ computed and returned.
+
+ So
+
+ ::
+
+ >>> header = AcceptLanguageValidHeader('de, zh, *')
+ >>> header.lookup(language_tags=['ja', 'en'], default='default')
+ 'default'
+
+ - Any tags in `language_tags` and `default_tag` and any tag matched
+ during the subtag truncation search for `default_range`, that are an
+ exact match for a non-``*`` range with ``q=0`` in the header, are
+ considered not acceptable and ruled out.
+
+ - If there is a ``*;q=0`` in the header, then `default_range` and
+ `default_tag` have no effect, as ``*;q=0`` means that all languages
+ not already matched by other ranges within the header are
+ unacceptable.
+ """
+ if default_tag is None and default is None:
+ raise TypeError(
+ '`default_tag` and `default` arguments cannot both be None.'
+ )
+
+ # We need separate `default_tag` and `default` arguments because if we
+ # only had the `default` argument, there would be no way to tell
+ # whether a str is a language tag (in which case we have to check
+ # whether it has been specified as not acceptable with a q=0 range in
+ # the header) or not (in which case we can just return the value).
+
+ if default_range == '*':
+ raise ValueError('default_range cannot be *.')
+
+ parsed = list(self.parsed)
+
+ tags = language_tags
+ not_acceptable_ranges = []
+ acceptable_ranges = []
+
+ asterisk_non0_found = False
+ # Whether there is a '*' range in the header with q={not 0}
+
+ asterisk_q0_found = False
+ # Whether there is a '*' range in the header with q=0
+ # While '*' is skipped in Lookup because it "does not convey enough
+ # information by itself to determine which language tag is most
+ # appropriate" (RFC 4647, section 3.4), '*;q=0' is clear in meaning:
+ # languages not matched by any other range within the header are not
+ # acceptable.
+
+ for range_, qvalue in parsed:
+ if qvalue == 0.0:
+ if range_ == '*': # *;q=0
+ asterisk_q0_found = True
+ else: # {non-* range};q=0
+ not_acceptable_ranges.append(range_.lower())
+ elif not asterisk_q0_found and range_ == '*': # *;q={not 0}
+ asterisk_non0_found = True
+ # if asterisk_q0_found, then it does not matter whether
+ # asterisk_non0_found
+ else: # {non-* range};q={not 0}
+ acceptable_ranges.append((range_, qvalue))
+ # Sort acceptable_ranges by qvalue, descending order
+ acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True)
+ # Sort guaranteed to be stable with Python >= 2.2, so position in
+ # header is tiebreaker when two ranges have the same qvalue
+
+ acceptable_ranges = [tuple_[0] for tuple_ in acceptable_ranges]
+ lowered_tags = [tag.lower() for tag in tags]
+
+ def best_match(range_):
+ subtags = range_.split('-')
+ while True:
+ for index, tag in enumerate(lowered_tags):
+ if tag in not_acceptable_ranges:
+ continue
+ # We think a non-'*' range with q=0 represents only
+ # itself as a tag, and there should be no falling back
+ # with subtag truncation. For example, with
+ # 'en-gb;q=0', it should not mean 'en;q=0': the client
+ # is unlikely to expect that specifying 'en-gb' as not
+ # acceptable would mean that 'en' is also not
+ # acceptable. There is no guidance on this at all in
+ # the RFCs, so it is left to us to decide how it should
+ # work.
+
+ if tag == range_:
+ return tags[index] # return the pre-lowered tag
+
+ try:
+ subtag_before_this = subtags[-2]
+ except IndexError: # len(subtags) == 1
+ break
+ # len(subtags) >= 2
+ if len(subtag_before_this) == 1 and (
+ subtag_before_this.isdigit() or
+ subtag_before_this.isalpha()
+ ): # if subtag_before_this is a single-letter or -digit subtag
+ subtags.pop(-1) # pop twice instead of once
+ subtags.pop(-1)
+ range_ = '-'.join(subtags)
+
+ for range_ in acceptable_ranges:
+ match = best_match(range_=range_.lower())
+ if match is not None:
+ return match
+
+ if not asterisk_q0_found:
+ if default_range is not None:
+ lowered_default_range = default_range.lower()
+ match = best_match(range_=lowered_default_range)
+ if match is not None:
+ return match
+
+ if default_tag is not None:
+ lowered_default_tag = default_tag.lower()
+ if lowered_default_tag not in not_acceptable_ranges:
+ return default_tag
+
+ try:
+ return default()
+ except TypeError: # default is not a callable
+ return default
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future.
+
+ :meth:`AcceptLanguageValidHeader.quality` uses its own algorithm
+ (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
+ best match. The algorithm has many issues, and does not conform to
+ :rfc:`RFC 7231 <7231>`.
+
+ What should be considered a match depends on the needs of your
+ application (for example, should a language range in the header
+ match a more specific language tag offer, or a less specific tag
+ offer?) :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests
+ three matching schemes from :rfc:`RFC 4647 <4647>`, two of which
+ WebOb supports with
+ :meth:`AcceptLanguageValidHeader.basic_filtering` and
+ :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to
+ mean that Extended Filtering cannot apply for the
+ ``Accept-Language`` header, as the header only accepts basic
+ language ranges.) :meth:`AcceptLanguageValidHeader.basic_filtering`
+ returns quality values with the matched language tags.
+ :meth:`AcceptLanguageValidHeader.lookup` returns a language tag
+ without the quality value, but the quality value is less likely to
+ be useful when we are looking for a best match.
+
+ If these are not suitable or sufficient for the needs of your
+ application, you may need to write your own matching using
+ :attr:`AcceptLanguageValidHeader.parsed`.
+
+ :param offer: (``str``) language tag offer
+ :return: (``float`` or ``None``)
+
+ | The highest quality value from the language range(s) that
+ match the `offer`, or ``None`` if there is no match.
+
+
+ **Issues**:
+
+ - Incorrect handling of ``q=0`` and ``*``::
+
+ >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *')
+ >>> header.quality(offer='en')
+ 1.0
+
+ - Matching only takes into account the first subtag when matching a
+ range with more specific or less specific tags::
+
+ >>> header = AcceptLanguageValidHeader(header_value='zh')
+ >>> header.quality(offer='zh-Hans-CN')
+ 1.0
+ >>> header = AcceptLanguageValidHeader(header_value='zh-Hans')
+ >>> header.quality(offer='zh-Hans-CN')
+ >>> header.quality(offer='zh-Hans-CN') is None
+ True
+
+ >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN')
+ >>> header.quality(offer='zh')
+ 1.0
+ >>> header.quality(offer='zh-Hans')
+ >>> header.quality(offer='zh-Hans') is None
+ True
+
+ """
+ warnings.warn(
+ 'The behavior of AcceptLanguageValidHeader.quality is'
+ 'currently being maintained for backward compatibility, but it '
+ 'will be deprecated in the future as it does not conform to the '
+ 'RFC.',
+ DeprecationWarning,
+ )
+ bestq = 0
+ for mask, q in self.parsed:
+ if self._old_match(mask, offer):
+ bestq = max(bestq, q)
+ return bestq or None
+
+
+class _AcceptLanguageInvalidOrNoHeader(AcceptLanguage):
+ """
+ Represent when an ``Accept-Language`` header is invalid or not in request.
+
+ This is the base class for the behaviour that
+ :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader`
+ have in common.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept-Language`` header has an invalid value. This implementation
+ disregards the header when the header is invalid, so
+ :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader`
+ have much behaviour in common.
+ """
+
+ def __nonzero__(self):
+ """
+ Return whether ``self`` represents a valid ``Accept-Language`` header.
+
+ Return ``True`` if ``self`` represents a valid header, and ``False`` if
+ it represents an invalid header, or the header not being in the
+ request.
+
+ For this class, it always returns ``False``.
+ """
+ return False
+ __bool__ = __nonzero__ # Python 3
+
+ def __contains__(self, offer):
+ """
+ Return ``bool`` indicating whether `offer` is acceptable.
+
+ .. warning::
+
+ The behavior of ``.__contains__`` for the ``AcceptLanguage`` classes
+ is currently being maintained for backward compatibility, but it
+ will change in the future to better conform to the RFC.
+
+ :param offer: (``str``) language tag offer
+ :return: (``bool``) Whether ``offer`` is acceptable according to the
+ header.
+
+ For this class, either there is no ``Accept-Language`` header in the
+ request, or the header is invalid, so any language tag is acceptable,
+ and this always returns ``True``.
+ """
+ warnings.warn(
+ 'The behavior of .__contains__ for the AcceptLanguage classes is '
+ 'currently being maintained for backward compatibility, but it '
+ 'will change in the future to better conform to the RFC.',
+ DeprecationWarning,
+ )
+ return True
+
+ def __iter__(self):
+ """
+ Return all the ranges with non-0 qvalues, in order of preference.
+
+ .. warning::
+
+ The behavior of this method is currently maintained for backward
+ compatibility, but will change in the future.
+
+ :return: iterator of all the language ranges in the header with non-0
+ qvalues, in descending order of qvalue. If two ranges have the
+ same qvalue, they are returned in the order of their positions
+ in the header, from left to right.
+
+ For this class, either there is no ``Accept-Language`` header in the
+ request, or the header is invalid, so there are no language ranges, and
+ this always returns an empty iterator.
+ """
+ warnings.warn(
+ 'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
+ 'maintained for backward compatibility, but will change in the '
+ 'future.',
+ DeprecationWarning,
+ )
+ return iter(())
+
+ def basic_filtering(self, language_tags):
+ """
+ Return the tags that match the header, using Basic Filtering.
+
+ :param language_tags: (``iterable``) language tags
+ :return: A list of tuples of the form (language tag, qvalue), in
+ descending order of preference.
+
+ When the header is invalid and when the header is not in the request,
+ there are no matches, so this method always returns an empty list.
+ """
+ return []
+
+ def best_match(self, offers, default_match=None):
+ """
+ Return the best match from the sequence of language tag `offers`.
+
+ This is the ``.best_match()`` method for when the header is invalid or
+ not found in the request, corresponding to
+ :meth:`AcceptLanguageValidHeader.best_match`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptLanguageValidHeader.best_match`).
+
+ When the header is invalid, or there is no `Accept-Language` header in
+ the request, any of the language tags in `offers` are considered
+ acceptable, so the best match is the tag in `offers` with the highest
+ server quality value (if the server quality value is not supplied, it
+ is 1).
+
+ If more than one language tags in `offers` have the same highest server
+ quality value, then the one that shows up first in `offers` is the best
+ match.
+
+ :param offers: (iterable)
+
+ | Each item in the iterable may be a ``str`` language
+ tag, or a (language tag, server quality value)
+ ``tuple`` or ``list``. (The two may be mixed in the
+ iterable.)
+
+ :param default_match: (optional, any type) the value to be returned if
+ `offers` is empty.
+
+ :return: (``str``, or the type of `default_match`)
+
+ | The language tag that has the highest server quality value.
+ If `offers` is empty, the value of `default_match` is
+ returned.
+ """
+ warnings.warn(
+ 'The behavior of .best_match for the AcceptLanguage classes is '
+ 'currently being maintained for backward compatibility, but the '
+ 'method will be deprecated in the future, as its behavior is not '
+ 'specified in (and currently does not conform to) RFC 7231.',
+ DeprecationWarning,
+ )
+ best_quality = -1
+ best_offer = default_match
+ for offer in offers:
+ if isinstance(offer, (list, tuple)):
+ offer, quality = offer
+ else:
+ quality = 1
+ if quality > best_quality:
+ best_offer = offer
+ best_quality = quality
+ return best_offer
+
+ def lookup(
+ self, language_tags=None, default_range=None, default_tag=None,
+ default=None,
+ ):
+ """
+ Return the language tag that best matches the header, using Lookup.
+
+ When the header is invalid, or there is no ``Accept-Language`` header
+ in the request, all language tags are considered acceptable, so it is
+ as if the header is '*'. As specified for the Lookup matching scheme in
+ :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, when the header is
+ '*', the default value is to be computed and returned. So this method
+ will ignore the `language_tags` and `default_range` arguments, and
+ proceed to `default_tag`, then `default`.
+
+ :param language_tags: (optional, any type)
+
+ | This argument is ignored, and is only used as a
+ placeholder so that the method signature
+ corresponds to that of
+ :meth:`AcceptLanguageValidHeader.lookup`.
+
+ :param default_range: (optional, any type)
+
+ | This argument is ignored, and is only used as a
+ placeholder so that the method signature
+ corresponds to that of
+ :meth:`AcceptLanguageValidHeader.lookup`.
+
+ :param default_tag: (optional, ``None`` or ``str``)
+
+ | At least one of `default_tag` or `default` must
+ be supplied as an argument to the method, to
+ define the defaulting behaviour.
+
+ | If this argument is not ``None``, then it is
+ returned.
+
+ | This parameter corresponds to "return a
+ particular language tag designated for the
+ operation", one of the examples of "defaulting
+ behavior" described in :rfc:`RFC 4647, section
+ 3.4.1 <4647#section-3.4.1>`.
+
+ :param default: (optional, ``None`` or any type, including a callable)
+
+ | At least one of `default_tag` or `default` must be
+ supplied as an argument to the method, to define the
+ defaulting behaviour.
+
+ | If `default_tag` is ``None``, then Lookup will next
+ examine the `default` argument.
+
+ | If `default` is a callable, it will be called, and
+ the callable's return value will be returned.
+
+ | If `default` is not a callable, the value itself will
+ be returned.
+
+ | This parameter corresponds to the "defaulting
+ behavior" described in :rfc:`RFC 4647, section 3.4.1
+ <4647#section-3.4.1>`
+
+ :return: (``str``, or any type)
+
+ | the return value from `default_tag` or `default`.
+ """
+ if default_tag is None and default is None:
+ raise TypeError(
+ '`default_tag` and `default` arguments cannot both be None.'
+ )
+
+ if default_tag is not None:
+ return default_tag
+
+ try:
+ return default()
+ except TypeError: # default is not a callable
+ return default
+
+ def quality(self, offer):
+ """
+ Return quality value of given offer, or ``None`` if there is no match.
+
+ This is the ``.quality()`` method for when the header is invalid or not
+ found in the request, corresponding to
+ :meth:`AcceptLanguageValidHeader.quality`.
+
+ .. warning::
+
+ This is currently maintained for backward compatibility, and will be
+ deprecated in the future (see the documentation for
+ :meth:`AcceptLanguageValidHeader.quality`).
+
+ :param offer: (``str``) language tag offer
+ :return: (``float``) ``1.0``.
+
+ When the ``Accept-Language`` header is invalid or not in the request,
+ all offers are equally acceptable, so 1.0 is always returned.
+ """
+ warnings.warn(
+ 'The behavior of .quality for the AcceptLanguage classes is '
+ 'currently being maintained for backward compatibility, but the '
+ 'method will be deprecated in the future, as its behavior is not '
+ 'specified in (and currently does not conform to) RFC 7231.',
+ DeprecationWarning,
+ )
+ return 1.0
+
+
+class AcceptLanguageNoHeader(_AcceptLanguageInvalidOrNoHeader):
+ """
+ Represent when there is no ``Accept-Language`` header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptLanguageNoHeader.__add__`).
+ """
+
+ def __init__(self):
+ """
+ Create an :class:`AcceptLanguageNoHeader` instance.
+ """
+ self._header_value = None
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__()
+
+ @property
+ def header_value(self):
+ """
+ (``str`` or ``None``) The header value.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As there is no header in the request, this is ``None``.
+ """
+ return self._parsed
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str``
+ * a ``dict``, with language ranges as keys and qvalues as values
+ * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
+ or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be
+ mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptLanguageValidHeader`,
+ :class:`AcceptLanguageNoHeader`, or
+ :class:`AcceptLanguageInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an
+ :class:`AcceptLanguageValidHeader` instance, a new
+ :class:`AcceptLanguageValidHeader` instance with the valid header value
+ is returned.
+
+ If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
+ invalid header value, or an :class:`AcceptLanguageInvalidHeader`
+ instance, a new :class:`AcceptLanguageNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptLanguageValidHeader):
+ return AcceptLanguageValidHeader(header_value=other.header_value)
+
+ if isinstance(
+ other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
+ ):
+ return self.__class__()
+
+ return self._add_instance_and_non_accept_language_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptLanguageNoHeader.__add__`.
+ """
+ return self.__add__(other=other)
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+
+ def __str__(self):
+ """Return the ``str`` ``'<no header in request>'``."""
+ return '<no header in request>'
+
+ def _add_instance_and_non_accept_language_type(self, instance, other):
+ if not other:
+ return self.__class__()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptLanguageValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return self.__class__()
+
+
+class AcceptLanguageInvalidHeader(_AcceptLanguageInvalidOrNoHeader):
+ """
+ Represent an invalid ``Accept-Language`` header.
+
+ An invalid header is one that does not conform to
+ :rfc:`7231#section-5.3.5`. As specified in the RFC, an empty header is an
+ invalid ``Accept-Language`` header.
+
+ :rfc:`7231` does not provide any guidance on what should happen if the
+ ``Accept-Language`` header has an invalid value. This implementation
+ disregards the header, and treats it as if there is no ``Accept-Language``
+ header in the request.
+
+ This object should not be modified. To add to the header, we can use the
+ addition operators (``+`` and ``+=``), which return a new object (see the
+ docstring for :meth:`AcceptLanguageInvalidHeader.__add__`).
+ """
+
+ def __init__(self, header_value):
+ """
+ Create an :class:`AcceptLanguageInvalidHeader` instance.
+ """
+ self._header_value = header_value
+ self._parsed = None
+ self._parsed_nonzero = None
+
+ def copy(self):
+ """
+ Create a copy of the header object.
+
+ """
+ return self.__class__(self._header_value)
+
+ @property
+ def header_value(self):
+ """(``str`` or ``None``) The header value."""
+ return self._header_value
+
+ @property
+ def parsed(self):
+ """
+ (``list`` or ``None``) Parsed form of the header.
+
+ As the header is invalid and cannot be parsed, this is ``None``.
+ """
+ return self._parsed
+
+ def __add__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ `other` can be:
+
+ * ``None``
+ * a ``str``
+ * a ``dict``, with language ranges as keys and qvalues as values
+ * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
+ or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
+ be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptLanguageValidHeader`,
+ :class:`AcceptLanguageNoHeader`, or
+ :class:`AcceptLanguageInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+
+ If `other` is a valid header value or an
+ :class:`AcceptLanguageValidHeader` instance, a new
+ :class:`AcceptLanguageValidHeader` instance with the valid header value
+ is returned.
+
+ If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
+ invalid header value, or an :class:`AcceptLanguageInvalidHeader`
+ instance, a new :class:`AcceptLanguageNoHeader` instance is returned.
+ """
+ if isinstance(other, AcceptLanguageValidHeader):
+ return AcceptLanguageValidHeader(header_value=other.header_value)
+
+ if isinstance(
+ other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
+ ):
+ return AcceptLanguageNoHeader()
+
+ return self._add_instance_and_non_accept_language_type(
+ instance=self, other=other,
+ )
+
+ def __radd__(self, other):
+ """
+ Add to header, creating a new header object.
+
+ See the docstring for :meth:`AcceptLanguageValidHeader.__add__`.
+ """
+ return self._add_instance_and_non_accept_language_type(
+ instance=self, other=other, instance_on_the_right=True,
+ )
+
+ def __repr__(self):
+ return '<{}>'.format(self.__class__.__name__)
+ # We do not display the header_value, as it is untrusted input. The
+ # header_value could always be easily obtained from the .header_value
+ # property.
+
+ def __str__(self):
+ """Return the ``str`` ``'<invalid header value>'``."""
+ return '<invalid header value>'
+
+ def _add_instance_and_non_accept_language_type(
+ self, instance, other, instance_on_the_right=False,
+ ):
+ if not other:
+ return AcceptLanguageNoHeader()
+
+ other_header_value = self._python_value_to_header_str(value=other)
+
+ try:
+ return AcceptLanguageValidHeader(header_value=other_header_value)
+ except ValueError: # invalid header value
+ return AcceptLanguageNoHeader()
+
+
+def create_accept_language_header(header_value):
+ """
+ Create an object representing the ``Accept-Language`` header in a request.
+
+ :param header_value: (``str``) header value
+ :return: If `header_value` is ``None``, an :class:`AcceptLanguageNoHeader`
+ instance.
+
+ | If `header_value` is a valid ``Accept-Language`` header, an
+ :class:`AcceptLanguageValidHeader` instance.
+
+ | If `header_value` is an invalid ``Accept-Language`` header, an
+ :class:`AcceptLanguageInvalidHeader` instance.
+ """
+ if header_value is None:
+ return AcceptLanguageNoHeader()
+ if isinstance(header_value, AcceptLanguage):
+ return header_value.copy()
+ try:
+ return AcceptLanguageValidHeader(header_value=header_value)
+ except ValueError:
+ return AcceptLanguageInvalidHeader(header_value=header_value)
+
+
+def accept_language_property():
+ doc = """
+ Property representing the ``Accept-Language`` header.
+
+ (:rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`)
+
+ The header value in the request environ is parsed and a new object
+ representing the header is created every time we *get* the value of the
+ property. (*set* and *del* change the header value in the request
+ environ, and do not involve parsing.)
+ """
+
+ ENVIRON_KEY = 'HTTP_ACCEPT_LANGUAGE'
+
+ def fget(request):
+ """Get an object representing the header in the request."""
+ return create_accept_language_header(
+ header_value=request.environ.get(ENVIRON_KEY)
+ )
+
+ def fset(request, value):
+ """
+ Set the corresponding key in the request environ.
+
+ `value` can be:
+
+ * ``None``
+ * a ``str``
+ * a ``dict``, with language ranges as keys and qvalues as values
+ * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
+ or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
+ be mixed within the ``tuple`` or ``list``)
+ * an :class:`AcceptLanguageValidHeader`,
+ :class:`AcceptLanguageNoHeader`, or
+ :class:`AcceptLanguageInvalidHeader` instance
+ * object of any other type that returns a value for ``__str__``
+ """
+ if value is None or isinstance(value, AcceptLanguageNoHeader):
+ fdel(request=request)
+ else:
+ if isinstance(
+ value, (AcceptLanguageValidHeader, AcceptLanguageInvalidHeader)
+ ):
+ header_value = value.header_value
+ else:
+ header_value = AcceptLanguage._python_value_to_header_str(
+ value=value,
+ )
+ request.environ[ENVIRON_KEY] = header_value
+
+ def fdel(request):
+ """Delete the corresponding key from the request environ."""
+ try:
+ del request.environ[ENVIRON_KEY]
+ except KeyError:
+ pass
+
+ return property(fget, fset, fdel, textwrap.dedent(doc))