From 50f66694ae3d2322bdc254ae71c4b6923b01f8fd Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 2 Mar 2024 12:45:06 -0700 Subject: [PATCH] consolidate AcceptEncodingXXXHeader classes into the single AcceptEncoding class --- CHANGES.txt | 30 + docs/api/webob.txt | 20 +- src/webob/acceptparse.py | 918 ++++++------------------------- tests/test_acceptparse.py | 1088 ++++++++++++------------------------- tests/test_request.py | 13 +- 5 files changed, 550 insertions(+), 1519 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 89a7f2c8..61c8574e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -34,6 +34,15 @@ Feature ``acceptable_offers``. See backward incompatibilities below. See https://github.com/Pylons/webob/pull/461 +- Consolidation of ``AcceptEncoding`` header handling into a single class. + See backward incompatibilities below for more information. + See https://github.com/Pylons/webob/pull/462 + +- ``webob.acceptparse.AcceptEncoding``, methods ``best_match``, ``quality``, + and ``__contains__`` are undeprecated and their logic is made consistent with + ``acceptable_offers``. See backward incompatibilities below. + See https://github.com/Pylons/webob/pull/462 + Compatibility ~~~~~~~~~~~~~ @@ -84,6 +93,27 @@ Backwards Incompatibilities See https://github.com/Pylons/webob/pull/461 +- Remove ``AcceptEncodingValidHeader``, ``AcceptEncodingNoHeader`` and + ``AcceptEncodingInvalidHeader``. These classes are consolidated into + ``AcceptEncoding`` with a ``header_state`` attribute for users that need + to know the state of the header. + See https://github.com/Pylons/webob/pull/462 + +- Remove previously-deprecated ``webob.acceptparse.AcceptEncoding.__iter__``. + See https://github.com/Pylons/webob/pull/462 + +- ``webob.acceptparse.AcceptEncoding`` methods, ``best_match``, ``quality``, + and ``__contains__`` are now thin wrappers around ``acceptable_offers`` which + modifies their behavior slightly: + + - A tuple can no longer be an offer containing server-side quality values. + - An offer will only match a ``*`` clause in a header if it does not match + any other clauses. + - The ``identity`` offer was not properly considered a match unless the + header explicitly it excluded via ``*;q=0`` or ``identity;q=0``. + + See https://github.com/Pylons/webob/pull/462 + Experimental Features ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/api/webob.txt b/docs/api/webob.txt index 6899dfe4..6c4bf7c0 100644 --- a/docs/api/webob.txt +++ b/docs/api/webob.txt @@ -41,22 +41,10 @@ methods: acceptable_offers, best_match, quality .. autoclass:: AcceptEncoding - :members: parse - -.. autoclass:: AcceptEncodingValidHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - acceptable_offers, best_match, quality - -.. autoclass:: AcceptEncodingNoHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - acceptable_offers, best_match, quality - -.. autoclass:: AcceptEncodingInvalidHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - acceptable_offers, best_match, quality + :members: + parse, header_value, parsed, header_state, __init__, __add__, + __bool__, __radd__, __repr__, __str__, __contains__, copy, + acceptable_offers, best_match, quality .. autoclass:: AcceptLanguage :members: parse diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 80e3adf9..e4315afd 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -1472,8 +1472,19 @@ class AcceptEncoding: """ Represent an ``Accept-Encoding`` header. - Base class for :class:`AcceptEncodingValidHeader`, - :class:`AcceptEncodingNoHeader`, and :class:`AcceptEncodingInvalidHeader`. + 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:`.__add__`). + + .. versionchanged:: 2.0 + + - Added the :attr:`.header_state` attribute. + + - Removed ``__iter__`` and changed the behavior of :meth:`.best_match`, + :meth:`.quality`, and :meth:`.__contains__`. """ # RFC 7231 Section 3.1.2.1 "Content Codings": @@ -1492,22 +1503,25 @@ class AcceptEncoding: @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 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) + 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 @@ -1539,18 +1553,28 @@ def generator(value): return generator(value=value) + def __init__(self, header_value): + """ + Create an :class:`AcceptEncoding` instance. -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>`. + :param header_value: (``str``) header value. + """ + header_value = self._python_value_to_header_str(header_value) + self._header_value = header_value + self._parsed = None + if header_value is not None: + try: + self._parsed = tuple(self.parse(header_value)) + except ValueError: + pass - 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__`). - """ + #: Instance of :enum:`.HeaderState` representing the state of + #: the ``Accept-Encoding`` header. + self.header_state = ( + HeaderState.Missing + if header_value is None + else (HeaderState.Invalid if self._parsed is None else HeaderState.Valid) + ) @property def header_value(self): @@ -1561,31 +1585,18 @@ 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 (*codings*, *qvalue*) tuples, where + A ``tuple`` of (*codings*, *qvalue*) tuples, where - *codings* (``str``) is a content-coding, the string "``identity``", or - "``*``"; and + *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. @@ -1593,54 +1604,6 @@ def copy(self): """ 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. @@ -1648,93 +1611,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:`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 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:`AcceptEncodingValidHeader.__add__`. """ - return self._add_instance_and_non_accept_encoding_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""" @@ -1743,58 +1630,16 @@ def __str__(self): 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) + if self.header_state is HeaderState.Missing: + return "" - try: - self.parse(value=other_header_value) - except ValueError: # invalid header value - return self.__class__(header_value=instance.header_value) + elif self.header_state is HeaderState.Invalid: + return "" - new_header_value = ( - (other_header_value + ", " + instance.header_value) - if instance_on_the_right - else (instance.header_value + ", " + other_header_value) + return ", ".join( + _item_qvalue_pair_to_header_element(pair=tuple_) for tuple_ in self.parsed ) - 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:`AcceptEncodingValidHeader.__contains__` - - :meth:`AcceptEncodingValidHeader.best_match` - - :meth:`AcceptEncodingValidHeader.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): """ @@ -1802,28 +1647,32 @@ def acceptable_offers(self, offers): 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. + ``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 + :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 + :return: A list of tuples of the form (content-coding or ``identity``, + qvalue), in descending order of qvalue. Where two offers match 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. + Use the string ``identity`` 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. """ + + if self.header_state is not HeaderState.Valid: + return [(offer, 1.0) for offer in offers] + lowercased_parsed = [ (codings.lower(), qvalue) for (codings, qvalue) in self.parsed ] @@ -1881,471 +1730,93 @@ def acceptable_offers(self, offers): def best_match(self, offers, default_match=None): """ - Return the best match from the sequence of `offers`. + Return the best match from the sequence of ``offers``. - .. warning:: + This is a thin wrapper around :meth:`.acceptable_offers` that makes + usage more convenient for typical use-cases where you just want + to know the client's most preferred match. - This is currently maintained for backward compatibility, and will be - deprecated in the future. + :param offers: ``iterable`` of ``str``s, where each ``str`` is a + content-coding or the string ``identity`` (the token + used to represent "no encoding") - :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. + :param default_match: + (optional, any type) the value to be returned if there is no match - 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). + :return: + (``str``, or the type of ``default_match``) - 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. + | The offer that is the best match based on q-value. If there is no + match, the value of ``default_match`` is returned. Where two + offers match the same qvalue, they are returned in the same order + as their order in ``offers``. - :param offers: (iterable) + .. versionchanged:: 2.0 - | 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. + - A tuple can no longer be an offer containing server-side quality + values. + - An offer will only match a ``*`` clause in a header if it does + not match any other clauses. + - The ``identity`` offer was not properly considered a match unless + the header explicitly it excluded via ``*;q=0`` or + ``identity;q=0``. + """ + matches = self.acceptable_offers(offers) + if matches: + return matches[0][0] + return default_match - :param default_match: (optional, any type) the value to be returned if - there is no match + def quality(self, offer): + """ + Return quality value of given ``offer``, or ``None`` if there is no match. - :return: (``str``, or the type of `default_match`) + This is a thin wrapper around :meth:`.acceptable_offers` that matches + a specific ``offer``. - | 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``) + :param offer: (``str``) a content-coding, or ``identity`` offer + :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. + .. versionchanged:: 2.0 - For this class, it always returns ``False``. + - A tuple can no longer be an offer containing server-side quality + values. + - An offer will only match a ``*`` clause in a header if it does + not match any other clauses. + - The ``identity`` offer was not properly considered a match unless + the header explicitly it excluded via ``*;q=0`` or + ``identity;q=0``. """ - return False + matches = self.acceptable_offers([offer]) + if matches: + return matches[0][1] 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. + This is a thin wrapper around :meth:`.acceptable_offers` that matches + a specific ``offer``. :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 f"<{self.__class__.__name__}>" - - def __str__(self): - """Return the ``str`` ``''``.""" - - return "" - - 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 + .. versionchanged:: 2.0 - def copy(self): + - A tuple can no longer be an offer containing server-side quality + values. + - An offer will only match a ``*`` clause in a header if it does + not match any other clauses. + - The ``identity`` offer was not properly considered a match unless + the header explicitly it excluded via ``*;q=0`` or + ``identity;q=0``. """ - Create a copy of the header object. - """ - return self.__class__(self._header_value) + return self.quality(offer) is not None def __add__(self, other): """ @@ -2360,65 +1831,41 @@ def __add__(self, other): * 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 + * an :class:`AcceptEncoding` 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. + 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, AcceptEncodingValidHeader): - return AcceptEncodingValidHeader(header_value=other.header_value) - - if isinstance(other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)): - return AcceptEncodingNoHeader() + other = create_accept_encoding_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_encoding_type( - instance=self, other=other - ) + if is_self_valid: + if is_other_valid: + if self.header_value == "": + return other + if other.header_value == "": + return self + return create_accept_encoding_header( + self.header_value + ", " + other.header_value + ) + return self + elif is_other_valid: + return other + return create_accept_encoding_header(None) def __radd__(self, other): """ Add to header, creating a new header object. - See the docstring for :meth:`AcceptEncodingValidHeader.__add__`. + See the docstring for :meth:`.__add__`. """ - return self._add_instance_and_non_accept_encoding_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_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() + other = create_accept_encoding_header(other) + return other + self def create_accept_encoding_header(header_value): @@ -2426,24 +1873,12 @@ 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. + :return: an :class:`AcceptEncoding` 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) + return header_value + return AcceptEncoding(header_value) def accept_encoding_property(): @@ -2463,9 +1898,7 @@ def accept_encoding_property(): def fget(request): """Get an object representing the header in the request.""" - return create_accept_encoding_header( - header_value=request.environ.get(ENVIRON_KEY) - ) + return create_accept_encoding_header(request.environ.get(ENVIRON_KEY)) def fset(request, value): """ @@ -2480,22 +1913,17 @@ def fset(request, value): * 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 + * an :class:`AcceptEncoding` instance * object of any other type that returns a value for ``__str__`` """ - if value is None or isinstance(value, AcceptEncodingNoHeader): - fdel(request=request) + if isinstance(value, AcceptEncoding): + value = value.header_value 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 + value = AcceptEncoding._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 46ec4754..1f9288ae 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -8,9 +8,6 @@ Accept, AcceptCharset, AcceptEncoding, - AcceptEncodingInvalidHeader, - AcceptEncodingNoHeader, - AcceptEncodingValidHeader, AcceptLanguage, AcceptLanguageInvalidHeader, AcceptLanguageNoHeader, @@ -2001,7 +1998,7 @@ def test_fdel_header_key_not_in_environ(self): assert "HTTP_ACCEPT_CHARSET" not in request.environ -class TestAcceptEncoding: +class TestAcceptEncoding__parsing: @pytest.mark.parametrize( "value", [ @@ -2059,274 +2056,32 @@ def test_parse__valid_header(self, value, expected_list): assert list_of_returned == expected_list -class TestAcceptEncodingValidHeader: - def test_parse__inherited(self): - returned = AcceptEncodingValidHeader.parse( - value=",,\t gzip;q=1.0, identity; q=0.5, *;q=0 \t ," - ) - list_of_returned = list(returned) - assert list_of_returned == [("gzip", 1.0), ("identity", 0.5), ("*", 0.0)] - - @pytest.mark.parametrize( - "header_value", [", ", "gzip;q=1.0, identity; q =0.5, *;q=0"] - ) - def test___init___invalid_header(self, header_value): - with pytest.raises(ValueError): - AcceptEncodingValidHeader(header_value=header_value) - - def test___init___valid_header(self): +class TestAcceptEncoding__valid: + def test___init___(self): header_value = ",,\t gzip;q=1.0, identity; q=0, *;q=0.5 \t ," - instance = AcceptEncodingValidHeader(header_value=header_value) + instance = AcceptEncoding(header_value) + assert instance.header_state is HeaderState.Valid assert instance.header_value == header_value - assert instance.parsed == [("gzip", 1.0), ("identity", 0.0), ("*", 0.5)] - assert instance._parsed_nonzero == [("gzip", 1.0), ("*", 0.5)] - assert isinstance(instance, AcceptEncoding) - - def test___add___None(self): - left_operand = AcceptEncodingValidHeader(header_value="gzip") - result = left_operand + None - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize("right_operand", [", ", [", "], (", ",), {", ": 1.0}]) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptEncodingValidHeader(header_value="gzip") - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - def test___add___other_type_with_invalid___str__(self): - left_operand = AcceptEncodingValidHeader(header_value="gzip") - - class Other: - def __str__(self): - return ", " - - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___add___valid_empty_value(self, value): - left_operand = AcceptEncodingValidHeader(header_value="gzip") - result = left_operand + value - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - def test___add___other_type_with_valid___str___empty(self): - left_operand = AcceptEncodingValidHeader(header_value="gzip") - - class Other: - def __str__(self): - return "" - - result = left_operand + Other() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("compress;q=0.5, deflate;q=0, *", "compress;q=0.5, deflate;q=0, *"), - (["compress;q=0.5", "deflate;q=0", "*"], "compress;q=0.5, deflate;q=0, *"), - ( - [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], - "compress;q=0.5, deflate;q=0, *", - ), - (("compress;q=0.5", "deflate;q=0", "*"), "compress;q=0.5, deflate;q=0, *"), - ( - (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), - "compress;q=0.5, deflate;q=0, *", - ), - ( - {"compress": 0.5, "deflate": 0.0, "*": 1.0}, - "*, compress;q=0.5, deflate;q=0", - ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - header = ",\t ,gzip, identity;q=0.333," - result = AcceptEncodingValidHeader(header_value=header) + value - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == header + ", " + value_as_header - - def test___add___other_type_with_valid___str___not_empty(self): - header = ",\t ,gzip, identity;q=0.333," - - class Other: - def __str__(self): - return "compress;q=0.5, deflate;q=0, *" - - right_operand = Other() - result = AcceptEncodingValidHeader(header_value=header) + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == header + ", " + str(right_operand) - - def test___add___AcceptEncodingValidHeader_header_value_empty(self): - left_operand = AcceptEncodingValidHeader( - header_value=",\t ,gzip, identity;q=0.333," - ) - right_operand = AcceptEncodingValidHeader(header_value="") - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - def test___add___AcceptEncodingValidHeader_header_value_not_empty(self): - left_operand = AcceptEncodingValidHeader( - header_value=",\t ,gzip, identity;q=0.333," - ) - right_operand = AcceptEncodingValidHeader( - header_value="compress;q=0.5, deflate;q=0, *" - ) - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert ( - result.header_value - == left_operand.header_value + ", " + right_operand.header_value - ) - - def test___add___AcceptEncodingNoHeader(self): - valid_header_instance = AcceptEncodingValidHeader(header_value="gzip") - result = valid_header_instance + AcceptEncodingNoHeader() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance - - @pytest.mark.parametrize("header_value", [", ", "compress;q=1.001"]) - def test___add___AcceptEncodingInvalidHeader(self, header_value): - valid_header_instance = AcceptEncodingValidHeader(header_value="gzip") - result = valid_header_instance + AcceptEncodingInvalidHeader( - header_value=header_value - ) - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance + assert instance.parsed == (("gzip", 1.0), ("identity", 0.0), ("*", 0.5)) def test___bool__(self): - instance = AcceptEncodingValidHeader(header_value="gzip") + instance = AcceptEncoding("gzip") returned = bool(instance) assert returned is True - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - accept = AcceptEncodingValidHeader("gzip, compress") - assert "gzip" in accept - assert "deflate" not in accept - for mask in ["*", "gzip", "gZIP"]: - assert "gzip" in AcceptEncodingValidHeader(mask) - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptEncodingValidHeader( - header_value="gzip; q=0.5, *; q=0, deflate; q=0.8, compress" - ) - assert list(instance) == ["compress", "deflate", "gzip"] - - def test___radd___None(self): - right_operand = AcceptEncodingValidHeader(header_value="gzip") - result = None + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize("left_operand", [", ", [", "], (", ",), {", ": 1.0}]) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptEncodingValidHeader(header_value="gzip") - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___radd___other_type_with_invalid___str__(self): - right_operand = AcceptEncodingValidHeader(header_value="gzip") - - class Other: - def __str__(self): - return ", " - - result = Other() + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___radd___valid_empty_value(self, value): - right_operand = AcceptEncodingValidHeader(header_value="gzip") - result = value + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___radd___other_type_with_valid___str___empty(self): - right_operand = AcceptEncodingValidHeader(header_value="gzip") - - class Other: - def __str__(self): - return "" - - result = Other() + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("compress;q=0.5, deflate;q=0, *", "compress;q=0.5, deflate;q=0, *"), - (["compress;q=0.5", "deflate;q=0", "*"], "compress;q=0.5, deflate;q=0, *"), - ( - [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], - "compress;q=0.5, deflate;q=0, *", - ), - (("compress;q=0.5", "deflate;q=0", "*"), "compress;q=0.5, deflate;q=0, *"), - ( - (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), - "compress;q=0.5, deflate;q=0, *", - ), - ( - {"compress": 0.5, "deflate": 0.0, "*": 1.0}, - "*, compress;q=0.5, deflate;q=0", - ), - ], - ) - def test___radd___valid_non_empty_value(self, value, value_as_header): - header = ",\t ,gzip, identity;q=0.333," - result = value + AcceptEncodingValidHeader(header_value=header) - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == value_as_header + ", " + header - - def test___radd___other_type_with_valid___str___not_empty(self): - header = ",\t ,gzip, identity;q=0.333," - - class Other: - def __str__(self): - return "compress;q=0.5, deflate;q=0, *" - - left_operand = Other() - result = left_operand + AcceptEncodingValidHeader(header_value=header) - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == str(left_operand) + ", " + header - @pytest.mark.parametrize( "header_value, expected_returned", [ - ("", ""), + ("", ""), ( ",\t, a ;\t q=0.20 , b ,',", # single quote is valid character in token - """""", + """""", ), ], ) def test___repr__(self, header_value, expected_returned): - instance = AcceptEncodingValidHeader(header_value=header_value) + instance = AcceptEncoding(header_value) assert repr(instance) == expected_returned @pytest.mark.parametrize( @@ -2334,9 +2089,17 @@ def test___repr__(self, header_value, expected_returned): [("", ""), (",\t, a ;\t q=0.20 , b ,',", "a;q=0.2, b, '")], ) def test___str__(self, header_value, expected_returned): - instance = AcceptEncodingValidHeader(header_value=header_value) + instance = AcceptEncoding(header_value) assert str(instance) == expected_returned + def test_copy(self): + instance = AcceptEncoding("gzip") + 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, offers, expected_returned", [ @@ -2375,584 +2138,397 @@ def test___str__(self, header_value, expected_returned): ], ) def test_acceptable_offers(self, header_value, offers, expected_returned): - instance = AcceptEncodingValidHeader(header_value=header_value) + instance = AcceptEncoding(header_value) returned = instance.acceptable_offers(offers=offers) assert returned == expected_returned - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): - accept = AcceptEncodingValidHeader("gzip, iso-8859-5") + accept = AcceptEncoding("gzip, iso-8859-5") assert accept.best_match(["gzip", "iso-8859-5"]) == "gzip" assert accept.best_match(["iso-8859-5", "gzip"]) == "iso-8859-5" - assert accept.best_match([("iso-8859-5", 0.5), "gzip"]) == "gzip" - assert accept.best_match([("iso-8859-5", 0.5), ("gzip", 0.4)]) == "iso-8859-5" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_one_lower_q(self): - accept = AcceptEncodingValidHeader("gzip, compress;q=0.5") + accept = AcceptEncoding("gzip, compress;q=0.5") assert accept.best_match(["gzip", "compress"]) == "gzip" - accept = AcceptEncodingValidHeader("gzip;q=0.5, compress") + accept = AcceptEncoding("gzip;q=0.5, compress") assert accept.best_match(["gzip", "compress"]) == "compress" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_complex_q(self): - accept = AcceptEncodingValidHeader("gzip, compress;q=0.55, deflate;q=0.59") + accept = AcceptEncoding("gzip, compress;q=0.55, deflate;q=0.59") assert accept.best_match(["gzip", "compress"]) == "gzip" - accept = AcceptEncodingValidHeader( - "gzip;q=0.5, compress;q=0.586, deflate;q=0.596" - ) + accept = AcceptEncoding("gzip;q=0.5, compress;q=0.586, deflate;q=0.596") assert accept.best_match(["gzip", "deflate"]) == "deflate" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_mixedcase(self): - accept = AcceptEncodingValidHeader("gZiP; q=0.2, COMPress; Q=0.4, *; q=0.05") + accept = AcceptEncoding("gZiP; q=0.2, COMPress; Q=0.4, *; q=0.05") assert accept.best_match(["gzIP"]) == "gzIP" assert accept.best_match(["DeFlAte"]) == "DeFlAte" assert accept.best_match(["deflaTe", "compRess", "UtF-8"]) == "compRess" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_best_match_zero_quality(self): - assert AcceptEncodingValidHeader("deflate, *;q=0").best_match(["gzip"]) is None - assert "content-coding" not in AcceptEncodingValidHeader("*;q=0") + assert AcceptEncoding("deflate, *;q=0").best_match(["gzip"]) is None + assert "content-coding" not in AcceptEncoding("*;q=0") - @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): - accept = AcceptEncodingValidHeader("gzip") + accept = AcceptEncoding("gzip") assert accept.quality("gzip") == 1 - accept = AcceptEncodingValidHeader("gzip;q=0.5") + accept = AcceptEncoding("gzip;q=0.5") assert accept.quality("gzip") == 0.5 - @pytest.mark.filterwarnings(IGNORE_QUALITY) + def test_quality_with_identity(self): + accept = AcceptEncoding("gzip;q=0.5") + assert accept.quality("identity") == 1.0 + accept = AcceptEncoding("gzip;q=0.5, identity;q=0") + assert accept.quality("identity") is None + accept = AcceptEncoding("gzip;q=0.5, identity;q=0.2, *;q=0") + assert accept.quality("identity") == 0.2 + assert accept.quality("foo") is None + def test_quality_not_found(self): - accept = AcceptEncodingValidHeader("gzip") + accept = AcceptEncoding("gzip") assert accept.quality("compress") is None + def test___contains__(self): + accept = AcceptEncoding("gzip, compress") + assert "gzip" in accept + assert "deflate" not in accept + for mask in ["*", "gzip", "gZIP"]: + accept = AcceptEncoding(mask) + assert "gzip" in accept + assert "identity" in accept -class TestAcceptEncodingNoHeader: - def test_parse__inherited(self): - returned = AcceptEncodingNoHeader.parse( - value=",,\t gzip;q=1.0, identity; q=0.5, *;q=0 \t ," - ) - list_of_returned = list(returned) - assert list_of_returned == [("gzip", 1.0), ("identity", 0.5), ("*", 0.0)] +class TestAcceptEncoding__missing: def test___init__(self): - instance = AcceptEncodingNoHeader() + instance = AcceptEncoding(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, AcceptEncoding) - def test___add___None(self): - left_operand = AcceptEncodingNoHeader() - result = left_operand + None - assert isinstance(result, AcceptEncodingNoHeader) + def test___bool__(self): + instance = AcceptEncoding(None) + returned = bool(instance) + assert returned is False - @pytest.mark.parametrize("right_operand", [", ", [", "], (", ",), {", ": 1.0}]) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptEncodingNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) + def test___repr__(self): + instance = AcceptEncoding(None) + assert repr(instance) == "" - def test___add___other_type_with_invalid___str__(self): - left_operand = AcceptEncodingNoHeader() + def test___str__(self): + instance = AcceptEncoding(None) + assert str(instance) == "" - class Other: - def __str__(self): - return ", " + def test_copy(self): + instance = AcceptEncoding(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 - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) + def test_acceptable_offers(self): + instance = AcceptEncoding(None) + returned = instance.acceptable_offers(offers=["a", "b", "c"]) + assert returned == [("a", 1.0), ("b", 1.0), ("c", 1.0)] - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___add___valid_empty_value(self, value): - left_operand = AcceptEncodingNoHeader() - result = left_operand + value - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" + def test_best_match(self): + accept = AcceptEncoding(None) + assert accept.best_match(["gzip", "compress"]) == "gzip" + assert accept.best_match(["compress", "gzip"]) == "compress" + assert accept.best_match(["compress", "gzip", "identity"]) == "compress" + assert accept.best_match([], default_match="fallback") == "fallback" - def test___add___other_type_with_valid___str___empty(self): - left_operand = AcceptEncodingNoHeader() + def test_quality(self): + instance = AcceptEncoding(None) + assert instance.quality("content-coding") == 1.0 + assert instance.quality("identity") == 1.0 - class Other: - def __str__(self): - return "" + def test___contains__(self): + instance = AcceptEncoding(None) + returned = "content-coding" in instance + assert returned is True - result = left_operand + Other() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" - @pytest.mark.parametrize( - "value, value_as_header", - [ - ("compress;q=0.5, deflate;q=0, *", "compress;q=0.5, deflate;q=0, *"), - (["compress;q=0.5", "deflate;q=0", "*"], "compress;q=0.5, deflate;q=0, *"), - ( - [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], - "compress;q=0.5, deflate;q=0, *", - ), - (("compress;q=0.5", "deflate;q=0", "*"), "compress;q=0.5, deflate;q=0, *"), - ( - (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), - "compress;q=0.5, deflate;q=0, *", - ), - ( - {"compress": 0.5, "deflate": 0.0, "*": 1.0}, - "*, compress;q=0.5, deflate;q=0", - ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - result = AcceptEncodingNoHeader() + value - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == value_as_header +class TestAcceptEncoding__invalid: + def test___init__(self): + header_value = "invalid header" + instance = AcceptEncoding(header_value) + assert instance.header_state is HeaderState.Invalid + assert instance.header_value == header_value + assert instance.parsed is None - def test___add___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "compress;q=0.5, deflate;q=0, *" + def test___bool__(self): + instance = AcceptEncoding(", ") + returned = bool(instance) + assert returned is False - right_operand = Other() - result = AcceptEncodingNoHeader() + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == str(right_operand) + def test___repr__(self): + instance = AcceptEncoding("\x00") + assert repr(instance) == "" - def test___add___AcceptEncodingValidHeader_header_value_empty(self): - right_operand = AcceptEncodingValidHeader(header_value="") - result = AcceptEncodingNoHeader() + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand + def test___str__(self): + instance = AcceptEncoding(", ") + assert str(instance) == "" - def test___add___AcceptEncodingValidHeader_header_value_not_empty(self): - right_operand = AcceptEncodingValidHeader( - header_value="compress;q=0.5, deflate;q=0, *" - ) - result = AcceptEncodingNoHeader() + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value + def test_copy(self): + instance = AcceptEncoding(", ") + 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___add___AcceptEncodingNoHeader(self): - left_operand = AcceptEncodingNoHeader() - right_operand = AcceptEncodingNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - assert result is not left_operand - assert result is not right_operand + def test_acceptable_offers(self): + instance = AcceptEncoding(", ") + returned = instance.acceptable_offers(offers=["a", "b", "c"]) + assert returned == [("a", 1.0), ("b", 1.0), ("c", 1.0)] - @pytest.mark.parametrize("header_value", [", ", "compress;q=1.001"]) - def test___add___AcceptEncodingInvalidHeader(self, header_value): - left_operand = AcceptEncodingNoHeader() - result = left_operand + AcceptEncodingInvalidHeader(header_value=header_value) - assert isinstance(result, AcceptEncodingNoHeader) - assert result is not left_operand + def test_best_match(self): + accept = AcceptEncoding(", ") + assert accept.best_match(["gzip", "compress"]) == "gzip" + assert accept.best_match(["compress", "gzip"]) == "compress" + assert accept.best_match(["compress", "gzip", "identity"]) == "compress" + assert accept.best_match([], default_match="fallback") == "fallback" - def test___bool__(self): - instance = AcceptEncodingNoHeader() - returned = bool(instance) - assert returned is False + def test_quality(self): + instance = AcceptEncoding(", ") + assert instance.quality("content-coding") == 1.0 + assert instance.quality("identity") == 1.0 - @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): - instance = AcceptEncodingNoHeader() + instance = AcceptEncoding(", ") returned = "content-coding" in instance assert returned is True - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptEncodingNoHeader() - returned = list(instance) - assert returned == [] - def test___radd___None(self): - right_operand = AcceptEncodingNoHeader() - result = None + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("left_operand", [", ", [", "], (", ",), {", ": 1.0}]) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptEncodingNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - assert result is not right_operand +class TestAcceptEncoding__add: + invalid_values = [ + ", ", + [", "], + (", ",), + {", ": 1.0}, + StringMe(", "), + ] - def test___radd___other_type_with_invalid___str__(self): - right_operand = AcceptEncodingNoHeader() + valid_nonempty_values_with_headers = [ + ( + "compress;q=0.5, deflate;q=0, *", + "compress;q=0.5, deflate;q=0, *", + ), + ( + ["compress;q=0.5", "deflate;q=0", "*"], + "compress;q=0.5, deflate;q=0, *", + ), + ( + [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], + "compress;q=0.5, deflate;q=0, *", + ), + ( + ("compress;q=0.5", "deflate;q=0", "*"), + "compress;q=0.5, deflate;q=0, *", + ), + ( + (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), + "compress;q=0.5, deflate;q=0, *", + ), + ( + {"compress": 0.5, "deflate": 0.0, "*": 1.0}, + "*, compress;q=0.5, deflate;q=0", + ), + ] - class Other: - def __str__(self): - return ", " + valid_empty_values = ["", [], (), {}, StringMe("")] - result = Other() + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - assert result is not right_operand + valid_values_with_headers = valid_nonempty_values_with_headers + [ + [x, ""] for x in valid_empty_values + ] - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___radd___valid_empty_value(self, value): - result = value + AcceptEncodingNoHeader() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" + # 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, + } + ) - def test___radd___other_type_with_valid___str___empty(self): - class Other: - def __str__(self): - return "" + # 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 AcceptEncoding - result = Other() + AcceptEncodingNoHeader() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" + # 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( - "value, value_as_header", - [ - ("compress;q=0.5, deflate;q=0, *", "compress;q=0.5, deflate;q=0, *"), - (["compress;q=0.5", "deflate;q=0", "*"], "compress;q=0.5, deflate;q=0, *"), - ( - [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], - "compress;q=0.5, deflate;q=0, *", - ), - (("compress;q=0.5", "deflate;q=0", "*"), "compress;q=0.5, deflate;q=0, *"), - ( - (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), - "compress;q=0.5, deflate;q=0, *", - ), - ( - {"compress": 0.5, "deflate": 0.0, "*": 1.0}, - "*, compress;q=0.5, deflate;q=0", - ), - ], + "input_value, input_header", + valid_values_with_headers, ) - def test___radd___valid_non_empty_value(self, value, value_as_header): - result = value + AcceptEncodingNoHeader() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == value_as_header - - def test___radd___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "compress;q=0.5, deflate;q=0, *" - - left_operand = Other() - result = left_operand + AcceptEncodingNoHeader() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == str(left_operand) - - def test___repr__(self): - instance = AcceptEncodingNoHeader() - assert repr(instance) == "" - - def test___str__(self): - instance = AcceptEncodingNoHeader() - assert str(instance) == "" - - def test_acceptable_offers(self): - instance = AcceptEncodingNoHeader() - returned = instance.acceptable_offers(offers=["a", "b", "c"]) - assert returned == [("a", 1.0), ("b", 1.0), ("c", 1.0)] - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self): - accept = AcceptEncodingNoHeader() - assert accept.best_match(["gzip", "compress"]) == "gzip" - assert accept.best_match([("gzip", 1), ("compress", 0.5)]) == "gzip" - assert accept.best_match([("gzip", 0.5), ("compress", 1)]) == "compress" - assert accept.best_match([("gzip", 0.5), "compress"]) == "compress" - assert ( - accept.best_match([("gzip", 0.5), "compress"], default_match=True) - == "compress" - ) - assert ( - accept.best_match([("gzip", 0.5), "compress"], default_match=False) - == "compress" - ) - assert accept.best_match([], default_match="fallback") == "fallback" - - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - instance = AcceptEncodingNoHeader() - returned = instance.quality(offer="content-coding") - assert returned == 1.0 - + def test_valid_add_missing(self, input_value, input_header, maker, fn): + inst = AcceptEncoding(input_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == input_header -class TestAcceptEncodingInvalidHeader: - def test_parse__inherited(self): - returned = AcceptEncodingInvalidHeader.parse( - value=",,\t gzip;q=1.0, identity; q=0.5, *;q=0 \t ," - ) - list_of_returned = list(returned) - assert list_of_returned == [("gzip", 1.0), ("identity", 0.5), ("*", 0.0)] + 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___init__(self): - header_value = "invalid header" - instance = AcceptEncodingInvalidHeader(header_value=header_value) - assert instance.header_value == header_value - assert instance.parsed is None - assert instance._parsed_nonzero is None - assert isinstance(instance, AcceptEncoding) + def test_invalid_add_missing(self, maker, fn): + inst = AcceptEncoding(", ") + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == ", " - def test___add___None(self): - left_operand = AcceptEncodingInvalidHeader(header_value=", ") - result = left_operand + None - assert isinstance(result, AcceptEncodingNoHeader) + 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("right_operand", [", ", [", "], (", ",), {", ": 1.0}]) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptEncodingInvalidHeader(header_value="invalid header") - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) + def test_missing_add_missing(self, maker, fn): + inst = AcceptEncoding(None) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Missing + assert inst.header_value is None - def test___add___other_type_with_invalid___str__(self): - left_operand = AcceptEncodingInvalidHeader(header_value="invalid header") + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None - class Other: - def __str__(self): - return ", " + @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 = AcceptEncoding(valid_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == valid_header - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) + 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("value", ["", [], (), {}]) - def test___add___valid_empty_value(self, value): - left_operand = AcceptEncodingInvalidHeader(header_value=", ") - result = left_operand + value - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_invalid_add_invalid(self, invalid_value, maker, fn): + inst = AcceptEncoding(", ") + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == ", " - def test___add___other_type_with_valid___str___empty(self): - left_operand = AcceptEncodingInvalidHeader(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 - class Other: - def __str__(self): - return "" + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_missing_add_invalid(self, invalid_value, maker, fn): + inst = AcceptEncoding(None) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Missing + assert inst.header_value is None - result = left_operand + Other() - assert isinstance(result, AcceptEncodingValidHeader) - assert result.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( - "value, value_as_header", - [ - ("compress;q=0.5, deflate;q=0, *", "compress;q=0.5, deflate;q=0, *"), - (["compress;q=0.5", "deflate;q=0", "*"], "compress;q=0.5, deflate;q=0, *"), - ( - [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], - "compress;q=0.5, deflate;q=0, *", - ), - (("compress;q=0.5", "deflate;q=0", "*"), "compress;q=0.5, deflate;q=0, *"), - ( - (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), - "compress;q=0.5, deflate;q=0, *", - ), - ( - {"compress": 0.5, "deflate": 0.0, "*": 1.0}, - "*, compress;q=0.5, deflate;q=0", - ), - ], + "input_value, input_header", + valid_nonempty_values_with_headers, ) - def test___add___valid_value(self, value, value_as_header): - result = AcceptEncodingInvalidHeader(header_value=", ") + value - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == value_as_header - - def test___add___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "*, compress;q=0.5, deflate;q=0" - - right_operand = Other() - result = AcceptEncodingInvalidHeader(header_value=", ") + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == str(right_operand) - - def test___add___AcceptEncodingValidHeader_header_value_empty(self): - left_operand = AcceptEncodingInvalidHeader(header_value=", ") - right_operand = AcceptEncodingValidHeader(header_value="") - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___add___AcceptEncodingValidHeader_header_value_not_empty(self): - left_operand = AcceptEncodingInvalidHeader(header_value=", ") - right_operand = AcceptEncodingValidHeader( - header_value="compress;q=0.5, deflate;q=0, *" - ) - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == right_operand.header_value - - def test___add___AcceptEncodingNoHeader(self): - left_operand = AcceptEncodingInvalidHeader(header_value=", ") - right_operand = AcceptEncodingNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("header_value", [", ", "compress;q=1.001"]) - def test___add___AcceptEncodingInvalidHeader(self, header_value): - result = AcceptEncodingInvalidHeader( - header_value="gzip;;q=1" - ) + AcceptEncodingInvalidHeader(header_value=header_value) - assert isinstance(result, AcceptEncodingNoHeader) - - def test___bool__(self): - instance = AcceptEncodingInvalidHeader(header_value=", ") - returned = bool(instance) - assert returned is False - - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - instance = AcceptEncodingInvalidHeader(header_value=", ") - returned = "content-coding" in instance - assert returned is True - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptEncodingInvalidHeader(header_value=", ") - returned = list(instance) - assert returned == [] - - def test___radd___None(self): - right_operand = AcceptEncodingInvalidHeader(header_value=", ") - result = None + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - - @pytest.mark.parametrize("left_operand", [", ", [", "], (", ",), {", ": 1.0}]) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptEncodingInvalidHeader(header_value="gzip;q= 1") - result = left_operand + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - - def test___radd___other_type_with_invalid___str__(self): - right_operand = AcceptEncodingInvalidHeader(header_value="gzip;q= 1") - - class Other: - def __str__(self): - return ", " - - result = Other() + right_operand - assert isinstance(result, AcceptEncodingNoHeader) - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___radd___valid_empty_value(self, value): - right_operand = AcceptEncodingInvalidHeader(header_value=", ") - result = value + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" - - def test___radd___other_type_with_valid___str___empty(self): - right_operand = AcceptEncodingInvalidHeader(header_value=", ") + def test_nonempty_valid_add_valid(self, input_value, input_header, maker): + seed_value = ",\t ,gzip, identity;q=0.333," + inst = AcceptEncoding(seed_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == seed_value - class Other: - def __str__(self): - return "" + 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 = Other() + right_operand - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == "" + 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 @pytest.mark.parametrize( - "value, value_as_header", - [ - ("compress;q=0.5, deflate;q=0, *", "compress;q=0.5, deflate;q=0, *"), - (["compress;q=0.5", "deflate;q=0", "*"], "compress;q=0.5, deflate;q=0, *"), - ( - [("compress", 0.5), ("deflate", 0.0), ("*", 1.0)], - "compress;q=0.5, deflate;q=0, *", - ), - (("compress;q=0.5", "deflate;q=0", "*"), "compress;q=0.5, deflate;q=0, *"), - ( - (("compress", 0.5), ("deflate", 0.0), ("*", 1.0)), - "compress;q=0.5, deflate;q=0, *", - ), - ( - {"compress": 0.5, "deflate": 0.0, "*": 1.0}, - "*, compress;q=0.5, deflate;q=0", - ), - ], + "input_value, input_header", + valid_nonempty_values_with_headers, ) - def test___radd___valid_non_empty_value(self, value, value_as_header): - result = value + AcceptEncodingInvalidHeader(header_value=", ") - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == value_as_header - - def test___radd___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "compress;q=0.5, deflate;q=0, *" - - left_operand = Other() - result = left_operand + AcceptEncodingInvalidHeader(header_value=", ") - assert isinstance(result, AcceptEncodingValidHeader) - assert result.header_value == str(left_operand) - - def test___repr__(self): - instance = AcceptEncodingInvalidHeader(header_value="\x00") - assert repr(instance) == "" - - def test___str__(self): - instance = AcceptEncodingInvalidHeader(header_value=", ") - assert str(instance) == "" + @pytest.mark.parametrize("empty_value", valid_empty_values) + def test_nonempty_valid_add_empty( + self, input_value, input_header, empty_value, maker, fn + ): + inst = AcceptEncoding(input_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == input_header - def test_acceptable_offers(self): - instance = AcceptEncodingInvalidHeader(header_value=", ") - returned = instance.acceptable_offers(offers=["a", "b", "c"]) - assert returned == [("a", 1.0), ("b", 1.0), ("c", 1.0)] + result = fn(inst, maker(empty_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == input_header - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self): - accept = AcceptEncodingInvalidHeader(header_value=", ") - assert accept.best_match(["gzip", "compress"]) == "gzip" - assert accept.best_match([("gzip", 1), ("compress", 0.5)]) == "gzip" - assert accept.best_match([("gzip", 0.5), ("compress", 1)]) == "compress" - assert accept.best_match([("gzip", 0.5), "compress"]) == "compress" - assert ( - accept.best_match([("gzip", 0.5), "compress"], default_match=True) - == "compress" - ) - assert ( - accept.best_match([("gzip", 0.5), "compress"], default_match=False) - == "compress" - ) - assert accept.best_match([], default_match="fallback") == "fallback" + @pytest.mark.parametrize("empty_value", valid_empty_values) + def test_empty_valid_add_empty(self, empty_value, maker, fn): + expected_value = "" + inst = AcceptEncoding(empty_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == expected_value - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - instance = AcceptEncodingInvalidHeader(header_value=", ") - returned = instance.quality(offer="content-coding") - assert returned == 1.0 + result = fn(inst, maker(empty_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == expected_value class TestCreateAcceptEncodingHeader: def test_header_value_is_None(self): header_value = None - returned = create_accept_encoding_header(header_value=header_value) - assert isinstance(returned, AcceptEncodingNoHeader) + returned = create_accept_encoding_header(header_value) + assert isinstance(returned, AcceptEncoding) + assert returned.header_state is HeaderState.Missing assert returned.header_value == header_value returned2 = create_accept_encoding_header(returned) - assert returned2 is not returned + assert isinstance(returned2, AcceptEncoding) + assert returned2.header_state is HeaderState.Missing assert returned2._header_value == returned._header_value def test_header_value_is_valid(self): header_value = "gzip, identity;q=0.9" - returned = create_accept_encoding_header(header_value=header_value) - assert isinstance(returned, AcceptEncodingValidHeader) + returned = create_accept_encoding_header(header_value) + assert isinstance(returned, AcceptEncoding) + assert returned.header_state is HeaderState.Valid assert returned.header_value == header_value returned2 = create_accept_encoding_header(returned) - assert returned2 is not returned + assert isinstance(returned2, AcceptEncoding) + assert returned2.header_state is HeaderState.Valid assert returned2._header_value == returned._header_value @pytest.mark.parametrize("header_value", [", ", "gzip;q= 1"]) def test_header_value_is_invalid(self, header_value): - returned = create_accept_encoding_header(header_value=header_value) - assert isinstance(returned, AcceptEncodingInvalidHeader) + returned = create_accept_encoding_header(header_value) + assert isinstance(returned, AcceptEncoding) + assert returned.header_state is HeaderState.Invalid assert returned.header_value == header_value returned2 = create_accept_encoding_header(returned) - assert returned2 is not returned + assert isinstance(returned2, AcceptEncoding) + assert returned2.header_state is HeaderState.Invalid assert returned2._header_value == returned._header_value @@ -2961,39 +2537,45 @@ def test_fget_header_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": None}) property_ = accept_encoding_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptEncodingNoHeader) + assert isinstance(returned, AcceptEncoding) + assert returned.header_state is HeaderState.Missing def test_fget_header_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": "gzip"}) property_ = accept_encoding_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptEncodingValidHeader) + assert isinstance(returned, AcceptEncoding) + assert returned.header_state is HeaderState.Valid def test_fget_header_is_invalid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": ", "}) property_ = accept_encoding_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptEncodingInvalidHeader) + assert isinstance(returned, AcceptEncoding) + assert returned.header_state is HeaderState.Invalid def test_fset_value_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": "gzip"}) property_ = accept_encoding_property() property_.fset(request=request, value=None) - assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Missing assert "HTTP_ACCEPT_ENCODING" not in request.environ def test_fset_value_is_invalid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": "gzip"}) property_ = accept_encoding_property() property_.fset(request=request, value=", ") - assert isinstance(request.accept_encoding, AcceptEncodingInvalidHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Invalid assert request.environ["HTTP_ACCEPT_ENCODING"] == ", " def test_fset_value_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": "gzip"}) property_ = accept_encoding_property() property_.fset(request=request, value="compress") - assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_ENCODING"] == "compress" @pytest.mark.parametrize( @@ -3018,58 +2600,60 @@ def test_fset_value_types(self, value, value_as_header): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": ""}) property_ = accept_encoding_property() property_.fset(request=request, value=value) - assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_ENCODING"] == value_as_header def test_fset_other_type_with_valid___str__(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": ""}) property_ = accept_encoding_property() - - class Other: - def __str__(self): - return "gzip;q=0.5, compress;q=0, deflate" - - value = Other() + value = StringMe("gzip;q=0.5, compress;q=0, deflate") property_.fset(request=request, value=value) - assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_ENCODING"] == str(value) - def test_fset_AcceptEncodingNoHeader(self): + def test_fset_missing_AcceptEncoding(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": ""}) property_ = accept_encoding_property() - header = AcceptEncodingNoHeader() + header = AcceptEncoding(None) property_.fset(request=request, value=header) - assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Missing assert "HTTP_ACCEPT_ENCODING" not in request.environ - def test_fset_AcceptEncodingValidHeader(self): + def test_fset_valid_AcceptEncoding(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": ""}) property_ = accept_encoding_property() - header = AcceptEncodingValidHeader("gzip") + header = AcceptEncoding("gzip") property_.fset(request=request, value=header) - assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_ENCODING"] == header.header_value - def test_fset_AcceptEncodingInvalidHeader(self): + def test_fset_invalid_AcceptEncoding(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": "gzip"}) property_ = accept_encoding_property() - header = AcceptEncodingInvalidHeader(", ") + header = AcceptEncoding(", ") property_.fset(request=request, value=header) - assert isinstance(request.accept_encoding, AcceptEncodingInvalidHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Invalid assert request.environ["HTTP_ACCEPT_ENCODING"] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank("/", environ={"HTTP_ACCEPT_ENCODING": "gzip"}) property_ = accept_encoding_property() property_.fdel(request=request) - assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Missing assert "HTTP_ACCEPT_ENCODING" not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank("/") property_ = accept_encoding_property() property_.fdel(request=request) - assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) + assert isinstance(request.accept_encoding, AcceptEncoding) + assert request.accept_encoding.header_state is HeaderState.Missing assert "HTTP_ACCEPT_ENCODING" not in request.environ diff --git a/tests/test_request.py b/tests/test_request.py index a1fd9831..e9284196 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -8,9 +8,7 @@ from webob.acceptparse import ( Accept, AcceptCharset, - AcceptEncodingInvalidHeader, - AcceptEncodingNoHeader, - AcceptEncodingValidHeader, + AcceptEncoding, AcceptLanguageInvalidHeader, AcceptLanguageNoHeader, AcceptLanguageValidHeader, @@ -786,21 +784,24 @@ def test_accept_charset_valid_header(self): def test_accept_encoding_no_header(self): req = self._makeOne(environ={}) header = req.accept_encoding - assert isinstance(header, AcceptEncodingNoHeader) + assert isinstance(header, AcceptEncoding) + assert header.header_state is AcceptHeaderState.Missing assert header.header_value is None @pytest.mark.parametrize("header_value", [", ", ", gzip;q=0.2, compress;q =0.3"]) def test_accept_encoding_invalid_header(self, header_value): req = self._makeOne(environ={"HTTP_ACCEPT_ENCODING": header_value}) header = req.accept_encoding - assert isinstance(header, AcceptEncodingInvalidHeader) + assert isinstance(header, AcceptEncoding) + assert header.header_state is AcceptHeaderState.Invalid assert header.header_value == header_value def test_accept_encoding_valid_header(self): header_value = "compress;q=0.372,gzip;q=0.977,, *;q=0.000" req = self._makeOne(environ={"HTTP_ACCEPT_ENCODING": header_value}) header = req.accept_encoding - assert isinstance(header, AcceptEncodingValidHeader) + assert isinstance(header, AcceptEncoding) + assert header.header_state is AcceptHeaderState.Valid assert header.header_value == header_value # accept_language