diff --git a/CHANGES.txt b/CHANGES.txt index 118120fd..a3ea17e6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,6 +25,16 @@ Feature content-negotiation is needed versus using ``acceptable_offers`` directly. See https://github.com/Pylons/webob/pull/460 +- Consolidation of ``AcceptCharset`` header handling into a single class. + See backward incompatibilities below for more information. + See https://github.com/Pylons/webob/pull/461 + +- ``webob.acceptparse.AcceptCharset``, methods ``best_match``, ``quality``, and + ``__contains__`` are now convenience methods for most use-cases where + content-negotiation is needed versus using ``acceptable_offers`` directly. + See backward incompatibilities below. + See https://github.com/Pylons/webob/pull/461 + Compatibility ~~~~~~~~~~~~~ @@ -51,6 +61,22 @@ Backwards Incompatibilities Their logic is consistent with ``acceptable_offers``. See https://github.com/Pylons/webob/pull/460 +- Remove ``AcceptCharsetValidHeader``, ``AcceptCharsetNoHeader`` and + ``AcceptCharsetInvalidHeader``. These classes are consolidated into + ``AcceptCharset`` with a ``header_state`` attribute for users that need + to know the state of the header. + See https://github.com/Pylons/webob/pull/461 + +- Remove previously-deprecated ``webob.acceptparse.AcceptCharset.__iter__``. + See https://github.com/Pylons/webob/pull/461 + +- ``webob.acceptparse.AcceptCharset`` methods, ``best_match``, ``quality``, + and ``__contains__`` properly take into account the full header, including + quality values of 0 prior to determining a match. Previously they would allow + wildcards to match and override explicit ``q=0`` clauses. Their logic is now + consistent with ``acceptable_offers``. + See https://github.com/Pylons/webob/pull/461 + Experimental Features ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/api/webob.txt b/docs/api/webob.txt index aa1f1734..29a62096 100644 --- a/docs/api/webob.txt +++ b/docs/api/webob.txt @@ -31,26 +31,14 @@ methods: .. autoclass:: Accept :members: parse, header_value, parsed, header_state, __init__, __add__, - __bool__, __radd__, __repr__, __str__, __contains__, + __bool__, __radd__, __repr__, __str__, __contains__, copy, accept_html, accepts_html, acceptable_offers, best_match, quality .. autoclass:: AcceptCharset - :members: parse - -.. autoclass:: AcceptCharsetValidHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - acceptable_offers, best_match, quality - -.. autoclass:: AcceptCharsetNoHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - acceptable_offers, best_match, quality - -.. autoclass:: AcceptCharsetInvalidHeader - :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:: AcceptEncoding :members: parse diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 03b21c86..80e3adf9 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -1041,8 +1041,19 @@ class AcceptCharset: """ Represent an ``Accept-Charset`` header. - Base class for :class:`AcceptCharsetValidHeader`, - :class:`AcceptCharsetNoHeader`, and :class:`AcceptCharsetInvalidHeader`. + A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.3 + <7231#section-5.3.3>`. + + This object should not be modified. To add to the header, we can use the + addition operators (``+`` and ``+=``), which return a new object (see the + docstring for :meth:`.__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.1.2 "Charset": @@ -1058,22 +1069,25 @@ class AcceptCharset: @classmethod def _python_value_to_header_str(cls, value): + if value is None: + return None + 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 @@ -1086,7 +1100,7 @@ def parse(cls, value): :return: If `value` is a valid ``Accept-Charset`` header, returns an iterator of (charset, quality value) tuples, as parsed from the header from left to right. - :raises ValueError: if `value` is an invalid header + :raises ValueError: if ``value`` is an invalid header """ # Check if header is valid # Using Python stdlib's `re` module, there is currently no way to check @@ -1105,49 +1119,45 @@ def generator(value): return generator(value=value) + def __init__(self, header_value): + """ + Create an :class:`AcceptCharset` instance. -class AcceptCharsetValidHeader(AcceptCharset): - """ - Represent a valid ``Accept-Charset`` header. - - A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.3 - <7231#section-5.3.3>`. + :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 = 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:`AcceptCharsetValidHeader.__add__`). - """ + #: Instance of :enum:`.HeaderState` representing the state of + #: the ``Accept-Charset`` 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): - """(``str``) The header value.""" + """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ - (``list``) Parsed form of the header. + (``tuple``) Parsed form of the header. A list of (charset, quality value) tuples. """ return self._parsed - def __init__(self, header_value): - """ - Create an :class:`AcceptCharsetValidHeader` instance. - - :param header_value: (``str``) header value. - :raises ValueError: if `header_value` is an invalid value for an - ``Accept-Charset`` header. - """ - self._header_value = header_value - self._parsed = list(self.parse(header_value)) - self._parsed_nonzero = [ - item for item in self.parsed if item[1] # item[1] is the qvalue - ] - def copy(self): """ Create a copy of the header object. @@ -1155,45 +1165,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``, where keys are charsets and values are qvalues - * a ``tuple`` or ``list``, where each item is a charset ``str`` or a - ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs - can be mixed within the ``tuple`` or ``list``) - * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, - or :class:`AcceptCharsetInvalidHeader` instance - * object of any other type that returns a value for ``__str__`` - - If `other` is a valid header value or another - :class:`AcceptCharsetValidHeader` instance, the two header values are - joined with ``', '``, and a new :class:`AcceptCharsetValidHeader` - instance with the new header value is returned. - - If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an - invalid header value, or an :class:`AcceptCharsetInvalidHeader` - instance, a new :class:`AcceptCharsetValidHeader` instance with the - same header value as ``self`` is returned. - """ - - if isinstance(other, AcceptCharsetValidHeader): - return create_accept_charset_header( - header_value=self.header_value + ", " + other.header_value - ) - - if isinstance(other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)): - return self.__class__(header_value=self.header_value) - - return self._add_instance_and_non_accept_charset_type( - instance=self, other=other - ) - def __bool__(self): """ Return whether ``self`` represents a valid ``Accept-Charset`` header. @@ -1201,91 +1172,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:`AcceptCharsetValidHeader.__contains__` is - currently being maintained for backward compatibility, but it will - change in the future to better conform to the RFC. - - :param offer: (``str``) charset offer - :return: (``bool``) Whether ``offer`` is acceptable according to the - header. - - This does not fully conform to :rfc:`RFC 7231, section 5.3.3 - <7231#section-5.3.3>`: it incorrect interprets ``*`` to mean 'match any - charset in the header', rather than 'match any charset that is not - mentioned elsewhere in the header':: - - >>> 'UTF-8' in AcceptCharsetValidHeader('UTF-8;q=0, *') - True - """ - warnings.warn( - "The behavior of AcceptCharsetValidHeader.__contains__ is " - "currently being maintained for backward compatibility, but it " - "will change in the future to better conform to the RFC.", - DeprecationWarning, - ) - - for mask, _quality in self._parsed_nonzero: - if self._old_match(mask, offer): - return True - - return False - - def __iter__(self): - """ - Return all the items with non-0 qvalues, in order of preference. - - .. warning:: - - The behavior of this method is currently maintained for backward - compatibility, but will change in the future. - - :return: iterator of all the items (charset or ``*``) in the header - with non-0 qvalues, in descending order of qvalue. If two - items have the same qvalue, they are returned in the order of - their positions in the header, from left to right. - - Please note that this is a simple filter for the items in the header - with non-0 qvalues, and is not necessarily the same as what the client - prefers, e.g. ``'utf-7;q=0, *'`` means 'everything but utf-7', but - ``list(instance)`` would return only ``['*']``. - """ - warnings.warn( - "The behavior of AcceptCharsetValidHeader.__iter__ is currently " - "maintained for backward compatibility, but will change in the " - "future.", - DeprecationWarning, - ) - - for 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:`AcceptCharsetValidHeader.__add__`. """ - return self._add_instance_and_non_accept_charset_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""" @@ -1295,52 +1192,16 @@ def __str__(self): utf-8;q=1.000, UTF-7, unicode-1-1;q=0.210 ,'``, ``str(instance)`` returns ``'iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21'``. """ - return ", ".join( - _item_qvalue_pair_to_header_element(pair=tuple_) for tuple_ in self.parsed - ) - - def _add_instance_and_non_accept_charset_type( - self, instance, other, instance_on_the_right=False - ): - if not other: - return self.__class__(header_value=instance.header_value) - other_header_value = self._python_value_to_header_str(value=other) + 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 charset offer matches header item (charset or ``*``). - - .. warning:: - - This is maintained for backward compatibility, and will be - deprecated in the future. - - This method was WebOb's old criterion for deciding whether a charset - matches a header item (charset or ``*``), used in - - - :meth:`AcceptCharsetValidHeader.__contains__` - - :meth:`AcceptCharsetValidHeader.best_match` - - :meth:`AcceptCharsetValidHeader.quality` - - It does not conform to :rfc:`RFC 7231, section 5.3.3 - <7231#section-5.3.3>` in that it does not interpret ``*`` values in the - header correctly: ``*`` should only match charsets not mentioned - elsewhere in the header. - """ - return mask == "*" or offer.lower() == mask.lower() def acceptable_offers(self, offers): """ @@ -1356,8 +1217,12 @@ def acceptable_offers(self, offers): :param offers: ``iterable`` of ``str`` charsets :return: A list of tuples of the form (charset, qvalue), in descending order of qvalue. Where two offers have the same qvalue, they - are returned in the same order as their order in `offers`. + are returned in the same order as their order in ``offers``. """ + + if self.header_state is not HeaderState.Valid: + return [(offer, 1.0) for offer in offers] + lowercased_parsed = [ (charset.lower(), qvalue) for (charset, qvalue) in self.parsed ] @@ -1413,469 +1278,90 @@ def acceptable_offers(self, offers): def best_match(self, offers, default_match=None): """ - Return the best match from the sequence of charset `offers`. + Return the best match from the sequence of charset ``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) - :meth:`AcceptCharsetValidHeader.best_match` has many issues, and - does not conform to :rfc:`RFC 7231 <7231>`. + | Each item in the iterable must be a ``str`` charset - Each charset in `offers` is checked against each non-``q=0`` item - (charset or ``*``) in the header. If the two are a match according to - WebOb's old criterion for a match, the quality value of the match is - the qvalue of the item from the header multiplied by the server quality - value of the offer (if the server quality value is not supplied, it is - 1). + :param default_match: + (optional, any type) the value to be returned if there is no 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. + :return: + (``str``, or the type of ``default_match``) - :param offers: (iterable) + | 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``. - | Each item in the iterable may be a ``str`` charset, or - a (charset, server quality value) ``tuple`` or - ``list``. (The two may be mixed in the iterable.) + .. versionchanged:: 2.0 - :param default_match: (optional, any type) the value to be returned if - there is no match + - 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. + """ + matches = self.acceptable_offers(offers) + if matches: + return matches[0][0] + return default_match - :return: (``str``, or the type of `default_match`) + def quality(self, offer): + """ + Return quality value of given ``offer``, or ``None`` if there is no match. - | The offer that is the best match. If there is no match, the - value of `default_match` is returned. + This is a thin wrapper around :meth:`.acceptable_offers` that matches + a specific ``offer``. - The algorithm behind this method was written for the ``Accept`` header - rather than the ``Accept-Charset`` header. It uses the old criterion of - a match in :meth:`AcceptCharsetValidHeader._old_match`, which does not - conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that - it does not interpret ``*`` values in the header correctly: ``*`` - should only match charsets not mentioned elsewhere in the header:: + :param offer: (``str``) charset offer + :return: (``float`` or ``None``) - >>> AcceptCharsetValidHeader('utf-8;q=0, *').best_match(['utf-8']) - 'utf-8' - """ - warnings.warn( - "The behavior of AcceptCharsetValidHeader.best_match is currently" - " being maintained for backward compatibility, but it will be " - "deprecated in the future, as it does not conform to the RFC.", - DeprecationWarning, - ) - best_quality = -1 - best_offer = default_match - matched_by = "*/*" - for offer in offers: - if isinstance(offer, (tuple, list)): - offer, server_quality = offer - else: - server_quality = 1 - for mask, quality in self._parsed_nonzero: - possible_quality = server_quality * quality - if possible_quality < best_quality: - continue - elif possible_quality == best_quality: - # 'text/plain' overrides 'message/*' overrides '*/*' - # (if all match w/ the same q=) - # [We can see that this was written for the Accept header, - # not the Accept-Charset header.] - if matched_by.count("*") <= mask.count("*"): - continue - if self._old_match(mask, offer): - best_quality = possible_quality - best_offer = offer - matched_by = mask - return best_offer - - def quality(self, offer): - """ - Return quality value of given offer, or ``None`` if there is no match. - - .. warning:: - - This is currently maintained for backward compatibility, and will be - deprecated in the future. - - :param offer: (``str``) charset offer - :return: (``float`` or ``None``) - - | The quality value from the charset that matches the `offer`, - or ``None`` if there is no match. - - This uses the old criterion of a match in - :meth:`AcceptCharsetValidHeader._old_match`, which does not conform to - :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that it does - not interpret ``*`` values in the header correctly: ``*`` should only - match charsets not mentioned elsewhere in the header:: - - >>> AcceptCharsetValidHeader('utf-8;q=0, *').quality('utf-8') - 1.0 - >>> AcceptCharsetValidHeader('utf-8;q=0.9, *').quality('utf-8') - 1.0 - """ - warnings.warn( - "The behavior of AcceptCharsetValidHeader.quality is currently " - "being maintained for backward compatibility, but it will be " - "deprecated in the future, as it does not conform to the RFC.", - DeprecationWarning, - ) - bestq = 0 - for mask, quality in self.parsed: - if self._old_match(mask, offer): - bestq = max(bestq, quality) - return bestq or None - - -class _AcceptCharsetInvalidOrNoHeader(AcceptCharset): - """ - Represent when an ``Accept-Charset`` header is invalid or not in request. - - This is the base class for the behaviour that - :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader` - have in common. - - :rfc:`7231` does not provide any guidance on what should happen if the - ``Accept-Charset`` header has an invalid value. This implementation - disregards the header when the header is invalid, so - :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader` - have much behaviour in common. - """ - - def __bool__(self): - """ - Return whether ``self`` represents a valid ``Accept-Charset`` header. + | The highest quality value from the charsets that match + the `offer`, or ``None`` if there is no match. - 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. """ - 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 ``AcceptCharset`` 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``) charset offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. - For this class, either there is no ``Accept-Charset`` header in the - request, or the header is invalid, so any charset is acceptable, and - this always returns ``True``. - """ - warnings.warn( - "The behavior of .__contains__ for the AcceptCharset classes is " - "currently being maintained for backward compatibility, but it " - "will change in the future to better conform to the RFC.", - DeprecationWarning, - ) - return True - - def __iter__(self): - """ - Return all the items with non-0 qvalues, in order of preference. - - .. warning:: - - The behavior of this method is currently maintained for backward - compatibility, but will change in the future. - - :return: iterator of all the items (charset or ``*``) in the header - with non-0 qvalues, in descending order of qvalue. If two - items have the same qvalue, they are returned in the order of - their positions in the header, from left to right. - - When there is no ``Accept-Charset`` header in the request or the header - is invalid, there are no items, and this always returns an empty - iterator. - """ - warnings.warn( - "The behavior of AcceptCharsetValidHeader.__iter__ is currently " - "maintained for backward compatibility, but will change in the " - "future.", - DeprecationWarning, - ) - return iter(()) - - def acceptable_offers(self, offers): - """ - Return the offers that are acceptable according to the header. - - The offers are returned in descending order of preference, where - preference is indicated by the qvalue of the charset or ``*`` in the - header matching the offer. - - This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3 - <7231#section-5.3.3>`. - - :param offers: ``iterable`` of ``str`` charsets - :return: A list of tuples of the form (charset, qvalue), in descending - order of qvalue. Where two offers have the same qvalue, they - are returned in the same order as their order in `offers`. - - | When the header is invalid or there is no ``Accept-Charset`` - header in the request, all `offers` are considered - acceptable, so this method returns a list of (charset, - qvalue) tuples where each offer in `offers` is paired with - the qvalue of 1.0, in the same order as `offers`. - """ - return [(offer, 1.0) for offer in offers] - - def best_match(self, offers, default_match=None): - """ - Return the best match from the sequence of charset `offers`. - - This is the ``.best_match()`` method for when the header is invalid or - not found in the request, corresponding to - :meth:`AcceptCharsetValidHeader.best_match`. - - .. warning:: - - This is currently maintained for backward compatibility, and will be - deprecated in the future (see the documentation for - :meth:`AcceptCharsetValidHeader.best_match`). - - When the header is invalid, or there is no `Accept-Charset` header in - the request, all the charsets in `offers` are considered acceptable, so - the best match is the charset in `offers` with the highest server - quality value (if the server quality value is not supplied, it is 1). - - If more than one charsets in `offers` have the same highest server - quality value, then the one that shows up first in `offers` is the best - match. - - :param offers: (iterable) - - | Each item in the iterable may be a ``str`` charset, or - a (charset, server quality value) ``tuple`` or - ``list``. (The two may be mixed in the iterable.) - - :param default_match: (optional, any type) the value to be returned if - `offers` is empty. - - :return: (``str``, or the type of `default_match`) - - | The charset that has the highest server quality value. If - `offers` is empty, the value of `default_match` is returned. - """ - warnings.warn( - "The behavior of .best_match for the AcceptCharset classes is " - "currently being maintained for backward compatibility, but the " - "method will be deprecated in the future, as its behavior is not " - "specified in (and currently does not conform to) RFC 7231.", - DeprecationWarning, - ) - best_quality = -1 - best_offer = default_match - for offer in offers: - if isinstance(offer, (list, tuple)): - offer, quality = offer - else: - quality = 1 - if quality > best_quality: - best_offer = offer - best_quality = quality - return best_offer - - def quality(self, offer): - """ - Return quality value of given offer, or ``None`` if there is no match. - - This is the ``.quality()`` method for when the header is invalid or not - found in the request, corresponding to - :meth:`AcceptCharsetValidHeader.quality`. - - .. warning:: - - This is currently maintained for backward compatibility, and will be - deprecated in the future (see the documentation for - :meth:`AcceptCharsetValidHeader.quality`). - - :param offer: (``str``) charset offer - :return: (``float``) ``1.0``. - - When the ``Accept-Charset`` header is invalid or not in the request, - all offers are equally acceptable, so 1.0 is always returned. - """ - warnings.warn( - "The behavior of .quality for the Accept-Charset classes is " - "currently being maintained for backward compatibility, but the " - "method will be deprecated in the future, as its behavior does not" - " conform to RFC 7231.", - DeprecationWarning, - ) - return 1.0 - - -class AcceptCharsetNoHeader(_AcceptCharsetInvalidOrNoHeader): - """ - Represent when there is no ``Accept-Charset`` header in the request. - - This object should not be modified. To add to the header, we can use the - addition operators (``+`` and ``+=``), which return a new object (see the - docstring for :meth:`AcceptCharsetNoHeader.__add__`). - """ - - @property - def header_value(self): - """ - (``str`` or ``None``) The header value. - - As there is no header in the request, this is ``None``. - """ - return self._header_value - - @property - def parsed(self): - """ - (``list`` or ``None``) Parsed form of the header. - - As there is no header in the request, this is ``None``. - """ - return self._parsed - - def __init__(self): - """ - Create an :class:`AcceptCharsetNoHeader` instance. - """ - self._header_value = None - self._parsed = None - self._parsed_nonzero = None - - def copy(self): - """ - Create a copy of the header object. - - """ - return self.__class__() - - def __add__(self, other): - """ - Add to header, creating a new header object. - - `other` can be: - - * ``None`` - * a ``str`` header value - * a ``dict``, where keys are charsets and values are qvalues - * a ``tuple`` or ``list``, where each item is a charset ``str`` or a - ``tuple`` or ``list`` (charset, qvalue) pair (``str`` and pairs - can be mixed within the ``tuple`` or ``list``) - * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, - or :class:`AcceptCharsetInvalidHeader` instance - * object of any other type that returns a value for ``__str__`` - - If `other` is a valid header value or an - :class:`AcceptCharsetValidHeader` instance, a new - :class:`AcceptCharsetValidHeader` instance with the valid header value - is returned. - - If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an - invalid header value, or an :class:`AcceptCharsetInvalidHeader` - instance, a new :class:`AcceptCharsetNoHeader` instance is returned. - """ - if isinstance(other, AcceptCharsetValidHeader): - return AcceptCharsetValidHeader(header_value=other.header_value) - - if isinstance(other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)): - return self.__class__() - - return self._add_instance_and_non_accept_charset_type( - instance=self, other=other - ) - - def __radd__(self, other): - """ - Add to header, creating a new header object. - - See the docstring for :meth:`AcceptCharsetNoHeader.__add__`. - """ - return self.__add__(other=other) - - def __repr__(self): - return f"<{self.__class__.__name__}>" - - def __str__(self): - """Return the ``str`` ``''``.""" - - return "" - - def _add_instance_and_non_accept_charset_type(self, instance, other): - if not other: - return self.__class__() - - other_header_value = self._python_value_to_header_str(value=other) - - try: - return AcceptCharsetValidHeader(header_value=other_header_value) - except ValueError: # invalid header value - return self.__class__() - - -class AcceptCharsetInvalidHeader(_AcceptCharsetInvalidOrNoHeader): - """ - Represent an invalid ``Accept-Charset`` header. - - An invalid header is one that does not conform to - :rfc:`7231#section-5.3.3`. As specified in the RFC, an empty header is an - invalid ``Accept-Charset`` header. - - :rfc:`7231` does not provide any guidance on what should happen if the - ``Accept-Charset`` header has an invalid value. This implementation - disregards the header, and treats it as if there is no ``Accept-Charset`` - header in the request. - - This object should not be modified. To add to the header, we can use the - addition operators (``+`` and ``+=``), which return a new object (see the - docstring for :meth:`AcceptCharsetInvalidHeader.__add__`). - """ - - @property - def header_value(self): - """(``str`` or ``None``) The header value.""" - - return self._header_value - - @property - def parsed(self): - """ - (``list`` or ``None``) Parsed form of the header. - - As the header is invalid and cannot be parsed, this is ``None``. - """ - - return self._parsed - - def __init__(self, header_value): - """ - Create an :class:`AcceptCharsetInvalidHeader` instance. - """ - self._header_value = header_value - self._parsed = None - self._parsed_nonzero = None + .. 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. """ - Create a copy of the header object. - """ - return self.__class__(self._header_value) + return self.quality(offer) is not None def __add__(self, other): """ Add to header, creating a new header object. - `other` can be: + ``other`` can be: * ``None`` * a ``str`` header value @@ -1883,89 +1369,50 @@ def __add__(self, other): * a ``tuple`` or ``list``, where each item is a charset ``str`` or a ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) - * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, - or :class:`AcceptCharsetInvalidHeader` instance + * an :class:`AcceptCharset` instance * object of any other type that returns a value for ``__str__`` - If `other` is a valid header value or an - :class:`AcceptCharsetValidHeader` instance, a new - :class:`AcceptCharsetValidHeader` instance with the valid header value - is returned. - - If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an - invalid header value, or an :class:`AcceptCharsetInvalidHeader` - instance, a new :class:`AcceptCharsetNoHeader` instance is returned. + 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, AcceptCharsetValidHeader): - return AcceptCharsetValidHeader(header_value=other.header_value) - - if isinstance(other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)): - return AcceptCharsetNoHeader() + other = create_accept_charset_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_charset_type( - instance=self, other=other - ) + if is_self_valid: + if is_other_valid: + return create_accept_charset_header( + self.header_value + ", " + other.header_value + ) + return self + elif is_other_valid: + return other + return create_accept_charset_header(None) def __radd__(self, other): """ Add to header, creating a new header object. - See the docstring for :meth:`AcceptCharsetValidHeader.__add__`. + See the docstring for :meth:`.__add__`. """ - return self._add_instance_and_non_accept_charset_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_charset_type( - self, instance, other, instance_on_the_right=False - ): - if not other: - return AcceptCharsetNoHeader() - - other_header_value = self._python_value_to_header_str(value=other) - - try: - return AcceptCharsetValidHeader(header_value=other_header_value) - except ValueError: # invalid header value - return AcceptCharsetNoHeader() + other = create_accept_charset_header(other) + return other + self def create_accept_charset_header(header_value): """ Create an object representing the ``Accept-Charset`` header in a request. - :param header_value: (``str``) header value - :return: If `header_value` is ``None``, an :class:`AcceptCharsetNoHeader` - instance. - - | If `header_value` is a valid ``Accept-Charset`` header, an - :class:`AcceptCharsetValidHeader` instance. - - | If `header_value` is an invalid ``Accept-Charset`` header, an - :class:`AcceptCharsetInvalidHeader` instance. + :param header_value: (``str`` or ``None``) header value + :return: an :class:`AcceptCharset` instance. """ - if header_value is None: - return AcceptCharsetNoHeader() if isinstance(header_value, AcceptCharset): - return header_value.copy() - try: - return AcceptCharsetValidHeader(header_value=header_value) - except ValueError: - return AcceptCharsetInvalidHeader(header_value=header_value) + return header_value + return AcceptCharset(header_value) def accept_charset_property(): @@ -1985,15 +1432,13 @@ def accept_charset_property(): def fget(request): """Get an object representing the header in the request.""" - return create_accept_charset_header( - header_value=request.environ.get(ENVIRON_KEY) - ) + return create_accept_charset_header(request.environ.get(ENVIRON_KEY)) def fset(request, value): """ Set the corresponding key in the request environ. - `value` can be: + ``value`` can be: * ``None`` * a ``str`` header value @@ -2001,21 +1446,17 @@ def fset(request, value): * a ``tuple`` or ``list``, where each item is a charset ``str`` or a ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) - * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, - or :class:`AcceptCharsetInvalidHeader` instance + * an :class:`AcceptCharset` instance * object of any other type that returns a value for ``__str__`` """ - if value is None or isinstance(value, AcceptCharsetNoHeader): - fdel(request=request) + if isinstance(value, AcceptCharset): + value = value.header_value else: - if isinstance( - value, (AcceptCharsetValidHeader, AcceptCharsetInvalidHeader) - ): - header_value = value.header_value - else: - header_value = AcceptCharset._python_value_to_header_str(value=value) - request.environ[ENVIRON_KEY] = header_value + value = AcceptCharset._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.""" @@ -2344,9 +1785,9 @@ def _old_match(self, mask, offer): content-coding offer matches a header item (content-coding, ``identity`` or ``*``), used in - - :meth:`AcceptCharsetValidHeader.__contains__` - - :meth:`AcceptCharsetValidHeader.best_match` - - :meth:`AcceptCharsetValidHeader.quality` + - :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 diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index c41b5964..46ec4754 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -7,9 +7,6 @@ from webob.acceptparse import ( Accept, AcceptCharset, - AcceptCharsetInvalidHeader, - AcceptCharsetNoHeader, - AcceptCharsetValidHeader, AcceptEncoding, AcceptEncodingInvalidHeader, AcceptEncodingNoHeader, @@ -1385,7 +1382,7 @@ def test_fdel_header_key_not_in_environ(self): assert "HTTP_ACCEPT" not in request.environ -class TestAcceptCharset: +class TestAcceptCharset__parsing: @pytest.mark.parametrize( "value", [ @@ -1446,9 +1443,9 @@ def test_parse__valid_header(self, value, expected_list): assert list_of_returned == expected_list -class TestAcceptCharsetValidHeader: +class TestAcceptCharset__valid: def test_parse__inherited(self): - returned = AcceptCharsetValidHeader.parse( + returned = AcceptCharset.parse( value=",iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90," ) list_of_returned = list(returned) @@ -1458,250 +1455,51 @@ def test_parse__inherited(self): ("unicode-1-1", 0.9), ] - @pytest.mark.parametrize("header_value", ["", ", iso-8859-5 "]) - def test___init___invalid_header(self, header_value): - with pytest.raises(ValueError): - AcceptCharsetValidHeader(header_value=header_value) - def test___init___valid_header(self): header_value = "iso-8859-5;q=0.372,unicode-1-1;q=0.977,UTF-8, *;q=0.000" - instance = AcceptCharsetValidHeader(header_value=header_value) + instance = AcceptCharset(header_value) + assert instance.header_state is HeaderState.Valid assert instance.header_value == header_value - assert instance.parsed == [ + assert instance.parsed == ( ("iso-8859-5", 0.372), ("unicode-1-1", 0.977), ("UTF-8", 1.0), ("*", 0.0), - ] - assert instance._parsed_nonzero == [ - ("iso-8859-5", 0.372), - ("unicode-1-1", 0.977), - ("UTF-8", 1.0), - ] - assert isinstance(instance, AcceptCharset) - - def test___add___None(self): - left_operand = AcceptCharsetValidHeader(header_value="iso-8859-5") - result = left_operand + None - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize( - "right_operand", - ["", [], (), {}, "UTF/8", ["UTF/8"], ("UTF/8",), {"UTF/8": 1.0}], - ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptCharsetValidHeader(header_value="iso-8859-5") - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize("str_", ["", "UTF/8"]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptCharsetValidHeader(header_value="iso-8859-5") - - class Other: - def __str__(self): - return str_ - - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ( - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, - "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", - ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - left_operand = AcceptCharsetValidHeader(header_value=",\t ,iso-8859-5;q=0.333,") - result = left_operand + value - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == left_operand.header_value + ", " + value_as_header - - def test___add___other_type_with_valid___str__(self): - left_operand = AcceptCharsetValidHeader(header_value=",\t ,iso-8859-5;q=0.333,") - - class Other: - def __str__(self): - return "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8" - - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == left_operand.header_value + ", " + str( - right_operand - ) - - def test___add___AcceptCharsetValidHeader(self): - left_operand = AcceptCharsetValidHeader(header_value=",\t ,iso-8859-5;q=0.333,") - right_operand = AcceptCharsetValidHeader( - header_value=", ,utf-7;q=0, \tutf-8;q=1," - ) - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert ( - result.header_value - == left_operand.header_value + ", " + right_operand.header_value - ) - - def test___add___AcceptCharsetNoHeader(self): - valid_header_instance = AcceptCharsetValidHeader( - header_value=", ,utf-7;q=0, \tutf-8;q=1," - ) - result = valid_header_instance + AcceptCharsetNoHeader() - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance - - @pytest.mark.parametrize("header_value", ["", "utf/8"]) - def test___add___AcceptCharsetInvalidHeader(self, header_value): - valid_header_instance = AcceptCharsetValidHeader(header_value="header") - result = valid_header_instance + AcceptCharsetInvalidHeader( - header_value=header_value ) - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance def test___bool__(self): - instance = AcceptCharsetValidHeader(header_value="valid-header") + instance = AcceptCharset("valid-header") returned = bool(instance) assert returned is True - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - for mask in ["*", "utf-8", "UTF-8"]: - assert "utf-8" in AcceptCharsetValidHeader(mask) - assert "utf-8" not in AcceptCharsetValidHeader("utf-7") - - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains___not(self): - accept = AcceptCharsetValidHeader("utf-8") - assert "utf-7" not in accept - - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains___zero_quality(self): - assert "foo" not in AcceptCharsetValidHeader("*;q=0") - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptCharsetValidHeader( - header_value="utf-8; q=0.5, utf-7; q=0, iso-8859-5; q=0.8, unicode-1-1" - ) - assert list(instance) == ["unicode-1-1", "iso-8859-5", "utf-8"] - - def test___radd___None(self): - right_operand = AcceptCharsetValidHeader(header_value="iso-8859-5") - result = None + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "left_operand", ["", [], (), {}, "UTF/8", ["UTF/8"], ("UTF/8",), {"UTF/8": 1.0}] - ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptCharsetValidHeader(header_value="iso-8859-5") - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize("str_", ["", "UTF/8"]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptCharsetValidHeader(header_value="iso-8859-5") - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ( - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, - "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", - ), - ], - ) - def test___radd___valid_value(self, value, value_as_header): - right_operand = AcceptCharsetValidHeader( - header_value=",\t ,iso-8859-5;q=0.333," - ) - result = value + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert ( - result.header_value == value_as_header + ", " + right_operand.header_value - ) - - def test___radd___other_type_with_valid___str__(self): - right_operand = AcceptCharsetValidHeader( - header_value=",\t ,iso-8859-5;q=0.333," - ) - - class Other: - def __str__(self): - return "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8" - - left_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert ( - result.header_value == str(left_operand) + ", " + right_operand.header_value - ) - def test___repr__(self): - instance = AcceptCharsetValidHeader(header_value=",utf-7;q=0.200,UTF-8;q=0.300") - assert ( - repr(instance) == "" - ) + instance = AcceptCharset(",utf-7;q=0.200,UTF-8;q=0.300") + assert repr(instance) == "" def test___str__(self): header_value = ( ", \t,iso-8859-5;q=0.000 \t, utf-8;q=1.000, UTF-7, " "unicode-1-1;q=0.210 ," ) - instance = AcceptCharsetValidHeader(header_value=header_value) + instance = AcceptCharset(header_value) assert str(instance) == "iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21" + def test_copy(self): + instance = AcceptCharset(",iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90,") + 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", ["*", "utf-8", "UTF-8"]) + def test___contains__match(self, header): + assert "utf-8" in AcceptCharset(header) + + def test___contains__not(self): + assert "utf-7" not in AcceptCharset("utf-8") + assert "utf-7" not in AcceptCharset("utf-7;q=0") + @pytest.mark.parametrize( "header_value, offers, returned", [ @@ -1749,518 +1547,329 @@ def test___str__(self): ], ) def test_acceptable_offers(self, header_value, offers, returned): - instance = AcceptCharsetValidHeader(header_value=header_value) + instance = AcceptCharset(header_value) assert instance.acceptable_offers(offers=offers) == returned - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): - accept = AcceptCharsetValidHeader("utf-8, iso-8859-5") + accept = AcceptCharset("utf-8, iso-8859-5") assert accept.best_match(["utf-8", "iso-8859-5"]) == "utf-8" assert accept.best_match(["iso-8859-5", "utf-8"]) == "iso-8859-5" - assert accept.best_match([("iso-8859-5", 0.5), "utf-8"]) == "utf-8" - assert accept.best_match([("iso-8859-5", 0.5), ("utf-8", 0.4)]) == "iso-8859-5" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_one_lower_q(self): - accept = AcceptCharsetValidHeader("utf-8, iso-8859-5;q=0.5") + accept = AcceptCharset("utf-8, iso-8859-5;q=0.5") assert accept.best_match(["utf-8", "iso-8859-5"]) == "utf-8" - accept = AcceptCharsetValidHeader("utf-8;q=0.5, iso-8859-5") + accept = AcceptCharset("utf-8;q=0.5, iso-8859-5") assert accept.best_match(["utf-8", "iso-8859-5"]) == "iso-8859-5" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_complex_q(self): - accept = AcceptCharsetValidHeader("utf-8, iso-8859-5;q=0.55, utf-7;q=0.59") + accept = AcceptCharset("utf-8, iso-8859-5;q=0.55, utf-7;q=0.59") assert accept.best_match(["utf-8", "iso-8859-5"]) == "utf-8" - accept = AcceptCharsetValidHeader( - "utf-8;q=0.5, iso-8859-5;q=0.586, utf-7;q=0.596" - ) + accept = AcceptCharset("utf-8;q=0.5, iso-8859-5;q=0.586, utf-7;q=0.596") assert accept.best_match(["utf-8", "utf-7"]) == "utf-7" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_mixedcase(self): - accept = AcceptCharsetValidHeader("uTf-8; q=0.2, UtF-7; Q=0.4, *; q=0.05") + accept = AcceptCharset("uTf-8; q=0.2, UtF-7; Q=0.4, *; q=0.05") assert accept.best_match(["UtF-8"]) == "UtF-8" assert accept.best_match(["IsO-8859-5"]) == "IsO-8859-5" assert accept.best_match(["iSo-8859-5", "uTF-7", "UtF-8"]) == "uTF-7" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_best_match_zero_quality(self): - assert AcceptCharsetValidHeader("utf-7, *;q=0").best_match(["utf-8"]) is None - assert "char-set" not in AcceptCharsetValidHeader("*;q=0") + assert AcceptCharset("utf-7, *;q=0").best_match(["utf-8"]) is None - @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): - accept = AcceptCharsetValidHeader("utf-8") + accept = AcceptCharset("utf-8") assert accept.quality("utf-8") == 1.0 - accept = AcceptCharsetValidHeader("utf-8;q=0.5") + accept = AcceptCharset("utf-8;q=0.5") assert accept.quality("utf-8") == 0.5 - @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality_not_found(self): - accept = AcceptCharsetValidHeader("utf-8") + accept = AcceptCharset("utf-8") assert accept.quality("iso-8859-5") is None -class TestAcceptCharsetNoHeader: - def test_parse__inherited(self): - returned = AcceptCharsetNoHeader.parse( - value=",iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90," - ) - list_of_returned = list(returned) - assert list_of_returned == [ - ("iso-8859-5", 0.333), - ("utf-8", 1.0), - ("unicode-1-1", 0.9), - ] - +class TestAcceptCharset__missing: def test___init__(self): - instance = AcceptCharsetNoHeader() + instance = AcceptCharset(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, AcceptCharset) - - def test___add___None(self): - instance = AcceptCharsetNoHeader() - result = instance + None - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not instance - - @pytest.mark.parametrize( - "right_operand", - ["", [], (), {}, "UTF/8", ["UTF/8"], ("UTF/8",), {"UTF/8": 1.0}], - ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptCharsetNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not left_operand - - @pytest.mark.parametrize("str_", ["", "UTF/8"]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptCharsetNoHeader() - - class Other: - def __str__(self): - return str_ - - result = left_operand + Other() - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not left_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ( - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, - "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", - ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - result = AcceptCharsetNoHeader() + value - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == value_as_header - - def test___add___other_type_with_valid___str__(self): - class Other: - def __str__(self): - return "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8" - - right_operand = Other() - result = AcceptCharsetNoHeader() + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == str(right_operand) - - def test___add___AcceptCharsetValidHeader(self): - right_operand = AcceptCharsetValidHeader( - header_value=", ,utf-7;q=0, \tutf-8;q=1," - ) - result = AcceptCharsetNoHeader() + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___add___AcceptCharsetNoHeader(self): - left_operand = AcceptCharsetNoHeader() - right_operand = AcceptCharsetNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not left_operand - assert result is not right_operand - - @pytest.mark.parametrize("header_value", ["", "utf/8"]) - def test___add___AcceptCharsetInvalidHeader(self, header_value): - left_operand = AcceptCharsetNoHeader() - result = left_operand + AcceptCharsetInvalidHeader(header_value=header_value) - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not left_operand def test___bool__(self): - instance = AcceptCharsetNoHeader() + instance = AcceptCharset(None) returned = bool(instance) assert returned is False - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - instance = AcceptCharsetNoHeader() - returned = "char-set" in instance - assert returned is True - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptCharsetNoHeader() - returned = list(instance) - assert returned == [] - - def test___radd___None(self): - right_operand = AcceptCharsetNoHeader() - result = None + right_operand - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize( - "left_operand", ["", [], (), {}, "UTF/8", ["UTF/8"], ("UTF/8",), {"UTF/8": 1.0}] - ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptCharsetNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("str_", ["", "UTF/8"]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptCharsetNoHeader() - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - ( - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, - "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", - ), - ], - ) - def test___radd___valid_value(self, value, value_as_header): - result = value + AcceptCharsetNoHeader() - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == value_as_header - - def test___radd___other_type_with_valid___str__(self): - class Other: - def __str__(self): - return "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8" + def test___repr__(self): + instance = AcceptCharset(None) + assert repr(instance) == "" - left_operand = Other() - result = left_operand + AcceptCharsetNoHeader() - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == str(left_operand) + def test___str__(self): + instance = AcceptCharset(None) + assert str(instance) == "" - def test___repr__(self): - instance = AcceptCharsetNoHeader() - assert repr(instance) == "" + def test_copy(self): + instance = AcceptCharset(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___str__(self): - instance = AcceptCharsetNoHeader() - assert str(instance) == "" + def test___contains__(self): + assert "char-set" in AcceptCharset(None) def test_acceptable_offers(self): - instance = AcceptCharsetNoHeader() + instance = AcceptCharset(None) returned = instance.acceptable_offers(offers=["utf-8", "utf-7", "unicode-1-1"]) assert returned == [("utf-8", 1.0), ("utf-7", 1.0), ("unicode-1-1", 1.0)] - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): - accept = AcceptCharsetNoHeader() + accept = AcceptCharset(None) assert accept.best_match(["utf-8", "iso-8859-5"]) == "utf-8" - assert accept.best_match([("utf-8", 1), ("iso-8859-5", 0.5)]) == "utf-8" - assert accept.best_match([("utf-8", 0.5), ("iso-8859-5", 1)]) == "iso-8859-5" - assert accept.best_match([("utf-8", 0.5), "iso-8859-5"]) == "iso-8859-5" - assert ( - accept.best_match([("utf-8", 0.5), "iso-8859-5"], default_match=True) - == "iso-8859-5" - ) - assert ( - accept.best_match([("utf-8", 0.5), "iso-8859-5"], default_match=False) - == "iso-8859-5" - ) + assert accept.best_match(["iso-8859-5", "utf-8"]) == "iso-8859-5" assert accept.best_match([], default_match="fallback") == "fallback" - @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): - instance = AcceptCharsetNoHeader() + instance = AcceptCharset(None) returned = instance.quality(offer="char-set") assert returned == 1.0 -class TestAcceptCharsetInvalidHeader: - def test_parse__inherited(self): - returned = AcceptCharsetInvalidHeader.parse( - value=",iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90," - ) - list_of_returned = list(returned) - assert list_of_returned == [ - ("iso-8859-5", 0.333), - ("utf-8", 1.0), - ("unicode-1-1", 0.9), - ] - +class TestAcceptCharset__invalid: def test___init__(self): header_value = "invalid header" - instance = AcceptCharsetInvalidHeader(header_value=header_value) + instance = AcceptCharset(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, AcceptCharset) - def test___add___None(self): - instance = AcceptCharsetInvalidHeader(header_value="") - result = instance + None - assert isinstance(result, AcceptCharsetNoHeader) + def test___bool__(self): + instance = AcceptCharset("") + returned = bool(instance) + assert returned is False - @pytest.mark.parametrize( - "right_operand", - ["", [], (), {}, "UTF/8", ["UTF/8"], ("UTF/8",), {"UTF/8": 1.0}], - ) - def test___add___invalid_value(self, right_operand): - result = AcceptCharsetInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptCharsetNoHeader) + def test___repr__(self): + instance = AcceptCharset("\x00") + assert repr(instance) == "" - @pytest.mark.parametrize("str_", ["", "UTF/8"]) - def test___add___other_type_with_invalid___str__(self, str_): - class Other: - def __str__(self): - return str_ + def test___str__(self): + instance = AcceptCharset("") + assert str(instance) == "" - result = AcceptCharsetInvalidHeader(header_value="") + Other() - assert isinstance(result, AcceptCharsetNoHeader) + def test_copy(self): + instance = AcceptCharset("") + 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( - "value, value_as_header", - [ - ( - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, - "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", - ), - ], - ) - def test___add___valid_header_value(self, value, value_as_header): - result = AcceptCharsetInvalidHeader(header_value="") + value - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == value_as_header + def test___contains__(self): + assert "char-set" in AcceptCharset(None) - def test___add___other_type_valid_header_value(self): - class Other: - def __str__(self): - return "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8" + def test_acceptable_offers(self): + instance = AcceptCharset("") + returned = instance.acceptable_offers(offers=["utf-8", "utf-7", "unicode-1-1"]) + assert returned == [("utf-8", 1.0), ("utf-7", 1.0), ("unicode-1-1", 1.0)] - right_operand = Other() - result = AcceptCharsetInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == str(right_operand) + def test_best_match(self): + accept = AcceptCharset("") + assert accept.best_match(["utf-8", "iso-8859-5"]) == "utf-8" + assert accept.best_match(["iso-8859-5", "utf-8"]) == "iso-8859-5" + assert accept.best_match([], default_match="fallback") == "fallback" - def test___add___AcceptCharsetValidHeader(self): - right_operand = AcceptCharsetValidHeader( - header_value=", ,utf-7;q=0, \tutf-8;q=1," - ) - result = AcceptCharsetInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand + def test_quality(self): + instance = AcceptCharset("") + returned = instance.quality(offer="char-set") + assert returned == 1.0 - def test___add___AcceptCharsetNoHeader(self): - right_operand = AcceptCharsetNoHeader() - result = AcceptCharsetInvalidHeader(header_value="") + right_operand - assert isinstance(result, AcceptCharsetNoHeader) - assert result is not right_operand - def test___add___AcceptCharsetInvalidHeader(self): - result = AcceptCharsetInvalidHeader( - header_value="" - ) + AcceptCharsetInvalidHeader(header_value="utf/8") - assert isinstance(result, AcceptCharsetNoHeader) +class TestAcceptCharset__add: + invalid_values = [ + "", + [], + (), + {}, + "UTF/8", + ["UTF/8"], + ("UTF/8",), + {"UTF/8": 1.0}, + StringMe(""), + StringMe("UTF/8"), + ] - def test___bool__(self): - instance = AcceptCharsetInvalidHeader(header_value="") - returned = bool(instance) - assert returned is False + valid_values_with_headers = [ + ( + "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", + "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", + ), + ( + [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], + "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", + ), + ( + (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), + "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", + ), + ( + {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, + "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", + ), + ( + StringMe("UTF-7;q=0.5, unicode-1-1;q=0, UTF-8"), + "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", + ), + ] - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - instance = AcceptCharsetInvalidHeader(header_value="") - returned = "char-set" in instance - assert returned is True + # 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, + } + ) - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptCharsetInvalidHeader(header_value="") - returned = list(instance) - assert returned == [] + # 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 AcceptCharset - def test___radd___None(self): - result = None + AcceptCharsetInvalidHeader(header_value="") - assert isinstance(result, AcceptCharsetNoHeader) + # 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( - "left_operand", ["", [], (), {}, "UTF/8", ["UTF/8"], ("UTF/8",), {"UTF/8": 1.0}] + "input_value, input_header", + valid_values_with_headers, ) - def test___radd___invalid_value(self, left_operand): - result = left_operand + AcceptCharsetInvalidHeader(header_value="") - assert isinstance(result, AcceptCharsetNoHeader) + def test_valid_add_missing(self, input_value, input_header, maker, fn): + inst = AcceptCharset(input_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == input_header - @pytest.mark.parametrize("str_", ["", "UTF/8"]) - def test___radd___other_type_with_invalid___str__(self, str_): - class Other: - def __str__(self): - return str_ + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == input_header - result = Other() + AcceptCharsetInvalidHeader(header_value="") - assert isinstance(result, AcceptCharsetNoHeader) + def test_invalid_add_missing(self, maker, fn): + invalid_value = "UTF/8" + inst = AcceptCharset(invalid_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == invalid_value - @pytest.mark.parametrize( - "value, value_as_header", - [ - ( - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - [("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"], - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - (("UTF-7", 0.5), ("unicode-1-1", 0.0), "UTF-8"), - "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8", - ), - ( - {"UTF-7": 0.5, "unicode-1-1": 0.0, "UTF-8": 1.0}, - "UTF-8, UTF-7;q=0.5, unicode-1-1;q=0", - ), - ], - ) - def test___radd___valid_header_value(self, value, value_as_header): - result = value + AcceptCharsetInvalidHeader(header_value="") - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == value_as_header + 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___radd___other_type_valid_header_value(self): - class Other: - def __str__(self): - return "UTF-7;q=0.5, unicode-1-1;q=0, UTF-8" + def test_missing_add_missing(self, maker, fn): + inst = AcceptCharset(None) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Missing + assert inst.header_value is None - left_operand = Other() - result = left_operand + AcceptCharsetInvalidHeader(header_value="") - assert isinstance(result, AcceptCharsetValidHeader) - assert result.header_value == str(left_operand) + 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___repr__(self): - instance = AcceptCharsetInvalidHeader(header_value="\x00") - assert repr(instance) == "" + @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 = AcceptCharset(valid_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == valid_header - def test___str__(self): - instance = AcceptCharsetInvalidHeader(header_value="") - assert str(instance) == "" + result = fn(inst, maker(invalid_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == valid_header - def test_acceptable_offers(self): - instance = AcceptCharsetInvalidHeader(header_value="") - returned = instance.acceptable_offers(offers=["utf-8", "utf-7", "unicode-1-1"]) - assert returned == [("utf-8", 1.0), ("utf-7", 1.0), ("unicode-1-1", 1.0)] + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_invalid_add_invalid(self, invalid_value, maker, fn): + inst = AcceptCharset("") + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == "" - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self): - accept = AcceptCharsetInvalidHeader(header_value="") - assert accept.best_match(["utf-8", "iso-8859-5"]) == "utf-8" - assert accept.best_match([("utf-8", 1), ("iso-8859-5", 0.5)]) == "utf-8" - assert accept.best_match([("utf-8", 0.5), ("iso-8859-5", 1)]) == "iso-8859-5" - assert accept.best_match([("utf-8", 0.5), "iso-8859-5"]) == "iso-8859-5" - assert ( - accept.best_match([("utf-8", 0.5), "iso-8859-5"], default_match=True) - == "iso-8859-5" - ) - assert ( - accept.best_match([("utf-8", 0.5), "iso-8859-5"], default_match=False) - == "iso-8859-5" - ) - assert accept.best_match([], default_match="fallback") == "fallback" + 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.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - instance = AcceptCharsetInvalidHeader(header_value="") - returned = instance.quality(offer="char-set") - assert returned == 1.0 + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_missing_add_invalid(self, invalid_value, maker, fn): + inst = AcceptCharset(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 = "iso-8859-5" + inst = AcceptCharset(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 TestCreateAcceptCharsetHeader: def test_header_value_is_valid(self): header_value = "iso-8859-5, unicode-1-1;q=0.8" - returned = create_accept_charset_header(header_value=header_value) - assert isinstance(returned, AcceptCharsetValidHeader) + returned = create_accept_charset_header(header_value) + assert isinstance(returned, AcceptCharset) + assert returned.header_state is HeaderState.Valid assert returned.header_value == header_value returned2 = create_accept_charset_header(returned) - assert returned2 is not returned assert returned2._header_value == returned._header_value def test_header_value_is_None(self): - header_value = None - returned = create_accept_charset_header(header_value=header_value) - assert isinstance(returned, AcceptCharsetNoHeader) - assert returned.header_value == header_value + returned = create_accept_charset_header(None) + assert isinstance(returned, AcceptCharset) + assert returned.header_state is HeaderState.Missing + assert returned.header_value is None returned2 = create_accept_charset_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2._header_value is None @pytest.mark.parametrize("header_value", ["", "iso-8859-5, unicode/1"]) def test_header_value_is_invalid(self, header_value): - returned = create_accept_charset_header(header_value=header_value) - assert isinstance(returned, AcceptCharsetInvalidHeader) + returned = create_accept_charset_header(header_value) + assert isinstance(returned, AcceptCharset) + assert returned.header_state is HeaderState.Invalid assert returned.header_value == header_value returned2 = create_accept_charset_header(returned) - assert returned2 is not returned assert returned2._header_value == returned._header_value @@ -2269,39 +1878,45 @@ def test_fget_header_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": None}) property_ = accept_charset_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptCharsetNoHeader) + assert isinstance(returned, AcceptCharset) + assert returned.header_state is HeaderState.Missing def test_fget_header_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "UTF-8"}) property_ = accept_charset_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptCharsetValidHeader) + assert isinstance(returned, AcceptCharset) + assert returned.header_state is HeaderState.Valid def test_fget_header_is_invalid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": ""}) property_ = accept_charset_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptCharsetInvalidHeader) + assert isinstance(returned, AcceptCharset) + assert returned.header_state is HeaderState.Invalid def test_fset_value_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "UTF-8"}) property_ = accept_charset_property() property_.fset(request=request, value=None) - assert isinstance(request.accept_charset, AcceptCharsetNoHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Missing assert "HTTP_ACCEPT_CHARSET" not in request.environ def test_fset_value_is_invalid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "UTF-8"}) property_ = accept_charset_property() property_.fset(request=request, value="") - assert isinstance(request.accept_charset, AcceptCharsetInvalidHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Invalid assert request.environ["HTTP_ACCEPT_CHARSET"] == "" def test_fset_value_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "UTF-8"}) property_ = accept_charset_property() property_.fset(request=request, value="UTF-7") - assert isinstance(request.accept_charset, AcceptCharsetValidHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_CHARSET"] == "UTF-7" @pytest.mark.parametrize( @@ -2329,58 +1944,60 @@ def test_fset_value_types(self, value, value_as_header): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": ""}) property_ = accept_charset_property() property_.fset(request=request, value=value) - assert isinstance(request.accept_charset, AcceptCharsetValidHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_CHARSET"] == value_as_header def test_fset_other_type_with_valid___str__(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": ""}) property_ = accept_charset_property() - - class Other: - def __str__(self): - return "utf-8;q=0.5, iso-8859-5;q=0, utf-7" - - value = Other() + value = StringMe("utf-8;q=0.5, iso-8859-5;q=0, utf-7") property_.fset(request=request, value=value) - assert isinstance(request.accept_charset, AcceptCharsetValidHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_CHARSET"] == str(value) - def test_fset_AcceptCharsetNoHeader(self): + def test_fset_missing_AcceptCharset(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "utf-8"}) property_ = accept_charset_property() - header = AcceptCharsetNoHeader() + header = AcceptCharset(None) property_.fset(request=request, value=header) - assert isinstance(request.accept_charset, AcceptCharsetNoHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Missing assert "HTTP_ACCEPT_CHARSET" not in request.environ - def test_fset_AcceptCharsetValidHeader(self): + def test_fset_valid_AcceptCharset(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "utf-8"}) property_ = accept_charset_property() - header = AcceptCharsetValidHeader("utf-7") + header = AcceptCharset("utf-7") property_.fset(request=request, value=header) - assert isinstance(request.accept_charset, AcceptCharsetValidHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Valid assert request.environ["HTTP_ACCEPT_CHARSET"] == header.header_value - def test_fset_AcceptCharsetInvalidHeader(self): + def test_fset_invalid_AcceptCharset(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "utf-8"}) property_ = accept_charset_property() - header = AcceptCharsetInvalidHeader("") + header = AcceptCharset("") property_.fset(request=request, value=header) - assert isinstance(request.accept_charset, AcceptCharsetInvalidHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Invalid assert request.environ["HTTP_ACCEPT_CHARSET"] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank("/", environ={"HTTP_ACCEPT_CHARSET": "utf-8"}) property_ = accept_charset_property() property_.fdel(request=request) - assert isinstance(request.accept_charset, AcceptCharsetNoHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Missing assert "HTTP_ACCEPT_CHARSET" not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank("/") property_ = accept_charset_property() property_.fdel(request=request) - assert isinstance(request.accept_charset, AcceptCharsetNoHeader) + assert isinstance(request.accept_charset, AcceptCharset) + assert request.accept_charset.header_state is HeaderState.Missing assert "HTTP_ACCEPT_CHARSET" not in request.environ diff --git a/tests/test_request.py b/tests/test_request.py index 4366907a..a1fd9831 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -7,9 +7,7 @@ from webob.acceptparse import ( Accept, - AcceptCharsetInvalidHeader, - AcceptCharsetNoHeader, - AcceptCharsetValidHeader, + AcceptCharset, AcceptEncodingInvalidHeader, AcceptEncodingNoHeader, AcceptEncodingValidHeader, @@ -764,21 +762,24 @@ def test_accept_valid_header(self): def test_accept_charset_no_header(self): req = self._makeOne(environ={}) header = req.accept_charset - assert isinstance(header, AcceptCharsetNoHeader) + assert isinstance(header, AcceptCharset) + assert header.header_state is AcceptHeaderState.Missing assert header.header_value is None @pytest.mark.parametrize("header_value", ["", ", utf-7;q=0.2, utf-8;q =0.3"]) def test_accept_charset_invalid_header(self, header_value): req = self._makeOne(environ={"HTTP_ACCEPT_CHARSET": header_value}) header = req.accept_charset - assert isinstance(header, AcceptCharsetInvalidHeader) + assert isinstance(header, AcceptCharset) + assert header.header_state is AcceptHeaderState.Invalid assert header.header_value == header_value def test_accept_charset_valid_header(self): header_value = "iso-8859-5;q=0.372,unicode-1-1;q=0.977,UTF-8, *;q=0.000" req = self._makeOne(environ={"HTTP_ACCEPT_CHARSET": header_value}) header = req.accept_charset - assert isinstance(header, AcceptCharsetValidHeader) + assert isinstance(header, AcceptCharset) + assert header.header_state is AcceptHeaderState.Valid assert header.header_value == header_value # accept_encoding