diff --git a/CHANGES.txt b/CHANGES.txt index 722e14e3..118120fd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,14 +10,21 @@ Feature - Add Request.remote_host, exposing REMOTE_HOST environment variable. -- Added ``acceptparse.Accept.parse_offer`` to codify what types of offers - are compatible with ``acceptparse.AcceptValidHeader.acceptable_offers``, - ``acceptparse.AcceptMissingHeader.acceptable_offers``, and - ``acceptparse.AcceptInvalidHeader.acceptable_offers``. This API also - normalizes the offer with lowercased type/subtype and parameter names. +- Added ``webob.acceptparse.Accept.parse_offer`` to codify what types of offers + are compatible with ``webob.acceptparse.Accept.acceptable_offers``. This API + also normalizes the offer with lowercased type/subtype and parameter names. See https://github.com/Pylons/webob/pull/376 and https://github.com/Pylons/webob/pull/379 +- Consolidation of ``Accept`` header handling into a single class. + See backward incompatibilities below for more information. + See https://github.com/Pylons/webob/pull/460 + +- ``webob.acceptparse.Accept``, 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 https://github.com/Pylons/webob/pull/460 + Compatibility ~~~~~~~~~~~~~ @@ -27,6 +34,23 @@ Backwards Incompatibilities - Drop support for Python 2.7, 3.4, 3.5, 3.6, and 3.7. +- Remove ``AcceptValidHeader``, ``AcceptNoHeader`` and ``AcceptInvalidHeader``. + These classes are consolidated into ``Accept`` with a ``header_state`` + attribute for users that need to know the exact state of the header. + See https://github.com/Pylons/webob/pull/460 + +- Remove previously-deprecated ``webob.acceptparse.MIMEAccept``. + See https://github.com/Pylons/webob/pull/460 + +- Remove previously-deprecated ``webob.acceptparse.Accept.__iter__``. + See https://github.com/Pylons/webob/pull/460 + +- ``webob.acceptparse.Accept`` methods, ``best_match``, ``quality``, and + ``__contains__`` now require explicit media types with no wildcards or + server-side quality values. They will not match values with a quality of 0. + Their logic is consistent with ``acceptable_offers``. + See https://github.com/Pylons/webob/pull/460 + Experimental Features ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/api/webob.txt b/docs/api/webob.txt index d10f0d45..aa1f1734 100644 --- a/docs/api/webob.txt +++ b/docs/api/webob.txt @@ -23,43 +23,33 @@ a certain type: The classes that may be returned by one of the functions above, and their methods: -.. autoclass:: Accept - :members: parse - .. autoclass:: AcceptOffer :members: __str__ -.. autoclass:: AcceptValidHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - accept_html, accepts_html, acceptable_offers, best_match, quality - -.. autoclass:: AcceptNoHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - accept_html, accepts_html, acceptable_offers, best_match, quality +.. autoenum:: HeaderState -.. autoclass:: AcceptInvalidHeader - :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, - accept_html, accepts_html, acceptable_offers, best_match, quality +.. autoclass:: Accept + :members: + parse, header_value, parsed, header_state, __init__, __add__, + __bool__, __radd__, __repr__, __str__, __contains__, + 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__, + __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__, + __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__, + __contains__, __iter__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptEncoding @@ -67,17 +57,17 @@ methods: .. autoclass:: AcceptEncodingValidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, - __contains__, __iter__, __radd__, __repr__, __str__, + __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__, + __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__, + __contains__, __iter__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptLanguage @@ -85,23 +75,19 @@ methods: .. autoclass:: AcceptLanguageValidHeader :members: header_value, parsed, __init__, __add__, __contains__, __iter__, - __radd__, __str__, parse, basic_filtering, best_match, lookup, + __radd__, __str__, parse, basic_filtering, best_match, lookup, quality .. autoclass:: AcceptLanguageNoHeader :members: header_value, parsed, __init__, __add__, __contains__, __iter__, - __radd__, __str__, parse, basic_filtering, best_match, lookup, + __radd__, __str__, parse, basic_filtering, best_match, lookup, quality .. autoclass:: AcceptLanguageInvalidHeader :members: header_value, parsed, __init__, __add__, __contains__, __iter__, - __radd__, __str__, parse, basic_filtering, best_match, lookup, + __radd__, __str__, parse, basic_filtering, best_match, lookup, quality -Deprecated: - -.. autoclass:: MIMEAccept - Cache-Control ~~~~~~~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index 51daed72..7555ee3b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -import pkg_resources +import importlib.metadata import sys import os import shlex @@ -6,6 +6,7 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", + "enum_tools.autoenum", ] # Add any paths that contain templates here, relative to this directory. @@ -27,7 +28,7 @@ copyright = "2018, Ian Bicking, Pylons Project and contributors" author = "Ian Bicking, Pylons Project, and contributors" -version = release = pkg_resources.get_distribution("webob").version +version = release = importlib.metadata.distribution("webob").version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.cfg b/setup.cfg index 394e664a..71f56d86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,6 @@ license_file = docs/license.txt python_files = test_*.py testpaths = tests -addopts = -W always --cov --cov-report=term-missing +addopts = --cov --cov-report=term-missing +filterwarnings = + always diff --git a/setup.py b/setup.py index 4186de41..fcb23224 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,11 @@ "pytest-xdist", ] -docs_extras = ["Sphinx >= 1.7.5", "pylons-sphinx-themes"] +docs_extras = [ + "Sphinx >= 1.7.5", + "pylons-sphinx-themes", + "enum-tools[sphinx]", +] setup( name="WebOb", diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 039a7726..c9ed0e76 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -6,6 +6,7 @@ """ from collections import namedtuple +import enum import re import textwrap import warnings @@ -89,6 +90,17 @@ def _list_1_or_more__compiled_re(element_re): ) +class HeaderState(enum.Enum): + """ + The state of an accept-style header to assist in identifying scenarios + an application may want to know about during accept-negotation. + """ + + Valid = "Valid" + Invalid = "Invalid" + Missing = "Missing" + + class AcceptOffer(namedtuple("AcceptOffer", ["type", "subtype", "params"])): """ A pre-parsed offer tuple represeting a value in the format @@ -116,8 +128,12 @@ class Accept: """ Represent an ``Accept`` header. - Base class for :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, and - :class:`AcceptInvalidHeader`. + A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.2 + <7231#section-5.3.2>`. + + This object should not be modified. To add to the header, we can use the + addition operators (``+`` and ``+=``), which return a new object (see the + docstring for :meth:`.__add__`). """ # RFC 6838 describes syntax rules for media types that are different to @@ -339,7 +355,7 @@ def _parse_media_type_params(cls, media_type_params_segment): value = cls._process_quoted_string_token(token=value) media_type_params[index] = (name, value) - return media_type_params + return tuple(media_type_params) @classmethod def _process_quoted_string_token(cls, token): @@ -358,6 +374,9 @@ def _python_value_to_header_str(cls, value): Convert Python value to header string for __add__/__radd__. """ + if value is None: + return value + if isinstance(value, str): return value @@ -399,9 +418,9 @@ def parse(cls, value): Parse an ``Accept`` header. :param value: (``str``) header value - :return: If `value` is a valid ``Accept`` header, returns an iterator - of (*media_range*, *qvalue*, *media_type_params*, - *extension_params*) tuples, as parsed from the header from + :return: If ``value`` is a valid ``Accept`` header, returns an iterator + of ``(*media_range*, *qvalue*, *media_type_params*, + *extension_params*)`` tuples, as parsed from the header from left to right. | *media_range* is the media range, including any media type @@ -421,7 +440,7 @@ def parse(cls, value): | *extension_params* is the extension parameters, as a list where each item is either a parameter string or a (parameter name, value) tuple. - :raises ValueError: if `value` is an invalid header + :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 @@ -472,6 +491,7 @@ def generator(value): extension_params[index] = token_key else: extension_params = [] + extension_params = tuple(extension_params) yield (media_range, qvalue, media_type_params, extension_params) @@ -533,18 +553,28 @@ def _parse_and_normalize_offers(cls, offers): return parsed_offers + def __init__(self, header_value): + """ + Create an :class:`.Accept` instance. -class AcceptValidHeader(Accept): - """ - Represent a valid ``Accept`` header. - - A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.2 - <7231#section-5.3.2>`. - - This object should not be modified. To add to the header, we can use the - addition operators (``+`` and ``+=``), which return a new object (see the - docstring for :meth:`AcceptValidHeader.__add__`). - """ + :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 + + #: Instance of :enum:`.HeaderState` representing the state of + #: the ``Accept`` 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): @@ -555,10 +585,10 @@ 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 (*media_range*, *qvalue*, *media_type_params*, - *extension_params*) tuples, where + A tuple of ``(*media_range*, *qvalue*, *media_type_params*, + *extension_params*)`` tuples, where *media_range* is the media range, including any media type parameters. The media range is returned in a canonicalised form (except the case of @@ -571,27 +601,14 @@ def parsed(self): *qvalue* is the quality value of the media range. *media_type_params* is the media type parameters, as a list of - (parameter name, value) tuples. + ``(parameter name, value)`` tuples. *extension_params* is the extension parameters, as a list where each - item is either a parameter string or a (parameter name, value) tuple. + item is either a parameter string or a ``(parameter name, value)`` tuple. """ return self._parsed - def __init__(self, header_value): - """ - Create an :class:`AcceptValidHeader` instance. - - :param header_value: (``str``) header value. - :raises ValueError: if `header_value` is an invalid value for an - ``Accept`` header. - """ - self._header_value = header_value - self._parsed = list(self.parse(header_value)) - self._parsed_nonzero = [item for item in self.parsed if item[1]] - # item[1] is the qvalue - def copy(self): """ Create a copy of the header object. @@ -599,56 +616,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 media ranges ``str``'s (including any media type - parameters) as keys, and either qvalues ``float``'s or (*qvalues*, - *extension_params*) tuples as values, where *extension_params* is a - ``str`` of the extension parameters segment of the header element, - starting with the first '``;``' - * a ``tuple`` or ``list``, where each item is either a header element - ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` - or ``list`` where *media_range* is a ``str`` of the media range - including any media type parameters, and *extension_params* is a - ``str`` of the extension parameters segment of the header element, - starting with the first '``;``' - * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or - :class:`AcceptInvalidHeader` instance - * object of any other type that returns a value for ``__str__`` - - If `other` is a valid header value or another - :class:`AcceptValidHeader` instance, and the header value it represents - is not `''`, then the two header values are joined with ``', '``, and a - new :class:`AcceptValidHeader` instance with the new header value is - returned. - - If `other` is a valid header value or another - :class:`AcceptValidHeader` instance representing a header value of - `''`; or if it is ``None`` or an :class:`AcceptNoHeader` instance; or - if it is an invalid header value, or an :class:`AcceptInvalidHeader` - instance, then a new :class:`AcceptValidHeader` instance with the same - header value as ``self`` is returned. - """ - - if isinstance(other, AcceptValidHeader): - if other.header_value == "": - return self.__class__(header_value=self.header_value) - else: - return create_accept_header( - header_value=self.header_value + ", " + other.header_value - ) - - if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)): - return self.__class__(header_value=self.header_value) - - return self._add_instance_and_non_accept_type(instance=self, other=other) - def __bool__(self): """ Return whether ``self`` represents a valid ``Accept`` header. @@ -656,106 +623,92 @@ 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 + return self.header_state is HeaderState.Valid def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. - .. warning:: + This is a thin wrapper around :meth:`.acceptable_offers`. + + .. note:: + + The underlying implementation was changed significantly in + WebOb 2.0 such that this method requires explicit media types + and does not support quality values on the offers. - The behavior of :meth:`AcceptValidHeader.__contains__` is currently - being maintained for backward compatibility, but it will change in - the future to better conform to the RFC. :param offer: (``str``) media type offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. - - This uses the old criterion of a match in - :meth:`AcceptValidHeader._old_match`, which is not as specified in - :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not - correctly take into account media type parameters: - - >>> 'text/html;p=1' in AcceptValidHeader('text/html') - False - - or media ranges with ``q=0`` in the header:: - - >>> 'text/html' in AcceptValidHeader('text/*, text/html;q=0') - True - >>> 'text/html' in AcceptValidHeader('text/html;q=0, */*') - True - - (See the docstring for :meth:`AcceptValidHeader._old_match` for other - problems with the old criterion for matching.) """ - warnings.warn( - "The behavior of AcceptValidHeader.__contains__ is " - "currently being maintained for backward compatibility, but it " - "will change in the future to better conform to the RFC.", - DeprecationWarning, - ) - - for ( - media_range, - _quality, - _media_type_params, - _extension_params, - ) in self._parsed_nonzero: - if self._old_match(media_range, offer): - return True - return False + return self.quality(offer) is not None - def __iter__(self): + def __add__(self, other): """ - Return all the ranges with non-0 qvalues, in order of preference. - - .. warning:: + Add to header, creating a new header object. - The behavior of this method is currently maintained for backward - compatibility, but will change in the future. + ``other`` can be: - :return: iterator of all the media ranges in the header with non-0 - qvalues, in descending order of qvalue. If two ranges have the - same qvalue, they are returned in the order of their positions - in the header, from left to right. + * ``None`` + * a ``str`` header value + * a ``dict``, with media ranges ``str``'s (including any media type + parameters) as keys, and either qvalues ``float``'s or (*qvalues*, + *extension_params*) tuples as values, where *extension_params* is a + ``str`` of the extension parameters segment of the header element, + starting with the first '``;``' + * a ``tuple`` or ``list``, where each item is either a header element + ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` + or ``list`` where *media_range* is a ``str`` of the media range + including any media type parameters, and *extension_params* is a + ``str`` of the extension parameters segment of the header element, + starting with the first '``;``' + * an :class:`Accept` instance + * object of any other type that returns a value for ``__str__`` - Please note that this is a simple filter for the ranges in the header - with non-0 qvalues, and is not necessarily the same as what the client - prefers, e.g. ``'audio/basic;q=0, */*'`` means 'everything but - audio/basic', but ``list(instance)`` would return only ``['*/*']``. + 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. """ - warnings.warn( - "The behavior of AcceptLanguageValidHeader.__iter__ is currently " - "maintained for backward compatibility, but will change in the " - "future.", - DeprecationWarning, - ) - for media_range, _qvalue, _media_type_params, _extension_params in sorted( - self._parsed_nonzero, key=lambda i: i[1], reverse=True - ): - yield media_range + other = create_accept_header(other) + is_self_valid = self.header_state is HeaderState.Valid + is_other_valid = other.header_state is HeaderState.Valid + + if is_self_valid: + if is_other_valid: + if self.header_value == "": + return other + if other.header_value == "": + return self + return create_accept_header( + self.header_value + ", " + other.header_value + ) + return self + elif is_other_valid: + return other + return create_accept_header(None) def __radd__(self, other): """ Add to header, creating a new header object. - See the docstring for :meth:`AcceptValidHeader.__add__`. + See the docstring for :meth:`.__add__`. """ - return self._add_instance_and_non_accept_type( - instance=self, other=other, instance_on_the_right=True - ) + other = create_accept_header(other) + return other + self 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""" @@ -765,6 +718,13 @@ def __str__(self): q=0.50; e1=1 ;e2 , text/plain ,'``, ``str(instance)`` returns ``r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain'``. """ + + if self.header_state is HeaderState.Missing: + return "" + + elif self.header_state is HeaderState.Invalid: + return "" + # self.parsed tuples are in the form: (media_range, qvalue, # media_type_params, extension_params) # self._iterable_to_header_element() requires iterable to be in the @@ -782,100 +742,6 @@ def __str__(self): for tuple_ in self.parsed ) - def _add_instance_and_non_accept_type( - self, instance, other, instance_on_the_right=False - ): - if not other: - return self.__class__(header_value=instance.header_value) - - other_header_value = self._python_value_to_header_str(value=other) - - if other_header_value == "": - # if ``other`` is an object whose type we don't recognise, and - # str(other) returns '' - return self.__class__(header_value=instance.header_value) - - try: - self.parse(value=other_header_value) - except ValueError: # invalid header value - return self.__class__(header_value=instance.header_value) - - new_header_value = ( - (other_header_value + ", " + instance.header_value) - if instance_on_the_right - else (instance.header_value + ", " + other_header_value) - ) - return self.__class__(header_value=new_header_value) - - def _old_match(self, mask, offer): - """ - Check if the offer is covered by the mask - - ``offer`` may contain wildcards to facilitate checking if a ``mask`` - would match a 'permissive' offer. - - Wildcard matching forces the match to take place against the type or - subtype of the mask and offer (depending on where the wildcard matches) - - .. warning:: - - This is maintained for backward compatibility, and will be - deprecated in the future. - - This method was WebOb's old criterion for deciding whether a media type - matches a media range, used in - - - :meth:`AcceptValidHeader.__contains__` - - :meth:`AcceptValidHeader.best_match` - - :meth:`AcceptValidHeader.quality` - - It allows offers of *, */*, type/*, */subtype and types with no - subtypes, which are not media types as specified in :rfc:`RFC 7231, - section 5.3.2 <7231#section-5.3.2>`. This is also undocumented in any - of the public APIs that uses this method. - """ - # Match if comparisons are the same or either is a complete wildcard - if mask.lower() == offer.lower() or "*/*" in (mask, offer) or "*" == offer: - return True - - # Set mask type with wildcard subtype for malformed masks - try: - mask_type, mask_subtype = (x.lower() for x in mask.split("/")) - except ValueError: - mask_type = mask - mask_subtype = "*" - - # Set offer type with wildcard subtype for malformed offers - try: - offer_type, offer_subtype = (x.lower() for x in offer.split("/")) - except ValueError: - offer_type = offer - offer_subtype = "*" - - if mask_subtype == "*": - # match on type only - if offer_type == "*": - return True - else: - return mask_type.lower() == offer_type.lower() - - if mask_type == "*": - # match on subtype only - if offer_subtype == "*": - return True - else: - return mask_subtype.lower() == offer_subtype.lower() - - if offer_subtype == "*": - # match on type only - return mask_type.lower() == offer_type.lower() - - if offer_type == "*": - # match on subtype only - return mask_subtype.lower() == offer_subtype.lower() - - return offer.lower() == mask.lower() - def accept_html(self): """ Return ``True`` if any HTML-like type is accepted. @@ -908,18 +774,28 @@ def acceptable_offers(self, offers): This uses the matching rules described in :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. - Any offers that cannot be parsed via - :meth:`.Accept.parse_offer` will be ignored. - - :param offers: ``iterable`` of ``str`` media types (media types can - include media type parameters) or pre-parsed instances - of :class:`.AcceptOffer`. - :return: A list of tuples of the form (media type, qvalue), in - descending order of qvalue. Where two offers have the same - qvalue, they are returned in the same order as their order in - `offers`. - """ - parsed = self.parsed + Any offers that cannot be parsed via :meth:`.parse_offer` + will be ignored. + + :param offers: + ``iterable`` of ``str`` media types (media types can + include media type parameters) or pre-parsed instances + of :class:`.AcceptOffer`. + :return: + A list of tuples of the form (media type, qvalue), in + descending order of qvalue. Where two offers have the same + qvalue, they are returned in the same order as their order in + ``offers``. + """ + + if self.header_state is not HeaderState.Valid: + # avoid returning any offers that don't match the grammar so + # that the return values here are consistent with what would be + # returned in a valid header + return [ + (offers[offer_index], 1.0) + for offer_index, _ in self._parse_and_normalize_offers(offers) + ] # RFC 7231, section 3.1.1.1 "Media Type": # "The type, subtype, and parameter name tokens are case-insensitive. @@ -931,7 +807,7 @@ def acceptable_offers(self, offers): qvalue, tuple((name.lower(), value) for name, value in media_type_params), ) - for media_range, qvalue, media_type_params, __ in parsed + for media_range, qvalue, media_type_params, __ in self.parsed ] lowercased_offers_parsed = self._parse_and_normalize_offers(offers) @@ -1014,739 +890,99 @@ def acceptable_offers(self, offers): def best_match(self, offers, default_match=None): """ - Return the best match from the sequence of media type `offers`. + Return the best match from the sequence of media type ``offers``. - .. warning:: + This is a thin wrapper around :meth:`.acceptable_offers` that makes + usage more convenient for typical use-cases that do not need quality + details. - This is currently maintained for backward compatibility, and will be - deprecated in the future. + .. note:: - :meth:`AcceptValidHeader.best_match` uses its own algorithm (one not - specified in :rfc:`RFC 7231 <7231>`) to determine what is a best - match. The algorithm has many issues, and does not conform to - :rfc:`RFC 7231 <7231>`. + The underlying implementation was changed significantly in + WebOb 2.0 such that this method requires explicit media types + and does not support quality values on the offers. The input + is expected to be sorted in order of server preference. - Each media type in `offers` is checked against each non-``q=0`` range - in the header. If the two are a match according to WebOb's old - criterion for a match, the quality value of the match is the qvalue of - the media range from the header multiplied by the server quality value - of the offer (if the server quality value is not supplied, it is 1). + :param offers: + (iterable) - The offer in the match with the highest quality value is the best - match. If there is more than one match with the highest qvalue, the - match where the media range has a lower number of '*'s is the best - match. If the two have the same number of '*'s, the one that shows up - first in `offers` is the best match. + | Each item in the iterable must be a ``str`` media type and + may contain params/extensions. - :param offers: (iterable) + :param default_match: + (optional, any type) the value to be returned if there is no match - | Each item in the iterable may be a ``str`` media type, - or a (media type, server quality value) ``tuple`` or - ``list``. (The two may be mixed in the iterable.) + :return: + (``str``, or the type of ``default_match``) - :param default_match: (optional, any type) the value to be returned if - there is no match + | The offer that is the best match. If there is no match, the + value of ``default_match`` is returned. + """ + 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``. - This uses the old criterion of a match in - :meth:`AcceptValidHeader._old_match`, which is not as specified in - :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not - correctly take into account media type parameters: + .. note:: - >>> instance = AcceptValidHeader('text/html') - >>> instance.best_match(offers=['text/html;p=1']) is None - True + The underlying implementation was changed significantly in + WebOb 2.0 such that this method requires explicit media types. + + :param offer: (``str``) media type offer + :return: (``float`` or ``None``) - or media ranges with ``q=0`` in the header:: + | The highest quality value from the media range(s) that match + the `offer`, or ``None`` if there is no match. + """ + matches = self.acceptable_offers([offer]) + if matches: + return matches[0][1] + + +def create_accept_header(header_value): + """ + Create an object representing the ``Accept`` header in a request. + + :param header_value: (``str`` or ``None``) header value + :return: An :class:`.Accept` instance. + """ + if isinstance(header_value, Accept): + # no need to copy, accept class is immutable + return header_value + return Accept(header_value) + + +def accept_property(): + doc = """ + Property representing the ``Accept`` header. - >>> instance = AcceptValidHeader('text/*, text/html;q=0') - >>> instance.best_match(offers=['text/html']) - 'text/html' + (:rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`) + + The header value in the request environ is parsed and a new object + representing the header is created every time we *get* the value of the + property. (*set* and *del* change the header value in the request + environ, and do not involve parsing.) + """ - >>> instance = AcceptValidHeader('text/html;q=0, */*') - >>> instance.best_match(offers=['text/html']) - 'text/html' + ENVIRON_KEY = "HTTP_ACCEPT" - (See the docstring for :meth:`AcceptValidHeader._old_match` for other - problems with the old criterion for matching.) + def fget(request): + """Get an object representing the header in the request.""" - Another issue is that this method considers the best matching range for - an offer to be the matching range with the highest quality value, - (where quality values are tied, the most specific media range is - chosen); whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` - specifies that we should consider the best matching range for a media - type offer to be the most specific matching range.:: + return create_accept_header(request.environ.get(ENVIRON_KEY)) - >>> instance = AcceptValidHeader('text/html;q=0.5, text/*') - >>> instance.best_match(offers=['text/html', 'text/plain']) - 'text/html' - """ - warnings.warn( - "The behavior of AcceptValidHeader.best_match is currently being " - "maintained for backward compatibility, but it will be deprecated" - " in the future, as it does not conform to the RFC.", - DeprecationWarning, - ) - best_quality = -1 - best_offer = default_match - matched_by = "*/*" - for offer in offers: - if isinstance(offer, (tuple, list)): - offer, server_quality = offer - else: - server_quality = 1 - for item in self._parsed_nonzero: - mask = item[0] - quality = item[1] - possible_quality = server_quality * quality - if possible_quality < best_quality: - continue - elif possible_quality == best_quality: - # 'text/plain' overrides 'message/*' overrides '*/*' - # (if all match w/ the same q=) - if matched_by.count("*") <= mask.count("*"): - continue - if self._old_match(mask, offer): - best_quality = possible_quality - best_offer = offer - matched_by = mask - return best_offer - - def quality(self, offer): - """ - Return quality value of given offer, or ``None`` if there is no match. - - .. warning:: - - This is currently maintained for backward compatibility, and will be - deprecated in the future. - - :param offer: (``str``) media type offer - :return: (``float`` or ``None``) - - | The highest quality value from the media range(s) that match - the `offer`, or ``None`` if there is no match. - - This uses the old criterion of a match in - :meth:`AcceptValidHeader._old_match`, which is not as specified in - :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not - correctly take into account media type parameters: - - >>> instance = AcceptValidHeader('text/html') - >>> instance.quality('text/html;p=1') is None - True - - or media ranges with ``q=0`` in the header:: - - >>> instance = AcceptValidHeader('text/*, text/html;q=0') - >>> instance.quality('text/html') - 1.0 - >>> AcceptValidHeader('text/html;q=0, */*').quality('text/html') - 1.0 - - (See the docstring for :meth:`AcceptValidHeader._old_match` for other - problems with the old criterion for matching.) - - Another issue is that this method considers the best matching range for - an offer to be the matching range with the highest quality value, - whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` specifies - that we should consider the best matching range for a media type offer - to be the most specific matching range.:: - - >>> instance = AcceptValidHeader('text/html;q=0.5, text/*') - >>> instance.quality('text/html') - 1.0 - """ - warnings.warn( - "The behavior of AcceptValidHeader.quality is currently being " - "maintained for backward compatibility, but it will be deprecated " - "in the future, as it does not conform to the RFC.", - DeprecationWarning, - ) - bestq = 0 - for item in self.parsed: - media_range = item[0] - qvalue = item[1] - if self._old_match(media_range, offer): - bestq = max(bestq, qvalue) - return bestq or None - - -class MIMEAccept(Accept): - """ - Backwards compatibility shim for the new functionality provided by - AcceptValidHeader, AcceptInvalidHeader, or AcceptNoHeader, that acts like - the old MIMEAccept from WebOb version 1.7 or lower. - - This shim does use the newer Accept header parsing, which will mean your - application may be less liberal in what Accept headers are correctly - parsed. It is recommended that user agents be updated to send appropriate - Accept headers that are valid according to :rfc:`7231#section-5.3.2`. - - .. deprecated:: 1.8 - - Instead of directly creating the Accept object, please see: - :func:`create_accept_header(header_value) - `, which will create the - appropriate object. - - This shim has an extended deprecation period to allow for application - developers to switch to the new API. - - """ - - def __init__(self, header_value): - warnings.warn( - "The MIMEAccept class has been replaced by " - "webob.acceptparse.create_accept_header. This compatibility shim " - "will be deprecated in a future version of WebOb.", - DeprecationWarning, - ) - self._accept = create_accept_header(header_value) - if self._accept.parsed: - self._parsed = [(media, q) for (media, q, _, _) in self._accept.parsed] - self._parsed_nonzero = [(m, q) for (m, q) in self._parsed if q] - else: - self._parsed = [] - self._parsed_nonzero = [] - - @staticmethod - def parse(value): - try: - parsed_accepted = Accept.parse(value) - - for media, q, _, _ in parsed_accepted: - yield (media, q) - except ValueError: - pass - - def __repr__(self): - return self._accept.__repr__() - - def __iter__(self): - return self._accept.__iter__() - - def __str__(self): - return self._accept.__str__() - - def __add__(self, other): - if isinstance(other, self.__class__): - return self.__class__(str(self._accept.__add__(other._accept))) - else: - return self.__class__(str(self._accept.__add__(other))) - - def __radd__(self, other): - return self.__class__(str(self._accept.__radd__(other))) - - def __contains__(self, offer): - return offer in self._accept - - def quality(self, offer): - return self._accept.quality(offer) - - def best_match(self, offers, default_match=None): - return self._accept.best_match(offers, default_match=default_match) - - def accept_html(self): - return self._accept.accept_html() - - -class _AcceptInvalidOrNoHeader(Accept): - """ - Represent when an ``Accept`` header is invalid or not in request. - - This is the base class for the behaviour that :class:`.AcceptInvalidHeader` - and :class:`.AcceptNoHeader` have in common. - - :rfc:`7231` does not provide any guidance on what should happen if the - ``Accept`` header has an invalid value. This implementation disregards the - header when the header is invalid, so :class:`.AcceptInvalidHeader` and - :class:`.AcceptNoHeader` have much behaviour in common. - """ - - def __bool__(self): - """ - Return whether ``self`` represents a valid ``Accept`` header. - - Return ``True`` if ``self`` represents a valid header, and ``False`` if - it represents an invalid header, or the header not being in the - request. - - For this class, it always returns ``False``. - """ - return False - - def __contains__(self, offer): - """ - Return ``bool`` indicating whether `offer` is acceptable. - - .. warning:: - - The behavior of ``.__contains__`` for the ``Accept`` classes is - currently being maintained for backward compatibility, but it will - change in the future to better conform to the RFC. - - :param offer: (``str``) media type offer - :return: (``bool``) Whether ``offer`` is acceptable according to the - header. - - For this class, either there is no ``Accept`` header in the request, or - the header is invalid, so any media type is acceptable, and this always - returns ``True``. - """ - warnings.warn( - "The behavior of .__contains__ for the Accept classes is " - "currently being maintained for backward compatibility, but it " - "will change in the future to better conform to the RFC.", - DeprecationWarning, - ) - return True - - def __iter__(self): - """ - Return all the ranges with non-0 qvalues, in order of preference. - - .. warning:: - - The behavior of this method is currently maintained for backward - compatibility, but will change in the future. - - :return: iterator of all the media ranges in the header with non-0 - qvalues, in descending order of qvalue. If two ranges have the - same qvalue, they are returned in the order of their positions - in the header, from left to right. - - When there is no ``Accept`` header in the request or the header is - invalid, there are no media ranges, so this always returns an empty - iterator. - """ - warnings.warn( - "The behavior of AcceptValidHeader.__iter__ is currently " - "maintained for backward compatibility, but will change in the " - "future.", - DeprecationWarning, - ) - return iter(()) - - def accept_html(self): - """ - Return ``True`` if any HTML-like type is accepted. - - The HTML-like types are 'text/html', 'application/xhtml+xml', - 'application/xml' and 'text/xml'. - - When the header is invalid, or there is no `Accept` header in the - request, all `offers` are considered acceptable, so this always returns - ``True``. - """ - return bool( - self.acceptable_offers( - offers=[ - "text/html", - "application/xhtml+xml", - "application/xml", - "text/xml", - ] - ) - ) - - accepts_html = property(fget=accept_html, doc=accept_html.__doc__) - # note the plural - - def acceptable_offers(self, offers): - """ - Return the offers that are acceptable according to the header. - - Any offers that cannot be parsed via - :meth:`.Accept.parse_offer` will be ignored. - - :param offers: ``iterable`` of ``str`` media types (media types can - include media type parameters) - :return: When the header is invalid, or there is no ``Accept`` header - in the request, all `offers` are considered acceptable, so - this method returns a list of (media type, qvalue) tuples - where each offer in `offers` is paired with the qvalue of 1.0, - in the same order as in `offers`. - """ - # avoid returning any offers that don't match the grammar so - # that the return values here are consistent with what would be - # returned in AcceptValidHeader - return [ - (offers[offer_index], 1.0) - for offer_index, _ in self._parse_and_normalize_offers(offers) - ] - - def best_match(self, offers, default_match=None): - """ - Return the best match from the sequence of language tag `offers`. - - This is the ``.best_match()`` method for when the header is invalid or - not found in the request, corresponding to - :meth:`AcceptValidHeader.best_match`. - - .. warning:: - - This is currently maintained for backward compatibility, and will be - deprecated in the future (see the documentation for - :meth:`AcceptValidHeader.best_match`). - - When the header is invalid, or there is no `Accept` header in the - request, all `offers` are considered acceptable, so the best match is - the media type in `offers` with the highest server quality value (if - the server quality value is not supplied for a media type, it is 1). - - If more than one media type in `offers` have the same highest server - quality value, then the one that shows up first in `offers` is the best - match. - - :param offers: (iterable) - - | Each item in the iterable may be a ``str`` media type, - or a (media type, server quality value) ``tuple`` or - ``list``. (The two may be mixed in the iterable.) - - :param default_match: (optional, any type) the value to be returned if - `offers` is empty. - - :return: (``str``, or the type of `default_match`) - - | The offer that has the highest server quality value. If - `offers` is empty, the value of `default_match` is returned. - """ - warnings.warn( - "The behavior of .best_match for the Accept classes is currently " - "being maintained for backward compatibility, but the method will" - " be deprecated in the future, as its behavior is not specified " - "in (and currently does not conform to) RFC 7231.", - DeprecationWarning, - ) - best_quality = -1 - best_offer = default_match - for offer in offers: - if isinstance(offer, (list, tuple)): - offer, quality = offer - else: - quality = 1 - if quality > best_quality: - best_offer = offer - best_quality = quality - return best_offer - - def quality(self, offer): - """ - Return quality value of given offer, or ``None`` if there is no match. - - This is the ``.quality()`` method for when the header is invalid or not - found in the request, corresponding to - :meth:`AcceptValidHeader.quality`. - - .. warning:: - - This is currently maintained for backward compatibility, and will be - deprecated in the future (see the documentation for - :meth:`AcceptValidHeader.quality`). - - :param offer: (``str``) media type offer - :return: (``float``) ``1.0``. - - When the ``Accept`` header is invalid or not in the request, all offers - are equally acceptable, so 1.0 is always returned. - """ - warnings.warn( - "The behavior of .quality for the Accept classes is currently " - "being maintained for backward compatibility, but the method will" - " be deprecated in the future, as its behavior does not conform to" - "RFC 7231.", - DeprecationWarning, - ) - return 1.0 - - -class AcceptNoHeader(_AcceptInvalidOrNoHeader): - """ - Represent when there is no ``Accept`` header in the request. - - This object should not be modified. To add to the header, we can use the - addition operators (``+`` and ``+=``), which return a new object (see the - docstring for :meth:`AcceptNoHeader.__add__`). - """ - - @property - def header_value(self): - """ - (``str`` or ``None``) The header value. - - As there is no header in the request, this is ``None``. - """ - return self._header_value - - @property - def parsed(self): - """ - (``list`` or ``None``) Parsed form of the header. - - As there is no header in the request, this is ``None``. - """ - return self._parsed - - def __init__(self): - """ - Create an :class:`AcceptNoHeader` instance. - """ - self._header_value = None - self._parsed = None - self._parsed_nonzero = None - - def copy(self): - """ - Create a copy of the header object. - - """ - return self.__class__() - - def __add__(self, other): - """ - Add to header, creating a new header object. - - `other` can be: - - * ``None`` - * a ``str`` header value - * a ``dict``, with media ranges ``str``'s (including any media type - parameters) as keys, and either qvalues ``float``'s or (*qvalues*, - *extension_params*) tuples as values, where *extension_params* is a - ``str`` of the extension parameters segment of the header element, - starting with the first '``;``' - * a ``tuple`` or ``list``, where each item is either a header element - ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` - or ``list`` where *media_range* is a ``str`` of the media range - including any media type parameters, and *extension_params* is a - ``str`` of the extension parameters segment of the header element, - starting with the first '``;``' - * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or - :class:`AcceptInvalidHeader` instance - * object of any other type that returns a value for ``__str__`` - - If `other` is a valid header value or an :class:`AcceptValidHeader` - instance, a new :class:`AcceptValidHeader` instance with the valid - header value is returned. - - If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid - header value, or an :class:`AcceptInvalidHeader` instance, a new - :class:`AcceptNoHeader` instance is returned. - """ - if isinstance(other, AcceptValidHeader): - return AcceptValidHeader(header_value=other.header_value) - - if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)): - return self.__class__() - - return self._add_instance_and_non_accept_type(instance=self, other=other) - - def __radd__(self, other): - """ - Add to header, creating a new header object. - - See the docstring for :meth:`AcceptNoHeader.__add__`. - """ - return self.__add__(other=other) - - def __repr__(self): - return f"<{self.__class__.__name__}>" - - def __str__(self): - """Return the ``str`` ``''``.""" - - return "" - - def _add_instance_and_non_accept_type(self, instance, other): - if other is None: - return self.__class__() - - other_header_value = self._python_value_to_header_str(value=other) - - try: - return AcceptValidHeader(header_value=other_header_value) - except ValueError: # invalid header value - return self.__class__() - - -class AcceptInvalidHeader(_AcceptInvalidOrNoHeader): - """ - Represent an invalid ``Accept`` header. - - An invalid header is one that does not conform to - :rfc:`7231#section-5.3.2`. - - :rfc:`7231` does not provide any guidance on what should happen if the - ``Accept`` header has an invalid value. This implementation disregards the - header, and treats it as if there is no ``Accept`` header in the request. - - This object should not be modified. To add to the header, we can use the - addition operators (``+`` and ``+=``), which return a new object (see the - docstring for :meth:`AcceptInvalidHeader.__add__`). - """ - - @property - def header_value(self): - """(``str`` or ``None``) The header value.""" - - return self._header_value - - @property - def parsed(self): - """ - (``list`` or ``None``) Parsed form of the header. - - As the header is invalid and cannot be parsed, this is ``None``. - """ - - return self._parsed - - def __init__(self, header_value): - """ - Create an :class:`AcceptInvalidHeader` instance. - """ - self._header_value = header_value - self._parsed = None - self._parsed_nonzero = None - - def copy(self): - """ - Create a copy of the header object. - - """ - return self.__class__(self._header_value) - - def __add__(self, other): - """ - Add to header, creating a new header object. - - `other` can be: - - * ``None`` - * a ``str`` header value - * a ``dict``, with media ranges ``str``'s (including any media type - parameters) as keys, and either qvalues ``float``'s or (*qvalues*, - *extension_params*) tuples as values, where *extension_params* is a - ``str`` of the extension parameters segment of the header element, - starting with the first '``;``' - * a ``tuple`` or ``list``, where each item is either a header element - ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` - or ``list`` where *media_range* is a ``str`` of the media range - including any media type parameters, and *extension_params* is a - ``str`` of the extension parameters segment of the header element, - starting with the first '``;``' - * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or - :class:`AcceptInvalidHeader` instance - * object of any other type that returns a value for ``__str__`` - - If `other` is a valid header value or an :class:`AcceptValidHeader` - instance, then a new :class:`AcceptValidHeader` instance with the valid - header value is returned. - - If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid - header value, or an :class:`AcceptInvalidHeader` instance, a new - :class:`AcceptNoHeader` instance is returned. - """ - - if isinstance(other, AcceptValidHeader): - return AcceptValidHeader(header_value=other.header_value) - - if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)): - return AcceptNoHeader() - - return self._add_instance_and_non_accept_type(instance=self, other=other) - - def __radd__(self, other): - """ - Add to header, creating a new header object. - - See the docstring for :meth:`AcceptValidHeader.__add__`. - """ - - return self._add_instance_and_non_accept_type( - instance=self, other=other, instance_on_the_right=True - ) - - def __repr__(self): - return 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_type( - self, instance, other, instance_on_the_right=False - ): - if other is None: - return AcceptNoHeader() - - other_header_value = self._python_value_to_header_str(value=other) - - try: - return AcceptValidHeader(header_value=other_header_value) - except ValueError: # invalid header value - return AcceptNoHeader() - - -def create_accept_header(header_value): - """ - Create an object representing the ``Accept`` header in a request. - - :param header_value: (``str``) header value - :return: If `header_value` is ``None``, an :class:`AcceptNoHeader` - instance. - - | If `header_value` is a valid ``Accept`` header, an - :class:`AcceptValidHeader` instance. - - | If `header_value` is an invalid ``Accept`` header, an - :class:`AcceptInvalidHeader` instance. - """ - - if header_value is None: - return AcceptNoHeader() - if isinstance(header_value, Accept): - return header_value.copy() - try: - return AcceptValidHeader(header_value=header_value) - except ValueError: - return AcceptInvalidHeader(header_value=header_value) - - -def accept_property(): - doc = """ - Property representing the ``Accept`` header. - - (:rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`) - - The header value in the request environ is parsed and a new object - representing the header is created every time we *get* the value of the - property. (*set* and *del* change the header value in the request - environ, and do not involve parsing.) - """ - - ENVIRON_KEY = "HTTP_ACCEPT" - - def fget(request): - """Get an object representing the header in the request.""" - - return create_accept_header(header_value=request.environ.get(ENVIRON_KEY)) - - def fset(request, value): + def fset(request, value): """ Set the corresponding key in the request environ. - `value` can be: + ``value`` can be: * ``None`` * a ``str`` header value @@ -1761,19 +997,17 @@ def fset(request, value): including any media type parameters, and *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' - * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or - :class:`AcceptInvalidHeader` instance + * an :class:`.Accept` instance * object of any other type that returns a value for ``__str__`` """ - if value is None or isinstance(value, AcceptNoHeader): - fdel(request=request) + if isinstance(value, Accept): + value = value.header_value else: - if isinstance(value, (AcceptValidHeader, AcceptInvalidHeader)): - header_value = value.header_value - else: - header_value = Accept._python_value_to_header_str(value=value) - request.environ[ENVIRON_KEY] = header_value + value = Accept._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 736a6826..150a4b80 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -1,3 +1,4 @@ +from copy import deepcopy import re import warnings @@ -13,14 +14,12 @@ AcceptEncodingInvalidHeader, AcceptEncodingNoHeader, AcceptEncodingValidHeader, - AcceptInvalidHeader, AcceptLanguage, AcceptLanguageInvalidHeader, AcceptLanguageNoHeader, AcceptLanguageValidHeader, - AcceptNoHeader, - AcceptValidHeader, - MIMEAccept, + AcceptOffer, + HeaderState, _item_n_weight_re, _list_1_or_more__compiled_re, accept_charset_property, @@ -38,7 +37,14 @@ IGNORE_QUALITY = "ignore:.*quality.*" IGNORE_CONTAINS = "ignore:.*__contains__.*" IGNORE_ITER = "ignore:.*__iter__.*" -IGNORE_MIMEACCEPT = "ignore:.*MIMEAccept.*" + + +class StringMe: + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value class Test_ItemNWeightRe: @@ -139,7 +145,7 @@ def test_valid(self, header_value): assert regex.match(header_value) -class TestAccept: +class TestAccept__parsing: @pytest.mark.parametrize( "value", [ @@ -199,7 +205,7 @@ class TestAccept: ) def test_parse__invalid_header(self, value): with pytest.raises(ValueError): - AcceptValidHeader.parse(value=value) + Accept.parse(value=value) @pytest.mark.parametrize( "value, expected_list", @@ -207,35 +213,35 @@ def test_parse__invalid_header(self, value): # Examples from RFC 7231, Section 5.3.2 "Accept": ( "audio/*; q=0.2, audio/basic", - [("audio/*", 0.2, [], []), ("audio/basic", 1.0, [], [])], + [("audio/*", 0.2, (), ()), ("audio/basic", 1.0, (), ())], ), ( "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c", [ - ("text/plain", 0.5, [], []), - ("text/html", 1.0, [], []), - ("text/x-dvi", 0.8, [], []), - ("text/x-c", 1.0, [], []), + ("text/plain", 0.5, (), ()), + ("text/html", 1.0, (), ()), + ("text/x-dvi", 0.8, (), ()), + ("text/x-c", 1.0, (), ()), ], ), ( "text/*, text/plain, text/plain;format=flowed, */*", [ - ("text/*", 1.0, [], []), - ("text/plain", 1.0, [], []), - ("text/plain;format=flowed", 1.0, [("format", "flowed")], []), - ("*/*", 1.0, [], []), + ("text/*", 1.0, (), ()), + ("text/plain", 1.0, (), ()), + ("text/plain;format=flowed", 1.0, (("format", "flowed"),), ()), + ("*/*", 1.0, (), ()), ], ), ( "text/*;q=0.3, text/html;q=0.7, text/html;level=1, " "text/html;level=2;q=0.4, */*;q=0.5", [ - ("text/*", 0.3, [], []), - ("text/html", 0.7, [], []), - ("text/html;level=1", 1.0, [("level", "1")], []), - ("text/html;level=2", 0.4, [("level", "2")], []), - ("*/*", 0.5, [], []), + ("text/*", 0.3, (), ()), + ("text/html", 0.7, (), ()), + ("text/html;level=1", 1.0, (("level", "1"),), ()), + ("text/html;level=2", 0.4, (("level", "2"),), ()), + ("*/*", 0.5, (), ()), ], ), # Our tests @@ -245,9 +251,9 @@ def test_parse__invalid_header(self, value): ( "*/*, text/*, text/html", [ - ("*/*", 1.0, [], []), - ("text/*", 1.0, [], []), - ("text/html", 1.0, [], []), + ("*/*", 1.0, (), ()), + ("text/*", 1.0, (), ()), + ("text/html", 1.0, (), ()), ], ), # It does not seem from RFC 7231, section 5.3.2 "Accept" that the '*' @@ -255,15 +261,15 @@ def test_parse__invalid_header(self, value): # (the section lists '*/*', 'type/*' and 'type/subtype', but not # '*/subtype'). However, because type and subtype are tokens (section # 3.1.1.1), and a token may contain '*'s, '*/subtype' is valid. - ("*/html", [("*/html", 1.0, [], [])]), + ("*/html", [("*/html", 1.0, (), ())]), ( 'text/html \t;\t param1=val1\t; param2="val2" ' + r'; param3="\"\\\\"', [ ( r'text/html;param1=val1;param2=val2;param3="\"\\\\"', 1.0, - [("param1", "val1"), ("param2", "val2"), ("param3", r'"\\')], - [], + (("param1", "val1"), ("param2", "val2"), ("param3", r'"\\')), + (), ) ], ), @@ -273,20 +279,20 @@ def test_parse__invalid_header(self, value): ( "text/html;param=!#$%&'*+-.^_`|~09AZaz", 1.0, - [("param", "!#$%&'*+-.^_`|~09AZaz")], - [], + (("param", "!#$%&'*+-.^_`|~09AZaz"),), + (), ) ], ), - ('text/html;param=""', [('text/html;param=""', 1.0, [("param", "")], [])]), + ('text/html;param=""', [('text/html;param=""', 1.0, (("param", ""),), ())]), ( 'text/html;param="\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"', [ ( 'text/html;param="\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"', 1.0, - [("param", "\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e")], - [], + (("param", "\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"),), + (), ) ], ), @@ -296,8 +302,8 @@ def test_parse__invalid_header(self, value): ( 'text/html;param="\x80\x81\xfe\xff\\\x22\\\x5c"', 1.0, - [("param", "\x80\x81\xfe\xff\x22\x5c")], - [], + (("param", "\x80\x81\xfe\xff\x22\x5c"),), + (), ) ], ), @@ -307,8 +313,8 @@ def test_parse__invalid_header(self, value): ( 'text/html;param="\t \x21\x7e\x80\xff"', 1.0, - [("param", "\t \x21\x7e\x80\xff")], - [], + (("param", "\t \x21\x7e\x80\xff"),), + (), ) ], ), @@ -318,28 +324,28 @@ def test_parse__invalid_header(self, value): # surrounded with single quotes instead of double quotes, but the # single quotes are actually part of the media type parameter value # token - [("text/html;param='val'", 1.0, [("param", "'val'")], [])], - ), - ("text/html;q=0.9", [("text/html", 0.9, [], [])]), - ("text/html;q=0", [("text/html", 0.0, [], [])]), - ("text/html;q=0.0", [("text/html", 0.0, [], [])]), - ("text/html;q=0.00", [("text/html", 0.0, [], [])]), - ("text/html;q=0.000", [("text/html", 0.0, [], [])]), - ("text/html;q=1", [("text/html", 1.0, [], [])]), - ("text/html;q=1.0", [("text/html", 1.0, [], [])]), - ("text/html;q=1.00", [("text/html", 1.0, [], [])]), - ("text/html;q=1.000", [("text/html", 1.0, [], [])]), - ("text/html;q=0.1", [("text/html", 0.1, [], [])]), - ("text/html;q=0.87", [("text/html", 0.87, [], [])]), - ("text/html;q=0.382", [("text/html", 0.382, [], [])]), - ("text/html;Q=0.382", [("text/html", 0.382, [], [])]), - ("text/html ;Q=0.382", [("text/html", 0.382, [], [])]), - ("text/html; Q=0.382", [("text/html", 0.382, [], [])]), - ("text/html ; Q=0.382", [("text/html", 0.382, [], [])]), - ("text/html;q=0.9;q=0.8", [("text/html", 0.9, [], [("q", "0.8")])]), + [("text/html;param='val'", 1.0, (("param", "'val'"),), ())], + ), + ("text/html;q=0.9", [("text/html", 0.9, (), ())]), + ("text/html;q=0", [("text/html", 0.0, (), ())]), + ("text/html;q=0.0", [("text/html", 0.0, (), ())]), + ("text/html;q=0.00", [("text/html", 0.0, (), ())]), + ("text/html;q=0.000", [("text/html", 0.0, (), ())]), + ("text/html;q=1", [("text/html", 1.0, (), ())]), + ("text/html;q=1.0", [("text/html", 1.0, (), ())]), + ("text/html;q=1.00", [("text/html", 1.0, (), ())]), + ("text/html;q=1.000", [("text/html", 1.0, (), ())]), + ("text/html;q=0.1", [("text/html", 0.1, (), ())]), + ("text/html;q=0.87", [("text/html", 0.87, (), ())]), + ("text/html;q=0.382", [("text/html", 0.382, (), ())]), + ("text/html;Q=0.382", [("text/html", 0.382, (), ())]), + ("text/html ;Q=0.382", [("text/html", 0.382, (), ())]), + ("text/html; Q=0.382", [("text/html", 0.382, (), ())]), + ("text/html ; Q=0.382", [("text/html", 0.382, (), ())]), + ("text/html;q=0.9;q=0.8", [("text/html", 0.9, (), (("q", "0.8"),))]), ( "text/html;q=1;q=1;q=1", - [("text/html", 1.0, [], [("q", "1"), ("q", "1")])], + [("text/html", 1.0, (), (("q", "1"), ("q", "1")))], ), ( 'text/html;q=0.9;extparam1;extparam2=val2;extparam3="val3"', @@ -347,34 +353,37 @@ def test_parse__invalid_header(self, value): ( "text/html", 0.9, - [], - ["extparam1", ("extparam2", "val2"), ("extparam3", "val3")], + (), + ("extparam1", ("extparam2", "val2"), ("extparam3", "val3")), ) ], ), ( "text/html;q=1;extparam=!#$%&'*+-.^_`|~09AZaz", - [("text/html", 1.0, [], [("extparam", "!#$%&'*+-.^_`|~09AZaz")])], + [("text/html", 1.0, (), (("extparam", "!#$%&'*+-.^_`|~09AZaz"),))], + ), + ( + 'text/html;q=1;extparam=""', + [("text/html", 1.0, (), (("extparam", ""),))], ), - ('text/html;q=1;extparam=""', [("text/html", 1.0, [], [("extparam", "")])]), ( 'text/html;q=1;extparam="\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"', [ ( "text/html", 1.0, - [], - [("extparam", "\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e")], + (), + (("extparam", "\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"),), ) ], ), ( 'text/html;q=1;extparam="\x80\x81\xfe\xff\\\x22\\\x5c"', - [("text/html", 1.0, [], [("extparam", "\x80\x81\xfe\xff\x22\x5c")])], + [("text/html", 1.0, (), (("extparam", "\x80\x81\xfe\xff\x22\x5c"),))], ), ( 'text/html;q=1;extparam="\\\t\\ \\\x21\\\x7e\\\x80\\\xff"', - [("text/html", 1.0, [], [("extparam", "\t \x21\x7e\x80\xff")])], + [("text/html", 1.0, (), (("extparam", "\t \x21\x7e\x80\xff"),))], ), ( "text/html;q=1;extparam='val'", @@ -382,7 +391,7 @@ def test_parse__invalid_header(self, value): # surrounded with single quotes instead of double quotes, but the # single quotes are actually part of the extension parameter value # token - [("text/html", 1.0, [], [("extparam", "'val'")])], + [("text/html", 1.0, (), (("extparam", "'val'"),))], ), ( 'text/html;param1="val1";param2=val2;q=0.9;extparam1="val1"' @@ -391,14 +400,14 @@ def test_parse__invalid_header(self, value): ( "text/html;param1=val1;param2=val2", 0.9, - [("param1", "val1"), ("param2", "val2")], - [("extparam1", "val1"), "extparam2", ("extparam3", "val3")], + (("param1", "val1"), ("param2", "val2")), + (("extparam1", "val1"), "extparam2", ("extparam3", "val3")), ) ], ), ( ", ,, a/b \t;\t p1=1 ;\t\tp2=2 ; q=0.6\t \t;\t\t e1\t; e2, ,", - [("a/b;p1=1;p2=2", 0.6, [("p1", "1"), ("p2", "2")], ["e1", "e2"])], + [("a/b;p1=1;p2=2", 0.6, (("p1", "1"), ("p2", "2")), ("e1", "e2"))], ), ( ( @@ -406,16 +415,16 @@ def test_parse__invalid_header(self, value): + "g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t," ), [ - ("a/b", 1.0, [], ["e1", ("e2", "v2")]), - ("c/d", 1.0, [], []), - ("e/f;p1=v1", 0.0, [("p1", "v1")], ["e1"]), - ("g/h;p1=v1;p2=v2", 0.5, [("p1", "v1"), ("p2", "v2")], []), + ("a/b", 1.0, (), ("e1", ("e2", "v2"))), + ("c/d", 1.0, (), ()), + ("e/f;p1=v1", 0.0, (("p1", "v1"),), ("e1",)), + ("g/h;p1=v1;p2=v2", 0.5, (("p1", "v1"), ("p2", "v2")), ()), ], ), ], ) def test_parse__valid_header(self, value, expected_list): - returned = AcceptValidHeader.parse(value=value) + returned = Accept.parse(value=value) list_of_returned = list(returned) assert list_of_returned == expected_list @@ -466,1570 +475,427 @@ def test_parse_offer__invalid(self, offer): Accept.parse_offer(offer) -class TestAcceptValidHeader: - def test_parse__inherited(self): - returned = AcceptValidHeader.parse( - value=( - ",\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, " - + "g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t," - ) - ) - list_of_returned = list(returned) - assert list_of_returned == [ - ("a/b", 1.0, [], ["e1", ("e2", "v2")]), - ("c/d", 1.0, [], []), - ("e/f;p1=v1", 0.0, [("p1", "v1")], ["e1"]), - ("g/h;p1=v1;p2=v2", 0.5, [("p1", "v1"), ("p2", "v2")], []), - ] - - @pytest.mark.parametrize( - "header_value", [", ", "text/html;param=val;q=1;extparam=\x19"] - ) - def test___init___invalid_header(self, header_value): - with pytest.raises(ValueError): - AcceptValidHeader(header_value=header_value) - - def test___init___valid_header(self): +class TestAccept__valid: + def test___init___(self): header_value = ( ",\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, " + "g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t," ) - instance = AcceptValidHeader(header_value=header_value) + instance = Accept(header_value) + assert instance.header_state == HeaderState.Valid assert instance.header_value == header_value - assert instance.parsed == [ - ("a/b", 1.0, [], ["e1", ("e2", "v2")]), - ("c/d", 1.0, [], []), - ("e/f;p1=v1", 0.0, [("p1", "v1")], ["e1"]), - ("g/h;p1=v1;p2=v2", 0.5, [("p1", "v1"), ("p2", "v2")], []), - ] - assert instance._parsed_nonzero == [ - ("a/b", 1.0, [], ["e1", ("e2", "v2")]), - ("c/d", 1.0, [], []), - ("g/h;p1=v1;p2=v2", 0.5, [("p1", "v1"), ("p2", "v2")], []), - ] - assert isinstance(instance, Accept) + assert instance.parsed == ( + ("a/b", 1.0, (), ("e1", ("e2", "v2"))), + ("c/d", 1.0, (), ()), + ("e/f;p1=v1", 0.0, (("p1", "v1"),), ("e1",)), + ("g/h;p1=v1;p2=v2", 0.5, (("p1", "v1"), ("p2", "v2")), ()), + ) - def test___add___None(self): - left_operand = AcceptValidHeader(header_value="text/html") - result = left_operand + None - assert isinstance(result, AcceptValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand + def test___bool__(self): + instance = Accept("type/subtype") + returned = bool(instance) + assert returned is True @pytest.mark.parametrize( - "right_operand", + "header_value, expected_returned", [ - ", ", - [", "], - (", ",), - {", ": 1.0}, - {", ;level=1": (1.0, ";e1=1")}, - "a/b, c/d;q=1;e1;", - ["a/b", "c/d;q=1;e1;"], - ("a/b", "c/d;q=1;e1;"), - {"a/b": 1.0, "cd": 1.0}, - {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, + ("", ""), + ( + r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,', + r"""", + ), + ( + ',\t, a/b ; p1=1 ; p2=2 ;\t q=0.20 ;\te1="\\"\\1\\""\t; e2 ; ' + + "e3=3, c/d ,,", + r"""", + ), ], ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptValidHeader(header_value="text/html") - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - @pytest.mark.parametrize("str_", [", ", "a/b, c/d;q=1;e1;"]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptValidHeader(header_value="text/html") + def test___repr__(self, header_value, expected_returned): + instance = Accept(header_value) + assert repr(instance) == expected_returned - class Other: - def __str__(self): - return str_ + @pytest.mark.parametrize( + "header_value, expected_returned", + [ + ("", ""), + ( + r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,', + r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain', + ), + ( + ',\t, a/b ; p1=1 ; p2=2 ;\t q=0.20 ;\te1="\\"\\1\\""\t; e2 ; ' + + "e3=3, c/d ,,", + 'a/b;p1=1;p2=2;q=0.2;e1="\\"1\\"";e2;e3=3, c/d', + ), + ], + ) + def test___str__(self, header_value, expected_returned): + instance = Accept(header_value) + assert str(instance) == expected_returned - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand + def test_copy(self): + instance = Accept("*/plain;charset=utf8;x-version=1") + 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", ["", [], (), {}]) - def test___add___valid_empty_value(self, value): - left_operand = AcceptValidHeader(header_value=",\t ,i/j, k/l;q=0.333,") - result = left_operand + value - assert isinstance(result, AcceptValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand + def test___contains__(self): + accept = Accept("A/a, B/b, C/c, B/x;q=0") + assert "A/a" in accept + assert "A/b" not in accept + assert "B/a" not in accept + assert "B/x" not in accept + for mask in ["*/*", "text/html", "TEXT/HTML"]: + assert "text/html" in Accept(mask) - def test___add___other_type_with_valid___str___empty(self): - left_operand = AcceptValidHeader(header_value=",\t ,i/j, k/l;q=0.333,") + @pytest.mark.parametrize( + "header_value, returned", + [ + ("tExt/HtMl", True), + ("APPlication/XHTML+xml", True), + ("appliCATION/xMl", True), + ("TeXt/XmL", True), + ("image/jpg", False), + ("TeXt/Plain", False), + ("image/jpg, text/html", True), + ], + ) + def test_accept_html(self, header_value, returned): + instance = Accept(header_value) + assert instance.accept_html() is returned - class Other: - def __str__(self): - return "" + @pytest.mark.parametrize( + "header_value, returned", + [ + ("tExt/HtMl", True), + ("APPlication/XHTML+xml", True), + ("appliCATION/xMl", True), + ("TeXt/XmL", True), + ("image/jpg", False), + ("TeXt/Plain", False), + ("image/jpg, text/html", True), + ], + ) + def test_accepts_html(self, header_value, returned): + instance = Accept(header_value) + assert instance.accepts_html is returned - result = left_operand + Other() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand + @pytest.mark.parametrize( + "offers, expected_returned", + [ + (["text/html;p=1;q=0.5"], []), + (["text/html;q=0.5"], []), + (["text/html;q=0.5;e=1"], []), + (["text/html", "text/plain;p=1;q=0.5;e=1", "foo"], [("text/html", 1.0)]), + ], + ) + def test_acceptable_offers__invalid_offers(self, offers, expected_returned): + assert Accept("text/html").acceptable_offers(offers=offers) == expected_returned @pytest.mark.parametrize( - "value, value_as_header", + "header_value, offers, expected_returned", [ - # str + # RFC 7231, section 5.3.2 ( - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "audio/*; q=0.2, audio/basic", + ["audio/mpeg", "audio/basic"], + [("audio/basic", 1.0), ("audio/mpeg", 0.2)], ), - # list of strs ( - ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c", + ["text/x-dvi", "text/x-c", "text/html", "text/plain"], + [ + ("text/x-c", 1.0), + ("text/html", 1.0), + ("text/x-dvi", 0.8), + ("text/plain", 0.5), + ], ), - # list of 3-item tuples, with extension parameters ( + "text/*;q=0.3, text/html;q=0.7, text/html;level=1, " + + "text/html;level=2;q=0.4, */*;q=0.5", [ - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), + "text/html;level=1", + "text/html", + "text/plain", + "image/jpeg", + "text/html;level=2", + "text/html;level=3", + ], + [ + ("text/html;level=1", 1.0), + ("text/html", 0.7), + ("text/html;level=3", 0.7), + ("image/jpeg", 0.5), + ("text/html;level=2", 0.4), + ("text/plain", 0.3), ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", ), - # list of 2-item tuples, without extension parameters + # Our tests ( - [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", + "teXT/*;Q=0.5, TeXt/hTmL;LeVeL=1", + ["tExT/HtMl;lEvEl=1", "TExt/PlAiN"], + [("tExT/HtMl;lEvEl=1", 1.0), ("TExt/PlAiN", 0.5)], ), - # list of a mixture of strs, 3-item tuples and 2-item tuples ( - [ - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "text/html, application/json", + ["text/html", "application/json"], + [("text/html", 1.0), ("application/json", 1.0)], ), - # tuple of strs ( - ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "text/html ;\t level=1", + ["text/html\t\t ; \tlevel=1"], + [("text/html\t\t ; \tlevel=1", 1.0)], ), - # tuple of 3-item tuples, with extension parameters + ("", ["text/html"], []), + ("text/html, image/jpeg", ["audio/basic", "text/plain"], []), ( - ( - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + r'text/html;p1=1;p2=2;p3="\""', + [r'text/html;p1=1;p2="2";p3="\""'], + [(r'text/html;p1=1;p2="2";p3="\""', 1.0)], ), - # tuple of 2-item tuples, without extension parameters + ("text/html;p1=1", ["text/html;p1=2"], []), + ("text/html", ["text/html;p1=1"], [("text/html;p1=1", 1.0)]), + ("text/html;p1=1", ["text/html"], []), + ("text/html", ["text/html"], [("text/html", 1.0)]), + ("text/*", ["text/html;p=1"], [("text/html;p=1", 1.0)]), + ("*/*", ["text/html;p=1"], [("text/html;p=1", 1.0)]), + ("text/*", ["text/html"], [("text/html", 1.0)]), + ("*/*", ["text/html"], [("text/html", 1.0)]), + ("text/html;p1=1;q=0", ["text/html;p1=1"], []), + ("text/html;q=0", ["text/html;p1=1", "text/html"], []), + ("text/*;q=0", ["text/html;p1=1", "text/html", "text/plain"], []), ( - (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", + "*/*;q=0", + ["text/html;p1=1", "text/html", "text/plain", "image/jpeg"], + [], ), - # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( - ( - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "*/*;q=0, audio/mpeg", + [ + "text/html;p1=1", + "audio/mpeg", + "text/html", + "text/plain", + "image/jpeg", + ], + [("audio/mpeg", 1.0)], ), - # dict ( - {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, - "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", + "text/html;p1=1, text/html;q=0", + ["text/html;p1=1"], + [("text/html;p1=1", 1.0)], ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - header = ",\t ,i/j, k/l;q=0.333," - result = AcceptValidHeader(header_value=header) + value - assert isinstance(result, AcceptValidHeader) - assert result.header_value == header + ", " + value_as_header - - def test___add___other_type_with_valid___str___not_empty(self): - header = ",\t ,i/j, k/l;q=0.333," - - class Other: - def __str__(self): - return "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - - right_operand = Other() - result = AcceptValidHeader(header_value=header) + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == header + ", " + str(right_operand) - - def test___add___AcceptValidHeader_header_value_empty(self): - left_operand = AcceptValidHeader( - header_value="a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - ) - right_operand = AcceptValidHeader(header_value="") - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == left_operand.header_value - assert result is not left_operand - - def test___add___AcceptValidHeader_header_value_not_empty(self): - left_operand = AcceptValidHeader( - header_value="a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - ) - right_operand = AcceptValidHeader(header_value=",\t ,i/j, k/l;q=0.333,") - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert ( - result.header_value - == left_operand.header_value + ", " + right_operand.header_value - ) - - def test___add___AcceptNoHeader(self): - valid_header_instance = AcceptValidHeader(header_value="a/b") - result = valid_header_instance + AcceptNoHeader() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance - - @pytest.mark.parametrize("header_value", [", ", 'a/b;p1=1;p2=2;q=0.8;e1;e2="']) - def test___add___AcceptInvalidHeader(self, header_value): - valid_header_instance = AcceptValidHeader(header_value="a/b") - result = valid_header_instance + AcceptInvalidHeader(header_value=header_value) - assert isinstance(result, AcceptValidHeader) - assert result.header_value == valid_header_instance.header_value - assert result is not valid_header_instance - - def test___bool__(self): - instance = AcceptValidHeader(header_value="type/subtype") - returned = bool(instance) - assert returned is True - - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - accept = AcceptValidHeader("A/a, B/b, C/c") - assert "A/a" in accept - assert "A/*" in accept - assert "*/a" in accept - assert "A/b" not in accept - assert "B/a" not in accept - for mask in ["*/*", "text/html", "TEXT/HTML"]: - assert "text/html" in AcceptValidHeader(mask) - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptValidHeader( - header_value=( - "text/plain; q=0.5, text/html; q=0, text/x-dvi; q=0.8, " "text/x-c" - ) - ) - assert list(instance) == ["text/x-c", "text/x-dvi", "text/plain"] - - def test___radd___None(self): - right_operand = AcceptValidHeader(header_value="a/b") - result = None + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "left_operand", - [ - ", ", - [", "], - (", ",), - {", ": 1.0}, - {", ;level=1": (1.0, ";e1=1")}, - "a/b, c/d;q=1;e1;", - ["a/b", "c/d;q=1;e1;"], - ("a/b", "c/d;q=1;e1;"), - {"a/b": 1.0, "cd": 1.0}, - {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, - ], - ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptValidHeader(header_value="a/b") - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize("str_", [", ", "a/b, c/d;q=1;e1;"]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptValidHeader(header_value="a/b") - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptValidHeader) - 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 = AcceptValidHeader(header_value="a/b") - result = value + right_operand - assert isinstance(result, AcceptValidHeader) - 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 = AcceptValidHeader(header_value=",\t ,i/j, k/l;q=0.333,") - - class Other: - def __str__(self): - return "" - - result = Other() + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - @pytest.mark.parametrize( - "value, value_as_header", - [ - # str - ( - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of strs - ( - ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 3-item tuples, with extension parameters + ("text/html, text/*;q=0", ["text/html"], [("text/html", 1.0)]), + ("text/*, */*;q=0", ["text/html"], [("text/html", 1.0)]), + ("text/html;q=0, text/html", ["text/html"], []), ( + "text/html", + ["text/html;level=1", "text/html", "text/html;level=2"], [ - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), + ("text/html;level=1", 1.0), + ("text/html", 1.0), + ("text/html;level=2", 1.0), ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 2-item tuples, without extension parameters - ( - [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", ), - # list of a mixture of strs, 3-item tuples and 2-item tuples ( + "text/*;q=0.3, text/html;q=0, image/png, text/html;level=1, " + + "text/html;level=2;q=0.4, image/jpeg;q=0.5", [ - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), + "text/html;level=1", + "text/html", + "text/plain", + "image/jpeg", + "text/html;level=2", + "text/html;level=3", + "audio/basic", + ], + [ + ("text/html;level=1", 1.0), + ("image/jpeg", 0.5), + ("text/html;level=2", 0.4), + ("text/plain", 0.3), ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of strs - ( - ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 3-item tuples, with extension parameters - ( - ( - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 2-item tuples, without extension parameters - ( - (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", ), - # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( - ( - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "text/*;q=0.3, text/html;q=0.5, text/html;level=1;q=0.7", + ["text/*", "*/*", "text/html", "image/*"], + [("text/html", 0.5)], ), - # dict ( - {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, - "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", + "text/html;level=1;q=0.7", + ["text/*", "*/*", "text/html", "text/html;level=1", "image/*"], + [("text/html;level=1", 0.7)], ), + ("*/*", ["text/*"], []), + ("", ["text/*", "*/*", "text/html", "text/html;level=1", "image/*"], []), ], ) - def test___radd___valid_non_empty_value(self, value, value_as_header): - header = ",\t ,i/j, k/l;q=0.333," - result = value + AcceptValidHeader(header_value=header) - assert isinstance(result, AcceptValidHeader) - assert result.header_value == value_as_header + ", " + header + def test_acceptable_offers__valid_offers( + self, header_value, offers, expected_returned + ): + instance = Accept(header_value) + returned = instance.acceptable_offers(offers=offers) + assert returned == expected_returned - def test___radd___other_type_with_valid___str___not_empty(self): - header = ",\t ,i/j, k/l;q=0.333," + def test_acceptable_offers_uses_AcceptOffer_objects(self): + offer = AcceptOffer("text", "html", (("level", "1"),)) + instance = Accept("text/*;q=0.5") + result = instance.acceptable_offers([offer]) + assert result == [(offer, 0.5)] - class Other: - def __str__(self): - return "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" + def test_best_match(self): + accept = Accept("text/html, foo/bar") + assert accept.best_match(["text/html", "foo/bar"]) == "text/html" + assert accept.best_match(["foo/bar", "text/html"]) == "foo/bar" - left_operand = Other() - result = left_operand + AcceptValidHeader(header_value=header) - assert isinstance(result, AcceptValidHeader) - assert result.header_value == str(left_operand) + ", " + header + def test_best_match_with_one_lower_q(self): + accept = Accept("text/html, foo/bar;q=0.5") + assert accept.best_match(["text/html", "foo/bar"]) == "text/html" + accept = Accept("text/html;q=0.5, foo/bar") + assert accept.best_match(["text/html", "foo/bar"]) == "foo/bar" - @pytest.mark.parametrize( - "header_value, expected_returned", - [ - ("", ""), - ( - r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,', - r"""", - ), - ( - ',\t, a/b ; p1=1 ; p2=2 ;\t q=0.20 ;\te1="\\"\\1\\""\t; e2 ; ' - + "e3=3, c/d ,,", - r"""", - ), - ], - ) - def test___repr__(self, header_value, expected_returned): - instance = AcceptValidHeader(header_value=header_value) - assert repr(instance) == expected_returned + def test_best_match_with_complex_q(self): + accept = Accept("text/html, foo/bar;q=0.55, baz/gort;q=0.59") + assert accept.best_match(["text/html", "foo/bar"]) == "text/html" + accept = Accept("text/html;q=0.5, foo/bar;q=0.586, baz/gort;q=0.596") + assert accept.best_match(["text/html", "baz/gort"]) == "baz/gort" - @pytest.mark.parametrize( - "header_value, expected_returned", - [ - ("", ""), - ( - r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,', - r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain', - ), - ( - ',\t, a/b ; p1=1 ; p2=2 ;\t q=0.20 ;\te1="\\"\\1\\""\t; e2 ; ' - + "e3=3, c/d ,,", - 'a/b;p1=1;p2=2;q=0.2;e1="\\"1\\"";e2;e3=3, c/d', - ), - ], - ) - def test___str__(self, header_value, expected_returned): - instance = AcceptValidHeader(header_value=header_value) - assert str(instance) == expected_returned + def test_best_match_json(self): + accept = Accept("text/html, */*; q=0.2") + assert accept.best_match(["application/json"]) == "application/json" - def test__old_match(self): - accept = AcceptValidHeader("image/jpg") - assert accept._old_match("image/jpg", "image/jpg") - assert accept._old_match("image/*", "image/jpg") - assert accept._old_match("*/*", "image/jpg") - assert not accept._old_match("text/html", "image/jpg") - - mismatches = [ - ("B/b", "A/a"), - ("B/b", "B/a"), - ("B/b", "A/b"), - ("A/a", "B/b"), - ("B/a", "B/b"), - ("A/b", "B/b"), - ] - for mask, offer in mismatches: - assert not accept._old_match(mask, offer) - - def test__old_match_wildcard_matching(self): - """ - Wildcard matching forces the match to take place against the type or - subtype of the mask and offer (depending on where the wildcard matches) - """ - accept = AcceptValidHeader("type/subtype") - matches = [ - ("*/*", "*/*"), - ("*/*", "A/*"), - ("*/*", "*/a"), - ("*/*", "A/a"), - ("A/*", "*/*"), - ("A/*", "A/*"), - ("A/*", "*/a"), - ("A/*", "A/a"), - ("*/a", "*/*"), - ("*/a", "A/*"), - ("*/a", "*/a"), - ("*/a", "A/a"), - ("A/a", "*/*"), - ("A/a", "A/*"), - ("A/a", "*/a"), - ("A/a", "A/a"), - # Offers might not contain a subtype - ("*/*", "*"), - ("A/*", "*"), - ("*/a", "*"), - ] - for mask, offer in matches: - assert accept._old_match(mask, offer) - # Test malformed mask and offer variants where either is missing a - # type or subtype - assert accept._old_match("A", offer) - assert accept._old_match(mask, "a") + def test_best_match_mixedcase(self): + accept = Accept("image/jpg; q=0.2, Image/pNg; Q=0.4, image/*; q=0.05") + assert accept.best_match(["Image/JpG"]) == "Image/JpG" + assert accept.best_match(["image/Tiff"]) == "image/Tiff" + assert ( + accept.best_match(["image/Tiff", "image/PnG", "image/jpg"]) == "image/PnG" + ) - mismatches = [("B/b", "A/*"), ("B/*", "A/a"), ("B/*", "A/*"), ("*/b", "*/a")] - for mask, offer in mismatches: - assert not accept._old_match(mask, offer) + def test_quality(self): + accept = Accept("text/html") + assert accept.quality("text/html") == 1 + accept = Accept("text/html;q=0.5") + assert accept.quality("text/html") == 0.5 - @pytest.mark.parametrize( - "header_value, returned", - [ - ("tExt/HtMl", True), - ("APPlication/XHTML+xml", True), - ("appliCATION/xMl", True), - ("TeXt/XmL", True), - ("image/jpg", False), - ("TeXt/Plain", False), - ("image/jpg, text/html", True), - ], - ) - def test_accept_html(self, header_value, returned): - instance = AcceptValidHeader(header_value=header_value) - assert instance.accept_html() is returned - - @pytest.mark.parametrize( - "header_value, returned", - [ - ("tExt/HtMl", True), - ("APPlication/XHTML+xml", True), - ("appliCATION/xMl", True), - ("TeXt/XmL", True), - ("image/jpg", False), - ("TeXt/Plain", False), - ("image/jpg, text/html", True), - ], - ) - def test_accepts_html(self, header_value, returned): - instance = AcceptValidHeader(header_value=header_value) - assert instance.accepts_html is returned - - @pytest.mark.parametrize( - "offers, expected_returned", - [ - (["text/html;p=1;q=0.5"], []), - (["text/html;q=0.5"], []), - (["text/html;q=0.5;e=1"], []), - (["text/html", "text/plain;p=1;q=0.5;e=1", "foo"], [("text/html", 1.0)]), - ], - ) - def test_acceptable_offers__invalid_offers(self, offers, expected_returned): - assert ( - AcceptValidHeader("text/html").acceptable_offers(offers=offers) - == expected_returned - ) - - @pytest.mark.parametrize( - "header_value, offers, expected_returned", - [ - # RFC 7231, section 5.3.2 - ( - "audio/*; q=0.2, audio/basic", - ["audio/mpeg", "audio/basic"], - [("audio/basic", 1.0), ("audio/mpeg", 0.2)], - ), - ( - "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c", - ["text/x-dvi", "text/x-c", "text/html", "text/plain"], - [ - ("text/x-c", 1.0), - ("text/html", 1.0), - ("text/x-dvi", 0.8), - ("text/plain", 0.5), - ], - ), - ( - "text/*;q=0.3, text/html;q=0.7, text/html;level=1, " - + "text/html;level=2;q=0.4, */*;q=0.5", - [ - "text/html;level=1", - "text/html", - "text/plain", - "image/jpeg", - "text/html;level=2", - "text/html;level=3", - ], - [ - ("text/html;level=1", 1.0), - ("text/html", 0.7), - ("text/html;level=3", 0.7), - ("image/jpeg", 0.5), - ("text/html;level=2", 0.4), - ("text/plain", 0.3), - ], - ), - # Our tests - ( - "teXT/*;Q=0.5, TeXt/hTmL;LeVeL=1", - ["tExT/HtMl;lEvEl=1", "TExt/PlAiN"], - [("tExT/HtMl;lEvEl=1", 1.0), ("TExt/PlAiN", 0.5)], - ), - ( - "text/html, application/json", - ["text/html", "application/json"], - [("text/html", 1.0), ("application/json", 1.0)], - ), - ( - "text/html ;\t level=1", - ["text/html\t\t ; \tlevel=1"], - [("text/html\t\t ; \tlevel=1", 1.0)], - ), - ("", ["text/html"], []), - ("text/html, image/jpeg", ["audio/basic", "text/plain"], []), - ( - r'text/html;p1=1;p2=2;p3="\""', - [r'text/html;p1=1;p2="2";p3="\""'], - [(r'text/html;p1=1;p2="2";p3="\""', 1.0)], - ), - ("text/html;p1=1", ["text/html;p1=2"], []), - ("text/html", ["text/html;p1=1"], [("text/html;p1=1", 1.0)]), - ("text/html;p1=1", ["text/html"], []), - ("text/html", ["text/html"], [("text/html", 1.0)]), - ("text/*", ["text/html;p=1"], [("text/html;p=1", 1.0)]), - ("*/*", ["text/html;p=1"], [("text/html;p=1", 1.0)]), - ("text/*", ["text/html"], [("text/html", 1.0)]), - ("*/*", ["text/html"], [("text/html", 1.0)]), - ("text/html;p1=1;q=0", ["text/html;p1=1"], []), - ("text/html;q=0", ["text/html;p1=1", "text/html"], []), - ("text/*;q=0", ["text/html;p1=1", "text/html", "text/plain"], []), - ( - "*/*;q=0", - ["text/html;p1=1", "text/html", "text/plain", "image/jpeg"], - [], - ), - ( - "*/*;q=0, audio/mpeg", - [ - "text/html;p1=1", - "audio/mpeg", - "text/html", - "text/plain", - "image/jpeg", - ], - [("audio/mpeg", 1.0)], - ), - ( - "text/html;p1=1, text/html;q=0", - ["text/html;p1=1"], - [("text/html;p1=1", 1.0)], - ), - ("text/html, text/*;q=0", ["text/html"], [("text/html", 1.0)]), - ("text/*, */*;q=0", ["text/html"], [("text/html", 1.0)]), - ("text/html;q=0, text/html", ["text/html"], []), - ( - "text/html", - ["text/html;level=1", "text/html", "text/html;level=2"], - [ - ("text/html;level=1", 1.0), - ("text/html", 1.0), - ("text/html;level=2", 1.0), - ], - ), - ( - "text/*;q=0.3, text/html;q=0, image/png, text/html;level=1, " - + "text/html;level=2;q=0.4, image/jpeg;q=0.5", - [ - "text/html;level=1", - "text/html", - "text/plain", - "image/jpeg", - "text/html;level=2", - "text/html;level=3", - "audio/basic", - ], - [ - ("text/html;level=1", 1.0), - ("image/jpeg", 0.5), - ("text/html;level=2", 0.4), - ("text/plain", 0.3), - ], - ), - ( - "text/*;q=0.3, text/html;q=0.5, text/html;level=1;q=0.7", - ["text/*", "*/*", "text/html", "image/*"], - [("text/html", 0.5)], - ), - ( - "text/html;level=1;q=0.7", - ["text/*", "*/*", "text/html", "text/html;level=1", "image/*"], - [("text/html;level=1", 0.7)], - ), - ("*/*", ["text/*"], []), - ("", ["text/*", "*/*", "text/html", "text/html;level=1", "image/*"], []), - ], - ) - def test_acceptable_offers__valid_offers( - self, header_value, offers, expected_returned - ): - instance = AcceptValidHeader(header_value=header_value) - returned = instance.acceptable_offers(offers=offers) - assert returned == expected_returned - - def test_acceptable_offers_uses_AcceptOffer_objects(self): - from webob.acceptparse import AcceptOffer - - offer = AcceptOffer("text", "html", (("level", "1"),)) - instance = AcceptValidHeader(header_value="text/*;q=0.5") - result = instance.acceptable_offers([offer]) - assert result == [(offer, 0.5)] - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self): - accept = AcceptValidHeader("text/html, foo/bar") - assert accept.best_match(["text/html", "foo/bar"]) == "text/html" - assert accept.best_match(["foo/bar", "text/html"]) == "foo/bar" - assert accept.best_match([("foo/bar", 0.5), "text/html"]) == "text/html" - assert accept.best_match([("foo/bar", 0.5), ("text/html", 0.4)]) == "foo/bar" - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match_with_one_lower_q(self): - accept = AcceptValidHeader("text/html, foo/bar;q=0.5") - assert accept.best_match(["text/html", "foo/bar"]) == "text/html" - accept = AcceptValidHeader("text/html;q=0.5, foo/bar") - assert accept.best_match(["text/html", "foo/bar"]) == "foo/bar" - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match_with_complex_q(self): - accept = AcceptValidHeader("text/html, foo/bar;q=0.55, baz/gort;q=0.59") - assert accept.best_match(["text/html", "foo/bar"]) == "text/html" - accept = AcceptValidHeader("text/html;q=0.5, foo/bar;q=0.586, baz/gort;q=0.596") - assert accept.best_match(["text/html", "baz/gort"]) == "baz/gort" - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match_json(self): - accept = AcceptValidHeader("text/html, */*; q=0.2") - assert accept.best_match(["application/json"]) == "application/json" - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match_mixedcase(self): - accept = AcceptValidHeader( - "image/jpg; q=0.2, Image/pNg; Q=0.4, image/*; q=0.05" - ) - assert accept.best_match(["Image/JpG"]) == "Image/JpG" - assert accept.best_match(["image/Tiff"]) == "image/Tiff" - assert ( - accept.best_match(["image/Tiff", "image/PnG", "image/jpg"]) == "image/PnG" - ) - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test_best_match_zero_quality(self): - assert ( - AcceptValidHeader("text/plain, */*;q=0").best_match(["text/html"]) is None - ) - assert "audio/basic" not in AcceptValidHeader("*/*;q=0") - - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - accept = AcceptValidHeader("text/html") - assert accept.quality("text/html") == 1 - accept = AcceptValidHeader("text/html;q=0.5") - assert accept.quality("text/html") == 0.5 - - @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality_not_found(self): - accept = AcceptValidHeader("text/html") + accept = Accept("text/html") assert accept.quality("foo/bar") is None -class TestAcceptNoHeader: - def test_parse__inherited(self): - returned = AcceptNoHeader.parse( - value=( - ",\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, " - + "g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t," - ) - ) - list_of_returned = list(returned) - assert list_of_returned == [ - ("a/b", 1.0, [], ["e1", ("e2", "v2")]), - ("c/d", 1.0, [], []), - ("e/f;p1=v1", 0.0, [("p1", "v1")], ["e1"]), - ("g/h;p1=v1;p2=v2", 0.5, [("p1", "v1"), ("p2", "v2")], []), - ] - - def test___init__(self): - instance = AcceptNoHeader() - assert instance.header_value is None - assert instance.parsed is None - assert instance._parsed_nonzero is None - assert isinstance(instance, Accept) - - def test___add___None(self): - left_operand = AcceptNoHeader() - result = left_operand + None - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize( - "right_operand", - [ - ", ", - [", "], - (", ",), - {", ": 1.0}, - {", ;level=1": (1.0, ";e1=1")}, - "a/b, c/d;q=1;e1;", - ["a/b", "c/d;q=1;e1;"], - ("a/b", "c/d;q=1;e1;"), - {"a/b": 1.0, "cd": 1.0}, - {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, - ], - ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize("str_", [", ", "a/b, c/d;q=1;e1;"]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptNoHeader() - - class Other: - def __str__(self): - return str_ - - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___add___valid_empty_value(self, value): - left_operand = AcceptNoHeader() - result = left_operand + value - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" - - def test___add___other_type_with_valid___str___empty(self): - left_operand = AcceptNoHeader() - - class Other: - def __str__(self): - return "" - - result = left_operand + Other() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" - - @pytest.mark.parametrize( - "value, value_as_header", - [ - # str - ( - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of strs - ( - ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 3-item tuples, with extension parameters - ( - [ - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 2-item tuples, without extension parameters - ( - [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # list of a mixture of strs, 3-item tuples and 2-item tuples - ( - [ - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of strs - ( - ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 3-item tuples, with extension parameters - ( - ( - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 2-item tuples, without extension parameters - ( - (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # tuple of a mixture of strs, 3-item tuples and 2-item tuples - ( - ( - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # dict - ( - {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, - "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", - ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - result = AcceptNoHeader() + value - assert isinstance(result, AcceptValidHeader) - assert result.header_value == value_as_header - - def test___add___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - - right_operand = Other() - result = AcceptNoHeader() + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == str(right_operand) - - def test___add___AcceptValidHeader_header_value_empty(self): - right_operand = AcceptValidHeader(header_value="") - result = AcceptNoHeader() + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___add___AcceptValidHeader_header_value_not_empty(self): - right_operand = AcceptValidHeader(header_value=",\t ,i/j, k/l;q=0.333,") - result = AcceptNoHeader() + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - - def test___add___AcceptNoHeader(self): - left_operand = AcceptNoHeader() - right_operand = AcceptNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - assert result is not left_operand - assert result is not right_operand - - @pytest.mark.parametrize("header_value", [", ", 'a/b;p1=1;p2=2;q=0.8;e1;e2="']) - def test___add___AcceptInvalidHeader(self, header_value): - left_operand = AcceptNoHeader() - result = left_operand + AcceptInvalidHeader(header_value=header_value) - assert isinstance(result, AcceptNoHeader) - assert result is not left_operand - - def test___bool__(self): - instance = AcceptNoHeader() - returned = bool(instance) - assert returned is False - - @pytest.mark.filterwarnings(IGNORE_CONTAINS) - def test___contains__(self): - instance = AcceptNoHeader() - returned = "type/subtype" in instance - assert returned is True - - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptNoHeader() - returned = list(instance) - assert returned == [] - - def test___radd___None(self): - right_operand = AcceptNoHeader() - result = None + right_operand - assert isinstance(result, AcceptNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize( - "left_operand", - [ - ", ", - [", "], - (", ",), - {", ": 1.0}, - {", ;level=1": (1.0, ";e1=1")}, - "a/b, c/d;q=1;e1;", - ["a/b", "c/d;q=1;e1;"], - ("a/b", "c/d;q=1;e1;"), - {"a/b": 1.0, "cd": 1.0}, - {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, - ], - ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("str_", [", ", "a/b, c/d;q=1;e1;"]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptNoHeader() - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___radd___valid_empty_value(self, value): - result = value + AcceptNoHeader() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" - - def test___radd___other_type_with_valid___str___empty(self): - class Other: - def __str__(self): - return "" - - result = Other() + AcceptNoHeader() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" - - @pytest.mark.parametrize( - "value, value_as_header", - [ - # str - ( - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of strs - ( - ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 3-item tuples, with extension parameters - ( - [ - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 2-item tuples, without extension parameters - ( - [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # list of a mixture of strs, 3-item tuples and 2-item tuples - ( - [ - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of strs - ( - ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 3-item tuples, with extension parameters - ( - ( - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 2-item tuples, without extension parameters - ( - (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # tuple of a mixture of strs, 3-item tuples and 2-item tuples - ( - ( - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # dict - ( - {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, - "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", - ), - ], - ) - def test___radd___valid_non_empty_value(self, value, value_as_header): - result = value + AcceptNoHeader() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == value_as_header - - def test___radd___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - - left_operand = Other() - result = left_operand + AcceptNoHeader() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == str(left_operand) - - def test___repr__(self): - instance = AcceptNoHeader() - assert repr(instance) == "" - - def test___str__(self): - instance = AcceptNoHeader() - assert str(instance) == "" - - def test_accept_html(self): - instance = AcceptNoHeader() - assert instance.accept_html() is True - - def test_accepts_html(self): - instance = AcceptNoHeader() - assert instance.accepts_html is True - - @pytest.mark.parametrize( - "offers, expected_returned", - [ - (["text/html;p=1;q=0.5"], []), - (["text/html;q=0.5"], []), - (["text/html;q=0.5;e=1"], []), - (["text/html", "text/plain;p=1;q=0.5;e=1", "foo"], [("text/html", 1.0)]), - ], - ) - def test_acceptable_offers__invalid_offers(self, offers, expected_returned): - assert AcceptNoHeader().acceptable_offers(offers=offers) == expected_returned - - def test_acceptable_offers__valid_offers(self): - instance = AcceptNoHeader() - returned = instance.acceptable_offers(offers=["a/b", "c/d", "e/f"]) - assert returned == [("a/b", 1.0), ("c/d", 1.0), ("e/f", 1.0)] - - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) - def test_best_match(self): - accept = AcceptNoHeader() - assert accept.best_match(["text/html", "audio/basic"]) == "text/html" - assert ( - accept.best_match([("text/html", 1), ("audio/basic", 0.5)]) == "text/html" - ) - assert ( - accept.best_match([("text/html", 0.5), ("audio/basic", 1)]) == "audio/basic" - ) - assert accept.best_match([("text/html", 0.5), "audio/basic"]) == "audio/basic" - assert ( - accept.best_match([("text/html", 0.5), "audio/basic"], default_match=True) - == "audio/basic" - ) - assert ( - accept.best_match([("text/html", 0.5), "audio/basic"], default_match=False) - == "audio/basic" - ) - assert accept.best_match([], default_match="fallback") == "fallback" - - @pytest.mark.filterwarnings(IGNORE_QUALITY) - def test_quality(self): - instance = AcceptNoHeader() - returned = instance.quality(offer="type/subtype") - assert returned == 1.0 - - -class TestAcceptInvalidHeader: - def test_parse__inherited(self): - returned = AcceptInvalidHeader.parse( - value=( - ",\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, " - + "g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t," - ) - ) - list_of_returned = list(returned) - assert list_of_returned == [ - ("a/b", 1.0, [], ["e1", ("e2", "v2")]), - ("c/d", 1.0, [], []), - ("e/f;p1=v1", 0.0, [("p1", "v1")], ["e1"]), - ("g/h;p1=v1;p2=v2", 0.5, [("p1", "v1"), ("p2", "v2")], []), - ] - - def test___init__(self): - header_value = ", " - instance = AcceptInvalidHeader(header_value=header_value) - assert instance.header_value == header_value - assert instance.parsed is None - assert instance._parsed_nonzero is None - assert isinstance(instance, Accept) - - def test___add___None(self): - left_operand = AcceptInvalidHeader(header_value=", ") - result = left_operand + None - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize( - "right_operand", - [ - ", ", - [", "], - (", ",), - {", ": 1.0}, - {", ;level=1": (1.0, ";e1=1")}, - "a/b, c/d;q=1;e1;", - ["a/b", "c/d;q=1;e1;"], - ("a/b", "c/d;q=1;e1;"), - {"a/b": 1.0, "cd": 1.0}, - {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, - ], - ) - def test___add___invalid_value(self, right_operand): - left_operand = AcceptInvalidHeader(header_value="invalid header") - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize("str_", [", ", "a/b, c/d;q=1;e1;"]) - def test___add___other_type_with_invalid___str__(self, str_): - left_operand = AcceptInvalidHeader(header_value="invalid header") - - class Other: - def __str__(self): - return str_ - - right_operand = Other() - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___add___valid_empty_value(self, value): - left_operand = AcceptInvalidHeader(header_value=", ") - result = left_operand + value - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" - - def test___add___other_type_with_valid___str___empty(self): - left_operand = AcceptInvalidHeader(header_value=", ") - - class Other: - def __str__(self): - return "" - - result = left_operand + Other() - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" - - @pytest.mark.parametrize( - "value, value_as_header", - [ - # str - ( - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of strs - ( - ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 3-item tuples, with extension parameters - ( - [ - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 2-item tuples, without extension parameters - ( - [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # list of a mixture of strs, 3-item tuples and 2-item tuples - ( - [ - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of strs - ( - ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 3-item tuples, with extension parameters - ( - ( - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 2-item tuples, without extension parameters - ( - (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # tuple of a mixture of strs, 3-item tuples and 2-item tuples - ( - ( - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # dict - ( - {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, - "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", - ), - ], - ) - def test___add___valid_value(self, value, value_as_header): - result = AcceptInvalidHeader(header_value=", ") + value - assert isinstance(result, AcceptValidHeader) - assert result.header_value == value_as_header - - def test___add___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - - right_operand = Other() - result = AcceptInvalidHeader(header_value=", ") + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == str(right_operand) - - def test___add___AcceptValidHeader_header_value_empty(self): - left_operand = AcceptInvalidHeader(header_value=", ") - right_operand = AcceptValidHeader(header_value="") - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - assert result is not right_operand - - def test___add___AcceptValidHeader_header_value_not_empty(self): - left_operand = AcceptInvalidHeader(header_value=", ") - right_operand = AcceptValidHeader(header_value=",\t ,i/j, k/l;q=0.333,") - result = left_operand + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == right_operand.header_value - - def test___add___AcceptNoHeader(self): - left_operand = AcceptInvalidHeader(header_value=", ") - right_operand = AcceptNoHeader() - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - assert result is not right_operand - - @pytest.mark.parametrize("header_value", [", ", 'a/b;p1=1;p2=2;q=0.8;e1;e2="']) - def test___add___AcceptInvalidHeader(self, header_value): - result = AcceptInvalidHeader(header_value=", ") + AcceptInvalidHeader( - header_value=header_value - ) - assert isinstance(result, AcceptNoHeader) +class TestAccept__missing: + def test___init__(self): + instance = Accept(None) + assert instance.header_value is None + assert instance.parsed is None + assert instance.header_state == HeaderState.Missing def test___bool__(self): - instance = AcceptInvalidHeader(header_value=", ") + instance = Accept(None) returned = bool(instance) assert returned is False - @pytest.mark.filterwarnings(IGNORE_CONTAINS) + def test___repr__(self): + instance = Accept(None) + assert repr(instance) == "" + + def test___str__(self): + instance = Accept(None) + assert str(instance) == "" + + def test_copy(self): + instance = Accept(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___contains__(self): - instance = AcceptInvalidHeader(header_value=", ") - returned = "type/subtype" in instance - assert returned is True + accept = Accept(None) + assert "text/subtype" in accept - @pytest.mark.filterwarnings(IGNORE_ITER) - def test___iter__(self): - instance = AcceptInvalidHeader(header_value=", ") - returned = list(instance) - assert returned == [] + def test_accept_html(self): + instance = Accept(None) + assert instance.accept_html() is True - def test___radd___None(self): - right_operand = AcceptInvalidHeader(header_value=", ") - result = None + right_operand - assert isinstance(result, AcceptNoHeader) + def test_accepts_html(self): + instance = Accept(None) + assert instance.accepts_html is True @pytest.mark.parametrize( - "left_operand", + "offers, expected_returned", [ - ", ", - [", "], - (", ",), - {", ": 1.0}, - {", ;level=1": (1.0, ";e1=1")}, - "a/b, c/d;q=1;e1;", - ["a/b", "c/d;q=1;e1;"], - ("a/b", "c/d;q=1;e1;"), - {"a/b": 1.0, "cd": 1.0}, - {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, + (["text/html;p=1;q=0.5"], []), + (["text/html;q=0.5"], []), + (["text/html;q=0.5;e=1"], []), + (["text/html", "text/plain;p=1;q=0.5;e=1", "foo"], [("text/html", 1.0)]), ], ) - def test___radd___invalid_value(self, left_operand): - right_operand = AcceptInvalidHeader(header_value=", ") - result = left_operand + right_operand - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize("str_", [", ", "a/b, c/d;q=1;e1;"]) - def test___radd___other_type_with_invalid___str__(self, str_): - right_operand = AcceptInvalidHeader(header_value=", ") - - class Other: - def __str__(self): - return str_ - - result = Other() + right_operand - assert isinstance(result, AcceptNoHeader) - - @pytest.mark.parametrize("value", ["", [], (), {}]) - def test___radd___valid_empty_value(self, value): - right_operand = AcceptInvalidHeader(header_value="invalid header") - result = value + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" + def test_acceptable_offers__invalid_offers(self, offers, expected_returned): + result = Accept(None).acceptable_offers(offers=offers) + assert result == expected_returned - def test___radd___other_type_with_valid___str___empty(self): - right_operand = AcceptInvalidHeader(header_value="invalid header") + def test_acceptable_offers__valid_offers(self): + instance = Accept(None) + returned = instance.acceptable_offers(offers=["a/b", "c/d", "e/f"]) + assert returned == [("a/b", 1.0), ("c/d", 1.0), ("e/f", 1.0)] - class Other: - def __str__(self): - return "" + def test_best_match(self): + accept = Accept(None) + assert accept.best_match(["text/html", "audio/basic"]) == "text/html" + assert accept.best_match(["audio/basic", "text/html"]) == "audio/basic" + assert accept.best_match([], default_match="fallback") == "fallback" - result = Other() + right_operand - assert isinstance(result, AcceptValidHeader) - assert result.header_value == "" + def test_quality(self): + accept = Accept(None) + assert accept.quality("text/subtype") == 1.0 - @pytest.mark.parametrize( - "value, value_as_header", - [ - # str - ( - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of strs - ( - ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 3-item tuples, with extension parameters - ( - [ - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # list of 2-item tuples, without extension parameters - ( - [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # list of a mixture of strs, 3-item tuples and 2-item tuples - ( - [ - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ], - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of strs - ( - ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 3-item tuples, with extension parameters - ( - ( - ("a/b", 0.5, ""), - ("c/d;p1=1", 0.0, ""), - ("e/f", 1.0, ""), - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # tuple of 2-item tuples, without extension parameters - ( - (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", - ), - # tuple of a mixture of strs, 3-item tuples and 2-item tuples - ( - ( - ("a/b", 0.5), - ("c/d;p1=1", 0.0, ""), - "e/f", - ("g/h;p1=1", 1.0, ";e1=1"), - ), - "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", - ), - # dict - ( - {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, - "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", - ), - ], - ) - def test___radd___valid_non_empty_value(self, value, value_as_header): - result = value + AcceptInvalidHeader(header_value="invalid header") - assert isinstance(result, AcceptValidHeader) - assert result.header_value == value_as_header - def test___radd___other_type_with_valid___str___not_empty(self): - class Other: - def __str__(self): - return "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" +class TestAccept__invalid: + def test___init__(self): + header_value = ", " + instance = Accept(header_value) + assert instance.header_value == header_value + assert instance.parsed is None + assert instance.header_state == HeaderState.Invalid - left_operand = Other() - result = left_operand + AcceptInvalidHeader(header_value="invalid header") - assert isinstance(result, AcceptValidHeader) - assert result.header_value == str(left_operand) + def test___bool__(self): + instance = Accept(", ") + returned = bool(instance) + assert returned is False def test___repr__(self): - instance = AcceptInvalidHeader(header_value="\x00") - assert repr(instance) == "" + instance = Accept("\x00") + assert repr(instance) == "" def test___str__(self): - instance = AcceptInvalidHeader(header_value=", ") + instance = Accept(", ") assert str(instance) == "" + def test_copy(self): + instance = Accept(", ") + 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___contains__(self): + accept = Accept(", ") + assert "text/subtype" in accept + def test_accept_html(self): - instance = AcceptInvalidHeader(header_value=", ") + instance = Accept(", ") assert instance.accept_html() is True def test_accepts_html(self): - instance = AcceptInvalidHeader(header_value=", ") + instance = Accept(", ") assert instance.accepts_html is True @pytest.mark.parametrize( @@ -2042,71 +908,304 @@ def test_accepts_html(self): ], ) def test_acceptable_offers__invalid_offers(self, offers, expected_returned): - assert ( - AcceptInvalidHeader(header_value=", ").acceptable_offers(offers=offers) - == expected_returned - ) + assert Accept(", ").acceptable_offers(offers=offers) == expected_returned def test_acceptable_offers__valid_offers(self): - instance = AcceptInvalidHeader(header_value=", ") + instance = Accept(", ") returned = instance.acceptable_offers(offers=["a/b", "c/d", "e/f"]) assert returned == [("a/b", 1.0), ("c/d", 1.0), ("e/f", 1.0)] - @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): - accept = AcceptInvalidHeader(header_value=", ") + accept = Accept(", ") assert accept.best_match(["text/html", "audio/basic"]) == "text/html" - assert ( - accept.best_match([("text/html", 1), ("audio/basic", 0.5)]) == "text/html" - ) - assert ( - accept.best_match([("text/html", 0.5), ("audio/basic", 1)]) == "audio/basic" - ) - assert accept.best_match([("text/html", 0.5), "audio/basic"]) == "audio/basic" - assert ( - accept.best_match([("text/html", 0.5), "audio/basic"], default_match=True) - == "audio/basic" - ) - assert ( - accept.best_match([("text/html", 0.5), "audio/basic"], default_match=False) - == "audio/basic" - ) + assert accept.best_match(["audio/basic", "text/html"]) == "audio/basic" assert accept.best_match([], default_match="fallback") == "fallback" - @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): - instance = AcceptInvalidHeader(header_value=", ") - returned = instance.quality(offer="type/subtype") - assert returned == 1.0 + accept = Accept(None) + assert accept.quality("text/subtype") == 1.0 + + +class TestAccept__add: + invalid_values = [ + ", ", + [", "], + (", ",), + {", ": 1.0}, + {", ;level=1": (1.0, ";e1=1")}, + "a/b, c/d;q=1;e1;", + ["a/b", "c/d;q=1;e1;"], + ("a/b", "c/d;q=1;e1;"), + {"a/b": 1.0, "cd": 1.0}, + {"a/b": (1.0, ";e1=1"), "c/d": (1.0, ";e2=2;")}, + StringMe(", "), + StringMe("a/b, c/d;q=1;e1;"), + ] + + valid_nonempty_values_with_headers = [ + # str + ( + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # object with __str__ + ( + StringMe("a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1"), + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # list of strs + ( + ["a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"], + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # list of 3-item tuples, with extension parameters + ( + [ + ("a/b", 0.5, ""), + ("c/d;p1=1", 0.0, ""), + ("e/f", 1.0, ""), + ("g/h;p1=1", 1.0, ";e1=1"), + ], + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # list of 2-item tuples, without extension parameters + ( + [("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)], + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", + ), + # list of a mixture of strs, 3-item tuples and 2-item tuples + ( + [ + ("a/b", 0.5), + ("c/d;p1=1", 0.0, ""), + "e/f", + ("g/h;p1=1", 1.0, ";e1=1"), + ], + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # tuple of strs + ( + ("a/b;q=0.5", "c/d;p1=1;q=0", "e/f", "g/h;p1=1;q=1;e1=1"), + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # tuple of 3-item tuples, with extension parameters + ( + ( + ("a/b", 0.5, ""), + ("c/d;p1=1", 0.0, ""), + ("e/f", 1.0, ""), + ("g/h;p1=1", 1.0, ";e1=1"), + ), + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # tuple of 2-item tuples, without extension parameters + ( + (("a/b", 0.5), ("c/d;p1=1", 0.0), ("e/f", 1.0), ("g/h;p1=1", 1.0)), + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1", + ), + # tuple of a mixture of strs, 3-item tuples and 2-item tuples + ( + ( + ("a/b", 0.5), + ("c/d;p1=1", 0.0, ""), + "e/f", + ("g/h;p1=1", 1.0, ";e1=1"), + ), + "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1", + ), + # dict + ( + {"a/b": (0.5, ";e1=1"), "c/d": 0.0, "e/f;p1=1": (1.0, ";e1=1;e2=2")}, + "e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0", + ), + ] + + valid_empty_values = ["", [], (), {}, StringMe("")] + + valid_values_with_headers = valid_nonempty_values_with_headers + [ + [x, ""] for x in valid_empty_values + ] + # snapshots help confirm the instance is immutable + def snapshot_instance(self, inst): + return deepcopy( + { + "header_value": inst.header_value, + "parsed": inst.parsed, + "header_state": inst.header_state, + } + ) + + # we want to test math with primitive python values and Accept instances + @pytest.fixture(params=["primitive", "instance"]) + def maker(self, request): + if request.param == "primitive": + return lambda x: x + return Accept + + # almost always add and radd are symmetrical so we can test both and + # expect the same result + @pytest.fixture(params=["add", "radd"]) + def fn(self, request): + if request.param == "add": + return lambda x, y: x + y + return lambda x, y: y + x + + @pytest.mark.parametrize( + "input_value, input_header", + valid_values_with_headers, + ) + def test_valid_add_missing(self, input_value, input_header, maker, fn): + inst = Accept(input_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == input_header + + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == input_header + + def test_invalid_add_missing(self, maker, fn): + inst = Accept("invalid") + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == "invalid" + + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + def test_missing_add_missing(self, maker, fn): + inst = Accept(None) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Missing + assert inst.header_value is None + + result = fn(inst, maker(None)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + @pytest.mark.parametrize("valid_value, valid_header", valid_values_with_headers) + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_valid_add_invalid( + self, valid_value, valid_header, invalid_value, maker, fn + ): + inst = Accept(valid_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == valid_header + + result = fn(inst, maker(invalid_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Valid + assert result.header_value == valid_header + + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_invalid_add_invalid(self, invalid_value, maker, fn): + inst = Accept("invalid") + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Invalid + assert inst.header_value == "invalid" + + result = fn(inst, maker(invalid_value)) + assert snap == self.snapshot_instance(inst) + assert result.header_state == HeaderState.Missing + assert result.header_value is None + + @pytest.mark.parametrize("invalid_value", invalid_values) + def test_missing_add_invalid(self, invalid_value, maker, fn): + inst = Accept(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_nonempty_values_with_headers, + ) + def test_nonempty_valid_add_valid(self, input_value, input_header, maker): + seed_value = ",\t ,i/j, k/l;q=0.333," + inst = Accept(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 TestCreateAcceptHeader: + @pytest.mark.parametrize( + "input_value, input_header", + valid_nonempty_values_with_headers, + ) + @pytest.mark.parametrize("empty_value", valid_empty_values) + def test_nonempty_valid_add_empty( + self, input_value, input_header, empty_value, maker, fn + ): + inst = Accept(input_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == input_header + + 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.parametrize("empty_value", valid_empty_values) + def test_empty_valid_add_empty(self, empty_value, maker, fn): + expected_value = "" + inst = Accept(empty_value) + snap = self.snapshot_instance(inst) + assert inst.header_state == HeaderState.Valid + assert inst.header_value == expected_value + + 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 TestCreateAccept: def test_header_value_is_None(self): - header_value = None - returned = create_accept_header(header_value=header_value) - assert isinstance(returned, AcceptNoHeader) - assert returned.header_value == header_value + returned = create_accept_header(None) + assert returned.header_state == HeaderState.Missing + assert returned.header_value is None returned2 = create_accept_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2 is returned + assert returned2.header_value is None def test_header_value_is_valid(self): header_value = "text/html, text/plain;q=0.9" - returned = create_accept_header(header_value=header_value) - assert isinstance(returned, AcceptValidHeader) + returned = create_accept_header(header_value) + assert returned.header_state == HeaderState.Valid assert returned.header_value == header_value returned2 = create_accept_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2 is returned + assert returned2.header_value == header_value @pytest.mark.parametrize("header_value", [", ", "noslash"]) def test_header_value_is_invalid(self, header_value): - returned = create_accept_header(header_value=header_value) - assert isinstance(returned, AcceptInvalidHeader) + returned = create_accept_header(header_value) + assert returned.header_state == HeaderState.Invalid assert returned.header_value == header_value returned2 = create_accept_header(returned) - assert returned2 is not returned - assert returned2._header_value == returned._header_value + assert returned2 is returned + assert returned2.header_value == header_value class TestAcceptProperty: @@ -2115,21 +1214,21 @@ def test_fget_header_is_valid(self): request = Request.blank("/", environ={"HTTP_ACCEPT": header_value}) property_ = accept_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptValidHeader) + assert returned.header_state is HeaderState.Valid assert returned.header_value == header_value def test_fget_header_is_None(self): request = Request.blank("/", environ={"HTTP_ACCEPT": None}) property_ = accept_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptNoHeader) + assert returned.header_state is HeaderState.Missing def test_fget_header_is_invalid(self): header_value = "invalid" request = Request.blank("/", environ={"HTTP_ACCEPT": header_value}) property_ = accept_property() returned = property_.fget(request=request) - assert isinstance(returned, AcceptInvalidHeader) + assert returned.header_state is HeaderState.Invalid assert returned.header_value == header_value def test_fset_value_is_valid(self): @@ -2243,34 +1342,29 @@ def test_fset_value_types(self, value, value_as_header): def test_fset_other_type_with___str__(self, header_value): request = Request.blank("/", environ={"HTTP_ACCEPT": "text/html"}) property_ = accept_property() - - class Other: - def __str__(self): - return header_value - - value = Other() + value = StringMe(header_value) property_.fset(request=request, value=value) assert request.environ["HTTP_ACCEPT"] == str(value) - def test_fset_AcceptValidHeader(self): + def test_fset_valid_Accept(self): request = Request.blank("/", environ={}) header_value = "a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1" - header = AcceptValidHeader(header_value=header_value) + header = Accept(header_value) property_ = accept_property() property_.fset(request=request, value=header) assert request.environ["HTTP_ACCEPT"] == header.header_value - def test_fset_AcceptNoHeader(self): + def test_fset_missing_Accept(self): request = Request.blank("/", environ={"HTTP_ACCEPT": "text/html"}) property_ = accept_property() - header = AcceptNoHeader() + header = Accept(None) property_.fset(request=request, value=header) assert "HTTP_ACCEPT" not in request.environ - def test_fset_AcceptInvalidHeader(self): + def test_fset_invalid_Accept(self): request = Request.blank("/", environ={}) header_value = "invalid" - header = AcceptInvalidHeader(header_value=header_value) + header = Accept(header_value) property_ = accept_property() property_.fset(request=request, value=header) assert request.environ["HTTP_ACCEPT"] == header.header_value @@ -5770,114 +4864,3 @@ def test_fdel_header_key_not_in_environ(self): property_.fdel(request=request) assert isinstance(request.accept_language, AcceptLanguageNoHeader) assert "HTTP_ACCEPT_LANGUAGE" not in request.environ - - -# Deprecated tests: - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_init_warns(): - with warnings.catch_warnings(record=True) as warning: - warnings.simplefilter("always") - MIMEAccept("image/jpg") - - assert len(warning) == 1 - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_init(): - mimeaccept = MIMEAccept("image/jpg") - assert mimeaccept._parsed == [("image/jpg", 1)] - mimeaccept = MIMEAccept("image/png, image/jpg;q=0.5") - assert mimeaccept._parsed == [("image/png", 1), ("image/jpg", 0.5)] - mimeaccept = MIMEAccept("image, image/jpg;q=0.5") - assert mimeaccept._parsed == [] - mimeaccept = MIMEAccept("*/*") - assert mimeaccept._parsed == [("*/*", 1)] - mimeaccept = MIMEAccept("*/png") - assert mimeaccept._parsed == [("*/png", 1)] - mimeaccept = MIMEAccept("image/pn*") - assert mimeaccept._parsed == [("image/pn*", 1.0)] - mimeaccept = MIMEAccept("image/*") - assert mimeaccept._parsed == [("image/*", 1)] - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -@pytest.mark.filterwarnings(IGNORE_CONTAINS) -def test_MIMEAccept_parse(): - assert list(MIMEAccept.parse("image/jpg")) == [("image/jpg", 1)] - assert list(MIMEAccept.parse("invalid")) == [] - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_accept_html(): - mimeaccept = MIMEAccept("image/jpg") - assert not mimeaccept.accept_html() - mimeaccept = MIMEAccept("image/jpg, text/html") - assert mimeaccept.accept_html() - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -@pytest.mark.filterwarnings(IGNORE_CONTAINS) -def test_MIMEAccept_contains(): - mimeaccept = MIMEAccept("A/a, B/b, C/c") - assert "A/a" in mimeaccept - assert "A/*" in mimeaccept - assert "*/a" in mimeaccept - assert "A/b" not in mimeaccept - assert "B/a" not in mimeaccept - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -@pytest.mark.filterwarnings(IGNORE_BEST_MATCH) -def test_MIMEAccept_json(): - mimeaccept = MIMEAccept("text/html, */*; q=.2") - assert mimeaccept.best_match(["application/json"]) == "application/json" - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_no_raise_invalid(): - assert MIMEAccept("invalid") - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -@pytest.mark.filterwarnings(IGNORE_ITER) -def test_MIMEAccept_iter(): - assert list(iter(MIMEAccept("text/html, other/whatever"))) == [ - "text/html", - "other/whatever", - ] - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_str(): - assert str(MIMEAccept("image/jpg")) == "image/jpg" - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_add(): - assert str(MIMEAccept("image/jpg") + "image/png") == "image/jpg, image/png" - assert ( - str(MIMEAccept("image/jpg") + MIMEAccept("image/png")) == "image/jpg, image/png" - ) - assert isinstance(MIMEAccept("image/jpg") + "image/png", MIMEAccept) - assert isinstance(MIMEAccept("image/jpg") + MIMEAccept("image/png"), MIMEAccept) - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -def test_MIMEAccept_radd(): - assert str("image/png" + MIMEAccept("image/jpg")) == "image/png, image/jpg" - assert isinstance("image/png" + MIMEAccept("image/jpg"), MIMEAccept) - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -@pytest.mark.filterwarnings(IGNORE_CONTAINS) -def test_MIMEAccept_repr(): - assert "image/jpg" in repr(MIMEAccept("image/jpg")) - - -@pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) -@pytest.mark.filterwarnings(IGNORE_QUALITY) -def test_MIMEAccept_quality(): - assert MIMEAccept("image/jpg;q=0.9").quality("image/jpg") == 0.9 - assert MIMEAccept("image/png;q=0.9").quality("image/jpg") is None diff --git a/tests/test_request.py b/tests/test_request.py index 86fbdfbd..4366907a 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -6,18 +6,17 @@ import pytest from webob.acceptparse import ( + Accept, AcceptCharsetInvalidHeader, AcceptCharsetNoHeader, AcceptCharsetValidHeader, AcceptEncodingInvalidHeader, AcceptEncodingNoHeader, AcceptEncodingValidHeader, - AcceptInvalidHeader, AcceptLanguageInvalidHeader, AcceptLanguageNoHeader, AcceptLanguageValidHeader, - AcceptNoHeader, - AcceptValidHeader, + HeaderState as AcceptHeaderState, ) from webob.multidict import NoVars from webob.util import bytes_, text_ @@ -741,21 +740,24 @@ def test_is_body_readable_special_flag(self): def test_accept_no_header(self): req = self._makeOne(environ={}) header = req.accept - assert isinstance(header, AcceptNoHeader) + assert isinstance(header, Accept) + assert header.header_state is AcceptHeaderState.Missing assert header.header_value is None def test_accept_invalid_header(self): header_value = "text/html;param=val;q=1;extparam=\x19" req = self._makeOne(environ={"HTTP_ACCEPT": header_value}) header = req.accept - assert isinstance(header, AcceptInvalidHeader) + assert isinstance(header, Accept) + assert header.header_state is AcceptHeaderState.Invalid assert header.header_value == header_value def test_accept_valid_header(self): header_value = ',,text/html;p1="v1";p2=v2;q=0.9;e1="v1";e2;e3=v3,' req = self._makeOne(environ={"HTTP_ACCEPT": header_value}) header = req.accept - assert isinstance(header, AcceptValidHeader) + assert isinstance(header, Accept) + assert header.header_state is AcceptHeaderState.Valid assert header.header_value == header_value # accept_charset @@ -1990,7 +1992,8 @@ def test_from_mimeparse(self): tests = [ ("image/png", "image/png"), ("image/*", "image/png"), - ("image/*, application/xml", "application/xml"), + ("image/*, application/xml", "image/png"), + ("image/jpeg, application/xml", "application/xml"), ] for accept, get in tests: @@ -2703,7 +2706,6 @@ def test_request_put(self): from datetime import datetime from webob import UTC, Response - from webob.acceptparse import Accept from webob.byterange import Range from webob.etag import ETagMatcher from webob.multidict import GetDict, MultiDict