From 85f85c9f63d819f3f7049a65658134c674f0f939 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 2 Mar 2024 15:48:27 -0700 Subject: [PATCH] consolidate AcceptLanguageXXXHeader classes into the single AcceptLanguage class --- docs/api/webob.txt | 20 +- src/webob/acceptparse.py | 1104 ++++--------------------------------- tests/test_acceptparse.py | 926 ++++++++++--------------------- tests/test_request.py | 17 +- 4 files changed, 402 insertions(+), 1665 deletions(-) diff --git a/docs/api/webob.txt b/docs/api/webob.txt index 8bb4145f..b199946c 100644 --- a/docs/api/webob.txt +++ b/docs/api/webob.txt @@ -47,23 +47,9 @@ methods: acceptable_offers, best_match, quality .. autoclass:: AcceptLanguage - :members: parse - -.. autoclass:: AcceptLanguageValidHeader - :members: header_value, parsed, __init__, __add__, __contains__, __iter__, - __radd__, __str__, parse, basic_filtering, best_match, lookup, - quality - -.. autoclass:: AcceptLanguageNoHeader - :members: header_value, parsed, __init__, __add__, __contains__, __iter__, - __radd__, __str__, parse, basic_filtering, best_match, lookup, - quality - -.. autoclass:: AcceptLanguageInvalidHeader - :members: header_value, parsed, __init__, __add__, __contains__, __iter__, - __radd__, __str__, parse, basic_filtering, best_match, lookup, - quality - + :members: + parse, header_value, parsed, header_state, __init__, __add__, + __bool__, __radd__, __repr__, __str__, copy, basic_filtering, lookup Cache-Control ~~~~~~~~~~~~~ diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index e4315afd..5dc83093 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -1872,7 +1872,7 @@ def create_accept_encoding_header(header_value): """ Create an object representing the ``Accept-Encoding`` header in a request. - :param header_value: (``str``) header value + :param header_value: (``str`` or ``None``) header value :return: an :class:`AcceptEncoding` instance. """ @@ -1939,8 +1939,26 @@ class AcceptLanguage: """ Represent an ``Accept-Language`` header. - Base class for :class:`AcceptLanguageValidHeader`, - :class:`AcceptLanguageNoHeader`, and :class:`AcceptLanguageInvalidHeader`. + 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:`.__add__`). + + .. versionchanged:: 2.0 + + - Added the :attr:`.header_state` attribute. + + - Removed previously-deprecated ``__contains__``, ``__iter__``, + ``best_match``, and ``quality`` methods. Look at using the + standards-compliant language-negotiation algorithms provided by + :meth:`.lookup` and :meth:`.basic_filtering` instead. """ # RFC 7231 Section 5.3.5 "Accept-Language": @@ -1959,22 +1977,25 @@ class AcceptLanguage: @classmethod def _python_value_to_header_str(cls, value): + if value is None: + return value + if isinstance(value, str): - header_str = value - else: - if hasattr(value, "items"): - value = sorted(value.items(), key=lambda item: item[1], reverse=True) + return value - if isinstance(value, (tuple, list)): - result = [] + if hasattr(value, "items"): + value = sorted(value.items(), key=lambda item: item[1], reverse=True) - 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) + 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 @@ -2006,36 +2027,28 @@ def generator(value): 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. + Create an :class:`AcceptLanguage` instance. - :param header_value: (``str``) header value. - :raises ValueError: if `header_value` is an invalid value for an - ``Accept-Language`` header. + :param header_value: (``str`` or ``None``) header value. """ + header_value = self._python_value_to_header_str(header_value) 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 + self._parsed = None + if header_value is not None: + try: + self._parsed = tuple(self.parse(header_value)) + except ValueError: + pass + + #: Instance of :enum:`.HeaderState` representing the state of + #: the ``Accept-Language`` header. + self.header_state = ( + HeaderState.Missing + if header_value is None + else (HeaderState.Invalid if self._parsed is None else HeaderState.Valid) + ) def copy(self): """ @@ -2053,53 +2066,13 @@ def header_value(self): @property def parsed(self): """ - (``list`` or ``None``) Parsed form of the header. + (``tuple`` or ``None``) Parsed form of the header. - A list of (language range, quality value) tuples. + A ``tuple`` 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 __bool__(self): """ Return whether ``self`` represents a valid ``Accept-Language`` header. @@ -2107,108 +2080,17 @@ def __bool__(self): 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 - - 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 mask, _quality in sorted( - self._parsed_nonzero, key=lambda i: i[1], reverse=True - ): - yield mask - - 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 - ) + return self.header_state is HeaderState.Valid def __repr__(self): - return f"<{self.__class__.__name__} ({str(self)!r})>" + filler = ( + f"({str(self)!r})" + if self.header_state is HeaderState.Valid + else f": {self.header_state.value}" + ) + return f"<{self.__class__.__name__}{filler}>" def __str__(self): r""" @@ -2218,100 +2100,15 @@ def __str__(self): 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``:: + if self.header_state is HeaderState.Missing: + return "" - >>> instance._old_match(mask='zh', item='zh-Hans-CN') - True - >>> instance._old_match(mask='zh-Hans', item='zh-Hans-CN') - False + elif self.header_state is HeaderState.Invalid: + return "" - 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] + return ", ".join( + _item_qvalue_pair_to_header_element(pair=tuple_) for tuple_ in self.parsed ) def basic_filtering(self, language_tags): @@ -2367,6 +2164,9 @@ def basic_filtering(self, language_tags): # arrive at an algorithm for Basic Filtering as applied to the # Accept-Language header. + if self.header_state is not HeaderState.Valid: + return [] + lowercased_parsed = [ (range_.lower(), qvalue) for (range_, qvalue) in self.parsed ] @@ -2470,151 +2270,6 @@ def match(tag, range_): # (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. @@ -2755,6 +2410,15 @@ def lookup(self, language_tags, default_range=None, default_tag=None, default=No "`default_tag` and `default` arguments cannot both be None." ) + if self.header_state is not HeaderState.Valid: + if default_tag is not None: + return default_tag + + try: + return default() + except TypeError: # default is not a callable + return default + # 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 @@ -2848,503 +2512,6 @@ def best_match(range_): 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 __bool__(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 - - 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 f"<{self.__class__.__name__}>" - - def __str__(self): - """Return the ``str`` ``''``.""" - - return "" - - 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. @@ -3357,90 +2524,50 @@ def __add__(self, other): * 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 + * an :class:`AcceptLanguage` 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. + The rules for adding values to a header are that the values are + appended if valid, or discarded. If everything is discarded then an + instance representing a missing header is returned. """ - if isinstance(other, AcceptLanguageValidHeader): - return AcceptLanguageValidHeader(header_value=other.header_value) - - if isinstance(other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)): - return AcceptLanguageNoHeader() + other = create_accept_language_header(other) + is_self_valid = self.header_state is HeaderState.Valid + is_other_valid = other.header_state is HeaderState.Valid - return self._add_instance_and_non_accept_language_type( - instance=self, other=other - ) + if is_self_valid: + if is_other_valid: + return create_accept_language_header( + self.header_value + ", " + other.header_value + ) + return self + elif is_other_valid: + return other + return create_accept_language_header(None) def __radd__(self, other): """ Add to header, creating a new header object. - See the docstring for :meth:`AcceptLanguageValidHeader.__add__`. + See the docstring for :meth:`.__add__`. """ - return self._add_instance_and_non_accept_language_type( - instance=self, other=other, instance_on_the_right=True - ) - - def __repr__(self): - return f"<{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`` ``''``.""" - - return "" - - 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() + other = create_accept_language_header(other) + return other + self 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. + :param header_value: (``str`` or ``None``) header value + :return: an :class:`AcceptLanguage` 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) + return header_value + return AcceptLanguage(header_value) def accept_language_property(): @@ -3460,9 +2587,7 @@ def accept_language_property(): def fget(request): """Get an object representing the header in the request.""" - return create_accept_language_header( - header_value=request.environ.get(ENVIRON_KEY) - ) + return create_accept_language_header(request.environ.get(ENVIRON_KEY)) def fset(request, value): """ @@ -3476,22 +2601,17 @@ def fset(request, value): * 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 + * an :class:`AcceptLanguage` instance * object of any other type that returns a value for ``__str__`` """ - if value is None or isinstance(value, AcceptLanguageNoHeader): - fdel(request=request) + if isinstance(value, AcceptLanguage): + value = value.header_value 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 + value = AcceptLanguage._python_value_to_header_str(value) + if value is None: + return fdel(request) + request.environ[ENVIRON_KEY] = value def fdel(request): """Delete the corresponding key from the request environ.""" diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index 1f9288ae..a72122aa 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -9,9 +9,6 @@ AcceptCharset, AcceptEncoding, AcceptLanguage, - AcceptLanguageInvalidHeader, - AcceptLanguageNoHeader, - AcceptLanguageValidHeader, AcceptOffer, HeaderState, _item_n_weight_re, @@ -27,11 +24,6 @@ ) from webob.request import Request -IGNORE_BEST_MATCH = "ignore:.*best_match.*" -IGNORE_QUALITY = "ignore:.*quality.*" -IGNORE_CONTAINS = "ignore:.*__contains__.*" -IGNORE_ITER = "ignore:.*__iter__.*" - class StringMe: def __init__(self, value): @@ -2726,267 +2718,41 @@ def test_parse__valid_header(self, value, expected_list): assert list_of_returned == expected_list -class TestAcceptLanguageValidHeader: - @pytest.mark.parametrize("header_value", ["", ", da;q=0.2, en-gb;q=0.3 "]) - def test___init___invalid_header(self, header_value): - with pytest.raises(ValueError): - AcceptLanguageValidHeader(header_value=header_value) - - def test___init___valid_header(self): +class TestAcceptLanguage__valid: + def test___init__(self): header_value = "zh-Hant;q=0.372,zh-CN-a-myExt-x-private;q=0.977,de,*;q=0.000" - instance = AcceptLanguageValidHeader(header_value=header_value) + instance = AcceptLanguage(header_value) + assert instance.header_state is HeaderState.Valid assert instance.header_value == header_value - assert instance.parsed == [ + assert instance.parsed == ( ("zh-Hant", 0.372), ("zh-CN-a-myExt-x-private", 0.977), ("de", 1.0), ("*", 0.0), - ] - assert instance._parsed_nonzero == [ - ("zh-Hant", 0.372), - ("zh-CN-a-myExt-x-private", 0.977), - ("de", 1.0), - ] - assert isinstance(instance, AcceptLanguage) - - def test___add___None(self): - left_operand = AcceptLanguageValidHeader(header_value="en") - result = left_operand + None - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize( - "right_operand", - [ - "", - [], - (), - {}, - "en_gb", - ["en_gb"], - ("en_gb",), - {"en_gb": 1.0}, - ",", - [","], - (",",), - {",": 1.0}, - ], - ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptLanguageValidHeader(header_value="en") - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize("str_", ["", "en_gb", ","]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptLanguageValidHeader(header_value="en") - - class Other: - def __str__(self): - return str_ - - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("en-gb;q=0.5, fr;q=0, es", "en-gb;q=0.5, fr;q=0, es"), - ([("en-gb", 0.5), ("fr", 0.0), "es"], "en-gb;q=0.5, fr;q=0, es"), - ((("en-gb", 0.5), ("fr", 0.0), "es"), "en-gb;q=0.5, fr;q=0, es"), - ({"en-gb": 0.5, "fr": 0.0, "es": 1.0}, "es, en-gb;q=0.5, fr;q=0"), - ], - ) - def test___add___valid_value(self, value, value_as_header): - header = ",\t ,de, zh-Hans;q=0.333," - result = AcceptLanguageValidHeader(header_value=header) + value - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == header + ", " + value_as_header - - def test___add___other_type_with_valid___str__(self): - header = ",\t ,de, zh-Hans;q=0.333," - - class Other: - def __str__(self): - return "en-gb;q=0.5, fr;q=0, es" - - right_operand = Other() - result = AcceptLanguageValidHeader(header_value=header) + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == header + ", " + str(right_operand) - - def test___add___AcceptLanguageValidHeader(self): - header1 = ",\t ,de, zh-Hans;q=0.333," - header2 = ", ,fr;q=0, \tes;q=1," - result = AcceptLanguageValidHeader( - header_value=header1 - ) + AcceptLanguageValidHeader(header_value=header2) - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == header1 + ", " + header2 - - def test___add___AcceptLanguageNoHeader(self): - valid_header_instance = AcceptLanguageValidHeader(header_value="es") - result = valid_header_instance + AcceptLanguageNoHeader() - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance - - @pytest.mark.parametrize("header_value", ["", "en_gb", ","]) - def test___add___AcceptLanguageInvalidHeader(self, header_value): - valid_header_instance = AcceptLanguageValidHeader(header_value="header") - result = valid_header_instance + AcceptLanguageInvalidHeader( - header_value=header_value ) - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance def test___bool__(self): - instance = AcceptLanguageValidHeader(header_value="valid-header") + instance = AcceptLanguage("valid-header") returned = bool(instance) assert returned is True - @pytest.mark.parametrize( - "header_value, offer", - [ - ("*", "da"), - ("da", "DA"), - ("en", "en-gb"), - ("en-gb", "en-gb"), - ("en-gb", "en"), - ("en-gb", "en_GB"), - ], - ) - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains___in(self, header_value, offer): - instance = AcceptLanguageValidHeader(header_value=header_value) - assert offer in instance - - @pytest.mark.parametrize( - "header_value, offer", - [("en-gb", "en-us"), ("en-gb", "fr-fr"), ("en-gb", "fr"), ("en", "fr-fr")], - ) - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains___not_in(self, header_value, offer): - instance = AcceptLanguageValidHeader(header_value=header_value) - assert offer not in instance - - @pytest.mark.parametrize( - "header_value, expected_list", - [ - ("fr;q=0, jp;q=0", []), - ("en-gb, da", ["en-gb", "da"]), - ("en-gb;q=0.5, da;q=0.5", ["en-gb", "da"]), - ( - "de;q=0.8, de-DE-1996;q=0.5, de-Deva;q=0, de-Latn-DE", - ["de-Latn-DE", "de", "de-DE-1996"], - ), - # __iter__ is currently a simple filter for the ranges in the header - # with non-0 qvalues, and does not attempt to account for the special - # meanings of q=0 and *: - ("en-gb;q=0, *", ["*"]), - ("de, de;q=0", ["de"]), - ], - ) - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self, header_value, expected_list): - instance = AcceptLanguageValidHeader(header_value=header_value) - assert list(instance) == expected_list - - def test___radd___None(self): - right_operand = AcceptLanguageValidHeader(header_value="en") - result = None + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "left_operand", - [ - "", - [], - (), - {}, - "en_gb", - ["en_gb"], - ("en_gb",), - {"en_gb": 1.0}, - ",", - [","], - (",",), - {",": 1.0}, - ], - ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptLanguageValidHeader(header_value="en") - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize("str_", ["", "en_gb", ","]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptLanguageValidHeader(header_value="en") - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("en-gb;q=0.5, fr;q=0, es", "en-gb;q=0.5, fr;q=0, es"), - ([("en-gb", 0.5), ("fr", 0.0), "es"], "en-gb;q=0.5, fr;q=0, es"), - ((("en-gb", 0.5), ("fr", 0.0), "es"), "en-gb;q=0.5, fr;q=0, es"), - ({"en-gb": 0.5, "fr": 0.0, "es": 1.0}, "es, en-gb;q=0.5, fr;q=0"), - ], - ) - def test___radd___valid_value(self, value, value_as_header): - right_operand = AcceptLanguageValidHeader( - header_value=",\t ,de, zh-Hans;q=0.333," - ) - result = value + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert ( - result.header_value == value_as_header + ", " + right_operand.header_value - ) - - def test___radd___other_type_with_valid___str__(self): - right_operand = AcceptLanguageValidHeader( - header_value=",\t ,de, zh-Hans;q=0.333," - ) - - class Other: - def __str__(self): - return "en-gb;q=0.5, fr;q=0, es" - - left_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert ( - result.header_value == str(left_operand) + ", " + right_operand.header_value - ) - def test___repr__(self): - instance = AcceptLanguageValidHeader(header_value=",da;q=0.200,en-gb;q=0.300") - assert repr(instance) == "" + instance = AcceptLanguage(",da;q=0.200,en-gb;q=0.300") + assert repr(instance) == "" def test___str__(self): header_value = ", \t,de;q=0.000 \t, es;q=1.000, zh, jp;q=0.210 ," - instance = AcceptLanguageValidHeader(header_value=header_value) + instance = AcceptLanguage(header_value) assert str(instance) == "de;q=0, es, zh, jp;q=0.21" + def test_copy(self): + instance = AcceptLanguage("valid-header") + result = instance.copy() + assert instance is not result + assert instance.header_value == result.header_value + assert instance.header_state == result.header_state + assert instance.parsed == result.parsed + @pytest.mark.parametrize( "header_value, language_tags, expected_returned", [ @@ -3104,37 +2870,12 @@ def test___str__(self): ], ) def test_basic_filtering(self, header_value, language_tags, expected_returned): - instance = AcceptLanguageValidHeader(header_value=header_value) - returned = instance.basic_filtering(language_tags=language_tags) - assert returned == expected_returned - - @pytest.mark.parametrize( - "header_value, offers, default_match, expected_returned", - [ - ("bar, *;q=0", ["foo"], None, None), - ("en-gb, sr-Cyrl", ["sr-Cyrl", "en-gb"], None, "sr-Cyrl"), - ("en-gb, sr-Cyrl", ["en-gb", "sr-Cyrl"], None, "en-gb"), - ("en-gb, sr-Cyrl", [("sr-Cyrl", 0.5), "en-gb"], None, "en-gb"), - ("en-gb, sr-Cyrl", [("sr-Cyrl", 0.5), ("en-gb", 0.4)], None, "sr-Cyrl"), - ("en-gb, sr-Cyrl;q=0.5", ["en-gb", "sr-Cyrl"], None, "en-gb"), - ("en-gb;q=0.5, sr-Cyrl", ["en-gb", "sr-Cyrl"], None, "sr-Cyrl"), - ("en-gb, sr-Cyrl;q=0.55, es;q=0.59", ["en-gb", "sr-Cyrl"], None, "en-gb"), - ( - "en-gb;q=0.5, sr-Cyrl;q=0.586, es-419;q=0.597", - ["en-gb", "es-419"], - None, - "es-419", - ), - ], - ) - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self, header_value, offers, default_match, expected_returned): - instance = AcceptLanguageValidHeader(header_value=header_value) - returned = instance.best_match(offers=offers, default_match=default_match) + instance = AcceptLanguage(header_value) + returned = instance.basic_filtering(language_tags) assert returned == expected_returned def test_lookup_default_tag_and_default_cannot_both_be_None(self): - instance = AcceptLanguageValidHeader(header_value="valid-header") + instance = AcceptLanguage("valid-header") with pytest.raises(TypeError): instance.lookup( language_tags=["tag"], @@ -3144,7 +2885,7 @@ def test_lookup_default_tag_and_default_cannot_both_be_None(self): ) def test_lookup_default_range_cannot_be_asterisk(self): - instance = AcceptLanguageValidHeader(header_value="valid-header") + instance = AcceptLanguage("valid-header") with pytest.raises(ValueError): instance.lookup( language_tags=["tag"], @@ -3520,7 +3261,7 @@ def test_lookup_default_range_cannot_be_asterisk(self): def test_lookup( self, header_value, language_tags, default_range, default_tag, default, expected ): - instance = AcceptLanguageValidHeader(header_value=header_value) + instance = AcceptLanguage(header_value) returned = instance.lookup( language_tags=language_tags, default_range=default_range, @@ -3529,206 +3270,44 @@ def test_lookup( ) assert returned == expected - @pytest.mark.parametrize( - "header_value, offer, expected_returned", - [ - ("en-gb", "en-gb", 1), - ("en-gb;q=0.5", "en-gb", 0.5), - ("en-gb", "sr-Cyrl", None), - ], - ) - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self, header_value, offer, expected_returned): - instance = AcceptLanguageValidHeader(header_value=header_value) - returned = instance.quality(offer=offer) - assert returned == expected_returned - -class TestAcceptLanguageNoHeader: +class TestAcceptLanguage__missing: def test___init__(self): - instance = AcceptLanguageNoHeader() + instance = AcceptLanguage(None) + assert instance.header_state is HeaderState.Missing assert instance.header_value is None assert instance.parsed is None - assert instance._parsed_nonzero is None - assert isinstance(instance, AcceptLanguage) - - def test___add___None(self): - instance = AcceptLanguageNoHeader() - result = instance + None - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not instance - - @pytest.mark.parametrize( - "right_operand", - ["", [], (), {}, "en_gb", ["en_gb"], ("en_gb",), {"en_gb": 1.0}], - ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptLanguageNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not left_operand - - @pytest.mark.parametrize("str_", ["", "en_gb"]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptLanguageNoHeader() - - class Other: - def __str__(self): - return str_ - - result = left_operand + Other() - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not left_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("en-gb;q=0.5, fr;q=0, es", "en-gb;q=0.5, fr;q=0, es"), - ([("en-gb", 0.5), ("fr", 0.0), "es"], "en-gb;q=0.5, fr;q=0, es"), - ((("en-gb", 0.5), ("fr", 0.0), "es"), "en-gb;q=0.5, fr;q=0, es"), - ({"en-gb": 0.5, "fr": 0.0, "es": 1.0}, "es, en-gb;q=0.5, fr;q=0"), - ], - ) - def test___add___valid_value(self, value, value_as_header): - result = AcceptLanguageNoHeader() + value - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == value_as_header - - def test___add___other_type_with_valid___str__(self): - class Other: - def __str__(self): - return "en-gb;q=0.5, fr;q=0, es" - - right_operand = Other() - result = AcceptLanguageNoHeader() + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == str(right_operand) - - def test___add___AcceptLanguageValidHeader(self): - right_operand = AcceptLanguageValidHeader(header_value=", ,fr;q=0, \tes;q=1,") - result = AcceptLanguageNoHeader() + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == right_operand.header_value - - def test___add___AcceptLanguageNoHeader(self): - left_operand = AcceptLanguageNoHeader() - right_operand = AcceptLanguageNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not left_operand - assert result is not right_operand - - @pytest.mark.parametrize("invalid_header_value", ["", "en_gb"]) - def test___add___AcceptLanguageInvalidHeader(self, invalid_header_value): - left_operand = AcceptLanguageNoHeader() - result = left_operand + AcceptLanguageInvalidHeader( - header_value=invalid_header_value - ) - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not left_operand def test___bool__(self): - instance = AcceptLanguageNoHeader() + instance = AcceptLanguage(None) returned = bool(instance) assert returned is False - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - instance = AcceptLanguageNoHeader() - returned = "any-tag" in instance - assert returned is True - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptLanguageNoHeader() - returned = list(instance) - assert returned == [] - - def test___radd___None(self): - right_operand = AcceptLanguageNoHeader() - result = None + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize( - "left_operand", ["", [], (), {}, "en_gb", ["en_gb"], ("en_gb",), {"en_gb": 1.0}] - ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptLanguageNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("str_", ["", "en_gb", ","]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptLanguageNoHeader() - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("en-gb;q=0.5, fr;q=0, es", "en-gb;q=0.5, fr;q=0, es"), - ([("en-gb", 0.5), ("fr", 0.0), "es"], "en-gb;q=0.5, fr;q=0, es"), - ((("en-gb", 0.5), ("fr", 0.0), "es"), "en-gb;q=0.5, fr;q=0, es"), - ({"en-gb": 0.5, "fr": 0.0, "es": 1.0}, "es, en-gb;q=0.5, fr;q=0"), - ], - ) - def test___radd___valid_value(self, value, value_as_header): - result = value + AcceptLanguageNoHeader() - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == value_as_header - - def test___radd___other_type_with_valid___str__(self): - class Other: - def __str__(self): - return "en-gb;q=0.5, fr;q=0, es" - - left_operand = Other() - result = left_operand + AcceptLanguageNoHeader() - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == str(left_operand) - def test___repr__(self): - instance = AcceptLanguageNoHeader() - assert repr(instance) == "" + instance = AcceptLanguage(None) + assert repr(instance) == "" def test___str__(self): - instance = AcceptLanguageNoHeader() + instance = AcceptLanguage(None) assert str(instance) == "" + def test_copy(self): + instance = AcceptLanguage(None) + result = instance.copy() + assert instance is not result + assert instance.header_value == result.header_value + assert instance.header_state == result.header_state + assert instance.parsed == result.parsed + def test_basic_filtering(self): - instance = AcceptLanguageNoHeader() - returned = instance.basic_filtering(language_tags=["tag1", "tag2"]) + instance = AcceptLanguage(None) + returned = instance.basic_filtering(["tag1", "tag2"]) assert returned == [] - @pytest.mark.parametrize( - "offers, default_match, expected_returned", - [ - (["foo", "bar"], None, "foo"), - ([("foo", 1), ("bar", 0.5)], None, "foo"), - ([("foo", 0.5), ("bar", 1)], None, "bar"), - ([("foo", 0.5), "bar"], None, "bar"), - ([("foo", 0.5), "bar"], object(), "bar"), - ([], "fallback", "fallback"), - ], - ) - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self, offers, default_match, expected_returned): - instance = AcceptLanguageNoHeader() - returned = instance.best_match(offers=offers, default_match=default_match) - assert returned == expected_returned - def test_lookup_default_tag_and_default_cannot_both_be_None(self): - instance = AcceptLanguageNoHeader() + instance = AcceptLanguage(None) with pytest.raises(TypeError): - instance.lookup(default_tag=None, default=None) + instance.lookup([], default_tag=None, default=None) @pytest.mark.parametrize( "default_tag, default, expected", @@ -3744,169 +3323,49 @@ def test_lookup_default_tag_and_default_cannot_both_be_None(self): ], ) def test_lookup(self, default_tag, default, expected): - instance = AcceptLanguageNoHeader() - returned = instance.lookup(default_tag=default_tag, default=default) + instance = AcceptLanguage(None) + returned = instance.lookup([], default_tag=default_tag, default=default) assert returned == expected - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - instance = AcceptLanguageNoHeader() - returned = instance.quality(offer="any-tag") - assert returned == 1.0 - -class TestAcceptLanguageInvalidHeader: +class TestAcceptLanguage__invalid: def test___init__(self): header_value = "invalid header" - instance = AcceptLanguageInvalidHeader(header_value=header_value) + instance = AcceptLanguage(header_value) + assert instance.header_state is HeaderState.Invalid assert instance.header_value == header_value assert instance.parsed is None - assert instance._parsed_nonzero is None - assert isinstance(instance, AcceptLanguage) - - def test___add___None(self): - instance = AcceptLanguageInvalidHeader(header_value="") - result = instance + None - assert isinstance(result, AcceptLanguageNoHeader) - - @pytest.mark.parametrize( - "right_operand", - ["", [], (), {}, "en_gb", ["en_gb"], ("en_gb",), {"en_gb": 1.0}], - ) - def test___add___invalid_value(self, right_operand): - result = AcceptLanguageInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - - @pytest.mark.parametrize("str_", ["", "en_gb"]) - def test___add___other_type_with_invalid___str__(self, str_): - class Other: - def __str__(self): - return str_ - - result = AcceptLanguageInvalidHeader(header_value="") + Other() - assert isinstance(result, AcceptLanguageNoHeader) - - @pytest.mark.parametrize("value", ["en", ["en"], ("en",), {"en": 1.0}]) - def test___add___valid_header_value(self, value): - result = AcceptLanguageInvalidHeader(header_value="") + value - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == "en" - - def test___add___other_type_valid_header_value(self): - class Other: - def __str__(self): - return "en" - - result = AcceptLanguageInvalidHeader(header_value="") + Other() - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == "en" - - def test___add___AcceptLanguageValidHeader(self): - right_operand = AcceptLanguageValidHeader(header_value="en") - result = AcceptLanguageInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___add___AcceptLanguageNoHeader(self): - right_operand = AcceptLanguageNoHeader() - result = AcceptLanguageInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptLanguageNoHeader) - assert result is not right_operand - - def test___add___AcceptLanguageInvalidHeader(self): - result = AcceptLanguageInvalidHeader( - header_value="" - ) + AcceptLanguageInvalidHeader(header_value="") - assert isinstance(result, AcceptLanguageNoHeader) def test___bool__(self): - instance = AcceptLanguageInvalidHeader(header_value="") + instance = AcceptLanguage("") returned = bool(instance) assert returned is False - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - instance = AcceptLanguageInvalidHeader(header_value="") - returned = "any-tag" in instance - assert returned is True - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptLanguageInvalidHeader(header_value="") - returned = list(instance) - assert returned == [] - - def test___radd___None(self): - instance = AcceptLanguageInvalidHeader(header_value="") - result = None + instance - assert isinstance(result, AcceptLanguageNoHeader) - - @pytest.mark.parametrize( - "left_operand", ["", [], (), {}, "en_gb", ["en_gb"], ("en_gb",), {"en_gb": 1.0}] - ) - def test___radd___invalid_value(self, left_operand): - result = left_operand + AcceptLanguageInvalidHeader(header_value="") - assert isinstance(result, AcceptLanguageNoHeader) - - @pytest.mark.parametrize("str_", ["", "en_gb"]) - def test___radd___other_type_with_invalid___str__(self, str_): - class Other: - def __str__(self): - return str_ - - result = Other() + AcceptLanguageInvalidHeader(header_value="") - assert isinstance(result, AcceptLanguageNoHeader) - - @pytest.mark.parametrize("value", ["en", ["en"], ("en",), {"en": 1.0}]) - def test___radd___valid_header_value(self, value): - result = value + AcceptLanguageInvalidHeader(header_value="") - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == "en" - - def test___radd___other_type_valid_header_value(self): - class Other: - def __str__(self): - return "en" - - result = Other() + AcceptLanguageInvalidHeader(header_value="") - assert isinstance(result, AcceptLanguageValidHeader) - assert result.header_value == "en" - def test___repr__(self): - instance = AcceptLanguageInvalidHeader(header_value="\x00") - assert repr(instance) == "" + instance = AcceptLanguage("\x00") + assert repr(instance) == "" def test___str__(self): - instance = AcceptLanguageInvalidHeader(header_value="invalid header") + instance = AcceptLanguage("invalid header") assert str(instance) == "" + def test_copy(self): + instance = AcceptLanguage("invalid header") + result = instance.copy() + assert instance is not result + assert instance.header_value == result.header_value + assert instance.header_state == result.header_state + assert instance.parsed == result.parsed + def test_basic_filtering(self): - instance = AcceptLanguageInvalidHeader(header_value="") - returned = instance.basic_filtering(language_tags=["tag1", "tag2"]) + instance = AcceptLanguage("") + returned = instance.basic_filtering(["tag1", "tag2"]) assert returned == [] - @pytest.mark.parametrize( - "offers, default_match, expected_returned", - [ - (["foo", "bar"], None, "foo"), - ([("foo", 1), ("bar", 0.5)], None, "foo"), - ([("foo", 0.5), ("bar", 1)], None, "bar"), - ([("foo", 0.5), "bar"], None, "bar"), - ([("foo", 0.5), "bar"], object(), "bar"), - ([], "fallback", "fallback"), - ], - ) - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self, offers, default_match, expected_returned): - instance = AcceptLanguageInvalidHeader(header_value="") - returned = instance.best_match(offers=offers, default_match=default_match) - assert returned == expected_returned - def test_lookup_default_tag_and_default_cannot_both_be_None(self): - instance = AcceptLanguageInvalidHeader(header_value="") + instance = AcceptLanguage("") with pytest.raises(TypeError): - instance.lookup(default_tag=None, default=None) + instance.lookup([], default_tag=None, default=None) @pytest.mark.parametrize( "default_tag, default, expected", @@ -3922,44 +3381,209 @@ def test_lookup_default_tag_and_default_cannot_both_be_None(self): ], ) def test_lookup(self, default_tag, default, expected): - instance = AcceptLanguageInvalidHeader(header_value="") - returned = instance.lookup(default_tag=default_tag, default=default) + instance = AcceptLanguage("") + returned = instance.lookup([], default_tag=default_tag, default=default) assert returned == expected - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - instance = AcceptLanguageInvalidHeader(header_value="") - returned = instance.quality(offer="any-tag") - assert returned == 1.0 + +class TestAcceptLanguage__add: + invalid_values = [ + "", + [], + (), + {}, + "en_gb", + ["en_gb"], + ("en_gb",), + {"en_gb": 1.0}, + ",", + [","], + (",",), + {",": 1.0}, + StringMe(""), + StringMe("en_gb"), + ] + + valid_values_with_headers = [ + ( + "en-gb;q=0.5, fr;q=0, es", + "en-gb;q=0.5, fr;q=0, es", + ), + ( + [("en-gb", 0.5), ("fr", 0.0), "es"], + "en-gb;q=0.5, fr;q=0, es", + ), + ( + (("en-gb", 0.5), ("fr", 0.0), "es"), + "en-gb;q=0.5, fr;q=0, es", + ), + ( + {"en-gb": 0.5, "fr": 0.0, "es": 1.0}, + "es, en-gb;q=0.5, fr;q=0", + ), + ( + StringMe("en-gb;q=0.5, fr;q=0, es"), + "en-gb;q=0.5, fr;q=0, es", + ), + ] + + # snapshots help confirm the instance is immutable + def snapshot_instance(self, inst): + return deepcopy( + { + "header_value": inst.header_value, + "parsed": inst.parsed, + "header_state": inst.header_state, + } + ) + + # we want to test math with primitive python values and Accept instances + @pytest.fixture(params=["primitive", "instance"]) + def maker(self, request): + if request.param == "primitive": + return lambda x: x + return AcceptLanguage + + # almost always add and radd are symmetrical so we can test both and + # expect the same result + @pytest.fixture(params=["add", "radd"]) + def fn(self, request): + if request.param == "add": + return lambda x, y: x + y + return lambda x, y: y + x + + @pytest.mark.parametrize( + "input_value, input_header", + valid_values_with_headers, + ) + def test_valid_add_missing(self, input_value, input_header, maker, fn): + inst = AcceptLanguage(input_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == input_header + + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == input_header + + def test_invalid_add_missing(self, maker, fn): + invalid_value = "en_gb" + inst = AcceptLanguage(invalid_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == invalid_value + + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + def test_missing_add_missing(self, maker, fn): + inst = AcceptLanguage(None) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Missing + assert inst.header_value is None + + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + @pytest.mark.parametrize("valid_value, valid_header", valid_values_with_headers) + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_valid_add_invalid( + self, valid_value, valid_header, invalid_value, maker, fn + ): + inst = AcceptLanguage(valid_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == valid_header + + result = fn(inst, maker(invalid_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == valid_header + + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_invalid_add_invalid(self, invalid_value, maker, fn): + inst = AcceptLanguage("") + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == "" + + result = fn(inst, maker(invalid_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_missing_add_invalid(self, invalid_value, maker, fn): + inst = AcceptLanguage(None) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Missing + assert inst.header_value is None + + result = fn(inst, maker(invalid_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + @pytest.mark.parametrize( + "input_value, input_header", + valid_values_with_headers, + ) + def test_valid_add_valid(self, input_value, input_header, maker): + seed_value = "en" + inst = AcceptLanguage(seed_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == seed_value + + result = inst + maker(input_value) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == seed_value + ", " + input_header + + result = maker(input_value) + inst + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == input_header + ", " + seed_value class TestCreateAcceptLanguageHeader: def test_header_value_is_None(self): header_value = None - returned = create_accept_language_header(header_value=header_value) - assert isinstance(returned, AcceptLanguageNoHeader) + returned = create_accept_language_header(header_value) + assert isinstance(returned, AcceptLanguage) + assert returned.header_state is HeaderState.Missing assert returned.header_value == header_value returned2 = create_accept_language_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2 is returned + assert returned2.header_state is HeaderState.Missing + assert returned2.header_value == header_value def test_header_value_is_valid(self): header_value = "es, ja" - returned = create_accept_language_header(header_value=header_value) - assert isinstance(returned, AcceptLanguageValidHeader) + returned = create_accept_language_header(header_value) + assert isinstance(returned, AcceptLanguage) + assert returned.header_state is HeaderState.Valid assert returned.header_value == header_value returned2 = create_accept_language_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2 is returned + assert returned2.header_state is HeaderState.Valid + assert returned2.header_value == header_value @pytest.mark.parametrize("header_value", ["", "en_gb"]) def test_header_value_is_invalid(self, header_value): - returned = create_accept_language_header(header_value=header_value) - assert isinstance(returned, AcceptLanguageInvalidHeader) + returned = create_accept_language_header(header_value) + assert isinstance(returned, AcceptLanguage) + assert returned.header_state is HeaderState.Invalid assert returned.header_value == header_value returned2 = create_accept_language_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2 is returned + assert returned2.header_state is HeaderState.Invalid + assert returned2.header_value == header_value class TestAcceptLanguageProperty: @@ -3967,39 +3591,45 @@ def test_fget_header_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": None}) property_ = accept_language_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptLanguageNoHeader) + assert isinstance(returned, AcceptLanguage) + assert returned.header_state is HeaderState.Missing def test_fget_header_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "es"}) property_ = accept_language_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptLanguageValidHeader) + assert isinstance(returned, AcceptLanguage) + assert returned.header_state is HeaderState.Valid def test_fget_header_is_invalid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "en_gb"}) property_ = accept_language_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptLanguageInvalidHeader) + assert isinstance(returned, AcceptLanguage) + assert returned.header_state is HeaderState.Invalid def test_fset_value_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "es"}) property_ = accept_language_property() property_.fset(request=request, value=None) - assert isinstance(request.accept_language, AcceptLanguageNoHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Missing assert "HTTP_ACCEPT_LANGUAGE" not in request.environ def test_fset_value_is_invalid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "es"}) property_ = accept_language_property() property_.fset(request=request, value="en_GB") - assert isinstance(request.accept_language, AcceptLanguageInvalidHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Invalid assert request.environ["HTTP_ACCEPT_LANGUAGE"] == "en_GB" def test_fset_value_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "es"}) property_ = accept_language_property() property_.fset(request=request, value="en-GB") - assert isinstance(request.accept_language, AcceptLanguageValidHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_LANGUAGE"] == "en-GB" @pytest.mark.parametrize( @@ -4015,56 +3645,58 @@ def test_fset_value_types(self, value, value_as_header): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": ""}) property_ = accept_language_property() property_.fset(request=request, value=value) - assert isinstance(request.accept_language, AcceptLanguageValidHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_LANGUAGE"] == value_as_header def test_fset_other_type_with_valid___str__(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": ""}) property_ = accept_language_property() - - class Other: - def __str__(self): - return "en-gb;q=0.5, fr;q=0, es" - - value = Other() + value = StringMe("en-gb;q=0.5, fr;q=0, es") property_.fset(request=request, value=value) - assert isinstance(request.accept_language, AcceptLanguageValidHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_LANGUAGE"] == str(value) - def test_fset_AcceptLanguageNoHeader(self): + def test_fset_missing_AcceptLanguage(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "en"}) property_ = accept_language_property() - header = AcceptLanguageNoHeader() + header = AcceptLanguage(None) property_.fset(request=request, value=header) - assert isinstance(request.accept_language, AcceptLanguageNoHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Missing assert "HTTP_ACCEPT_LANGUAGE" not in request.environ - def test_fset_AcceptLanguageValidHeader(self): + def test_fset_valid_AcceptLanguage(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": ""}) property_ = accept_language_property() - header = AcceptLanguageValidHeader("es") + header = AcceptLanguage("es") property_.fset(request=request, value=header) - assert isinstance(request.accept_language, AcceptLanguageValidHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_LANGUAGE"] == header.header_value - def test_fset_AcceptLanguageInvalidHeader(self): + def test_fset_invalid_AcceptLanguage(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": ""}) property_ = accept_language_property() - header = AcceptLanguageInvalidHeader("en_gb") + header = AcceptLanguage("en_gb") property_.fset(request=request, value=header) - assert isinstance(request.accept_language, AcceptLanguageInvalidHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Invalid assert request.environ["HTTP_ACCEPT_LANGUAGE"] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank("/", environ={"HTTP_ACCEPT_LANGUAGE": "es"}) property_ = accept_language_property() property_.fdel(request=request) - assert isinstance(request.accept_language, AcceptLanguageNoHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Missing assert "HTTP_ACCEPT_LANGUAGE" not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank("/") property_ = accept_language_property() property_.fdel(request=request) - assert isinstance(request.accept_language, AcceptLanguageNoHeader) + assert isinstance(request.accept_language, AcceptLanguage) + assert request.accept_language.header_state is HeaderState.Missing assert "HTTP_ACCEPT_LANGUAGE" not in request.environ diff --git a/tests/test_request.py b/tests/test_request.py index e9284196..0faf529d 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -9,9 +9,7 @@ Accept, AcceptCharset, AcceptEncoding, - AcceptLanguageInvalidHeader, - AcceptLanguageNoHeader, - AcceptLanguageValidHeader, + AcceptLanguage, HeaderState as AcceptHeaderState, ) from webob.multidict import NoVars @@ -808,21 +806,24 @@ def test_accept_encoding_valid_header(self): def test_accept_language_no_header(self): req = self._makeOne(environ={}) header = req.accept_language - assert isinstance(header, AcceptLanguageNoHeader) + assert isinstance(header, AcceptLanguage) + assert header.header_state is AcceptHeaderState.Missing assert header.header_value is None @pytest.mark.parametrize("header_value", ["", ", da;q=0.2, en-gb;q =0.3"]) def test_accept_language_invalid_header(self, header_value): req = self._makeOne(environ={"HTTP_ACCEPT_LANGUAGE": header_value}) header = req.accept_language - assert isinstance(header, AcceptLanguageInvalidHeader) + assert isinstance(header, AcceptLanguage) + assert header.header_state is AcceptHeaderState.Invalid assert header.header_value == header_value def test_accept_language_valid_header(self): header_value = "zh-Hant;q=0.372,zh-CN-a-myExt-x-private;q=0.977,de,*;q=0.000" req = self._makeOne(environ={"HTTP_ACCEPT_LANGUAGE": header_value}) header = req.accept_language - assert isinstance(header, AcceptLanguageValidHeader) + assert isinstance(header, AcceptLanguage) + assert header.header_state is AcceptHeaderState.Valid assert header.header_value == header_value # authorization @@ -1954,7 +1955,6 @@ def fut(r, offers): ("message/x-foo", 1.0), ] - @pytest.mark.filterwarnings("ignore:.*best_match.*") def test_from_mimeparse(self): # http://mimeparse.googlecode.com/svn/trunk/mimeparse.py supported = ["application/xbel+xml", "application/xml"] @@ -2703,7 +2703,6 @@ def test_request_query_and_POST_vars(self): assert req.params["name"] == "Bob" assert req.params.getall("name"), ["Bob" == "Joe"] - @pytest.mark.filterwarnings("ignore:.*best_match.*") def test_request_put(self): from datetime import datetime @@ -2755,7 +2754,7 @@ def test_request_put(self): ) req.accept_language = "es, pt-BR" - assert req.accept_language.best_match(["es"]) == "es" + assert req.accept_language.lookup(["es"], default=lambda: None) == "es" # Conditional Requests server_token = "opaque-token"