From d150f9ff265e2b98f304e329406bc4e8afa871ba Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 12 Sep 2018 22:48:27 -0500 Subject: [PATCH 1/8] support a standard api for parsing media types --- src/webob/acceptparse.py | 51 ++++++++++++++++++++++++++++++++++++--- tests/test_acceptparse.py | 27 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 04aabfcd..9fe3f7c7 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -407,9 +407,52 @@ def generator(value): ) return generator(value=value) - def _parse_and_normalize_offers(self, offers): + @classmethod + def parse_offer(cls, offer): + """ + Parse an offer into its component parts. + + :param offer: A media type or range in the format + ``type/subtype[;params]``. + :return: A tuple of ``(*type*, *subtype*, *params*)``. + + | *params* is a list containing ``(*parameter name*, *value*)`` + values. + + :raises ValueError: If the offer does not match the required format. + + """ + match = cls.media_type_compiled_re.match(offer) + if not match: + raise ValueError('Invalid value for an Accept offer.') + + groups = match.groups() + offer_type, offer_subtype = groups[0].split('/') + offer_params = cls._parse_media_type_params( + media_type_params_segment=groups[1], + ) + # offer_type, offer_subtype, offer_params, invalid, example + # == * == * true Y/N + # N N N N a/b + # N N Y N a/b;x=y + # N Y N N a/* + # N Y Y Y a/*;x=y + # Y N N Y */b + # Y N Y Y */b;x=y + # Y Y N N */* + # Y Y Y Y */*;x=y + # simplifies to (A and not B or B and C) + if ( + (offer_type == '*' and offer_subtype != '*') + or (offer_subtype == '*' and offer_params) + ): + raise ValueError('Invalid value for an Accept offer.') + return (offer_type, offer_subtype, offer_params) + + @classmethod + def _parse_and_normalize_offers(cls, offers): """ - Throw out any offers that do not match the media type ABNF. + Throw out any offers that do not match the media range ABNF. :return: A list of offers split into the format ``[offer_index, offer_type_subtype, offer_media_type_params]``. @@ -417,8 +460,8 @@ def _parse_and_normalize_offers(self, offers): """ lowercased_offers_parsed = [] for index, offer in enumerate(offers): - match = self.media_type_compiled_re.match(offer.lower()) - # we're willing to try to match any offer that matches the + match = cls.media_type_compiled_re.match(offer.lower()) + # we're willing to try to match any range that matches the # media type grammar can parse, but we'll throw out anything # that doesn't fit the correct syntax - this is not saying that # the media type is actually a real media type, just that it looks diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index f9d01579..2cac1c49 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -377,6 +377,33 @@ def test_parse__valid_header(self, value, expected_list): list_of_returned = list(returned) assert list_of_returned == expected_list + @pytest.mark.parametrize('offer, expected_return', [ + ['text/html', ('text', 'html', [])], + ['text/html;charset=utf8', ('text', 'html', [('charset', 'utf8')])], + ['text/html;charset=utf8;x-version=1', ('text', 'html', [ + ('charset', 'utf8'), + ('x-version', '1'), + ])], + ['text/*', ('text', '*', [])], + ['*/*', ('*', '*', [])], + ]) + def test_parse_offer__valid(self, offer, expected_return): + assert Accept.parse_offer(offer) == expected_return + + @pytest.mark.parametrize('offer', [ + '', + 'foo', + 'foo/bar/baz', + '*/plain', + '*/plain;charset=utf8', + '*/plain;charset=utf8;x-version=1', + '*/*;charset=utf8', + 'text/*;charset=utf8', + ]) + def test_parse_offer__invalid(self, offer): + with pytest.raises(ValueError): + Accept.parse_offer(offer) + class TestAcceptValidHeader(object): def test_parse__inherited(self): From 9d361a4dbdd494ceb380ccbc463e5a16638e1d64 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 25 Sep 2018 23:17:35 -0500 Subject: [PATCH 2/8] return a named tuple --- src/webob/acceptparse.py | 29 +++++++++++++++++++++++++++-- tests/test_acceptparse.py | 30 +++++++++++++++++++----------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 9fe3f7c7..7f309d5b 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -5,6 +5,7 @@ ``Accept-Language``. """ +from collections import namedtuple import re import textwrap import warnings @@ -74,6 +75,25 @@ def _list_1_or_more__compiled_re(element_re): ) +class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])): + __slots__ = () + + @property + def is_range(self): + return self.type == '*' or self.subtype == '*' + + @property + def specificity(self): + if self.params: + return 4 + elif self.subtype != '*': + return 3 + elif self.type != '*': + return 2 + else: + return 1 + + class Accept(object): """ Represent an ``Accept`` header. @@ -414,11 +434,16 @@ def parse_offer(cls, offer): :param offer: A media type or range in the format ``type/subtype[;params]``. - :return: A tuple of ``(*type*, *subtype*, *params*)``. + :return: A named tuple containing ``(*type*, *subtype*, *params*)``. | *params* is a list containing ``(*parameter name*, *value*)`` values. + | The result also supports ``is_range`` and ``specificity`` + properties. Specificity is a value from 1 to 4 where ``*/*`` + is 1, ``text/*`` is 2, ``text/html`` is 3 and + ``text/html;charset=utf8`` is 4. + :raises ValueError: If the offer does not match the required format. """ @@ -447,7 +472,7 @@ def parse_offer(cls, offer): or (offer_subtype == '*' and offer_params) ): raise ValueError('Invalid value for an Accept offer.') - return (offer_type, offer_subtype, offer_params) + return AcceptOffer(offer_type, offer_subtype, offer_params) @classmethod def _parse_and_normalize_offers(cls, offers): diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index 2cac1c49..8de15182 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -377,18 +377,26 @@ def test_parse__valid_header(self, value, expected_list): list_of_returned = list(returned) assert list_of_returned == expected_list - @pytest.mark.parametrize('offer, expected_return', [ - ['text/html', ('text', 'html', [])], - ['text/html;charset=utf8', ('text', 'html', [('charset', 'utf8')])], - ['text/html;charset=utf8;x-version=1', ('text', 'html', [ - ('charset', 'utf8'), - ('x-version', '1'), - ])], - ['text/*', ('text', '*', [])], - ['*/*', ('*', '*', [])], + @pytest.mark.parametrize('offer, expected_return, is_range, specificity', [ + ['text/html', ('text', 'html', []), False, 3], + [ + 'text/html;charset=utf8', + ('text', 'html', [('charset', 'utf8')]), + False, 4 + ], + [ + 'text/html;charset=utf8;x-version=1', + ('text', 'html', [('charset', 'utf8'), ('x-version', '1')]), + False, 4 + ], + ['text/*', ('text', '*', []), True, 2], + ['*/*', ('*', '*', []), True, 1], ]) - def test_parse_offer__valid(self, offer, expected_return): - assert Accept.parse_offer(offer) == expected_return + def test_parse_offer__valid(self, offer, expected_return, is_range, specificity): + result = Accept.parse_offer(offer) + assert result == expected_return + assert result.is_range == is_range + assert result.specificity == specificity @pytest.mark.parametrize('offer', [ '', From 8e1cccb3483927b23c13c04b724d3f48606848d1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 26 Sep 2018 21:04:10 -0500 Subject: [PATCH 3/8] more clearly define specificity constants --- src/webob/acceptparse.py | 13 +++++++++---- tests/test_acceptparse.py | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 7f309d5b..15af5373 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -78,6 +78,11 @@ def _list_1_or_more__compiled_re(element_re): class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])): __slots__ = () + SPECIFICITY_NONE = 1 # */* + SPECIFICITY_TYPE = 2 # text/* + SPECIFICITY_SUBTYPE = 3 # text/html + SPECIFICITY_PARAMS = 4 # text/html;charset=utf8 + @property def is_range(self): return self.type == '*' or self.subtype == '*' @@ -85,13 +90,13 @@ def is_range(self): @property def specificity(self): if self.params: - return 4 + return self.SPECIFICITY_PARAMS elif self.subtype != '*': - return 3 + return self.SPECIFICITY_SUBTYPE elif self.type != '*': - return 2 + return self.SPECIFICITY_TYPE else: - return 1 + return self.SPECIFICITY_NONE class Accept(object): diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index 8de15182..2a175003 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -382,12 +382,12 @@ def test_parse__valid_header(self, value, expected_list): [ 'text/html;charset=utf8', ('text', 'html', [('charset', 'utf8')]), - False, 4 + False, 4, ], [ 'text/html;charset=utf8;x-version=1', ('text', 'html', [('charset', 'utf8'), ('x-version', '1')]), - False, 4 + False, 4, ], ['text/*', ('text', '*', []), True, 2], ['*/*', ('*', '*', []), True, 1], From 258295df717898ac75b13415b4dbae424d21da04 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 2 Oct 2018 01:55:56 -0500 Subject: [PATCH 4/8] use Accept.parse_offer to be consistent across the codebase --- src/webob/acceptparse.py | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 15af5373..97c4fb84 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -485,20 +485,17 @@ def _parse_and_normalize_offers(cls, offers): Throw out any offers that do not match the media range ABNF. :return: A list of offers split into the format ``[offer_index, - offer_type_subtype, offer_media_type_params]``. + offer_type, offer_subtype, offer_media_type_params]``. """ - lowercased_offers_parsed = [] + parsed_offers = [] for index, offer in enumerate(offers): - match = cls.media_type_compiled_re.match(offer.lower()) - # we're willing to try to match any range that matches the - # media type grammar can parse, but we'll throw out anything - # that doesn't fit the correct syntax - this is not saying that - # the media type is actually a real media type, just that it looks - # like one - if match: - lowercased_offers_parsed.append([index] + list(match.groups())) - return lowercased_offers_parsed + try: + parsed_offer = cls.parse_offer(offer.lower()) + except ValueError: + continue + parsed_offers.append([index] + list(parsed_offer)) + return parsed_offers class AcceptValidHeader(Accept): @@ -896,13 +893,9 @@ def acceptable_offers(self, offers): acceptable_offers_n_quality_factors = {} for ( - offer_index, offer_type_subtype, offer_media_type_params + offer_index, offer_type, offer_subtype, offer_media_type_params ) in lowercased_offers_parsed: offer = offers[offer_index] - offer_type, offer_subtype = offer_type_subtype.split('/', 1) - offer_media_type_params = self._parse_media_type_params( - media_type_params_segment=offer_media_type_params, - ) offer_is_range = '*' in offer for ( range_type_subtype, range_qvalue, range_media_type_params, __, @@ -914,8 +907,17 @@ def acceptable_offers(self, offers): # highest matching qvalue if offer_is_range: if ( - offer_type_subtype == '*/*' - or offer_type == range_type and offer_subtype == '*' + # Accept: anything, offer=*/* + (offer_type == '*' and offer_subtype == '*') + + # Accept: text/anything, offer=*/* + or (offer_type == range_type and offer_subtype == '*') + + # Accept: */*, offer=anything + or ( + range_type == '*' and range_subtype == '*' + and range_media_type_params == [] + ) ): prev_match = acceptable_offers_n_quality_factors.get(offer) if not prev_match or prev_match[0] < range_qvalue: @@ -933,7 +935,10 @@ def acceptable_offers(self, offers): # items in reverse order, so specificity 4, 3, 2, 1 correspond # to 1, 2, 3, 4 in the list, respectively (so that higher # specificity has higher precedence). - elif offer_type_subtype == range_type_subtype: + elif ( + offer_type == range_type + and offer_subtype == range_subtype + ): if range_media_type_params == []: # If offer_media_type_params == [], the offer and the # range match exactly, with neither having media type @@ -1364,7 +1369,7 @@ def acceptable_offers(self, offers): """ return [ (offers[offer_index], 1.0) - for offer_index, _, _ + for offer_index, _, _, _ # avoid returning any offers that don't match the grammar so # that the return values here are consistent with what would be # returned in AcceptValidHeader From ce6eff4c126ab96ef3212498d38e0352e45f35a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 2 Oct 2018 01:58:06 -0500 Subject: [PATCH 5/8] further simplify --- src/webob/acceptparse.py | 13 ++++++------- tests/test_acceptparse.py | 5 +++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 97c4fb84..6d402440 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -485,7 +485,7 @@ def _parse_and_normalize_offers(cls, offers): Throw out any offers that do not match the media range ABNF. :return: A list of offers split into the format ``[offer_index, - offer_type, offer_subtype, offer_media_type_params]``. + parsed_offer]``. """ parsed_offers = [] @@ -494,7 +494,7 @@ def _parse_and_normalize_offers(cls, offers): parsed_offer = cls.parse_offer(offer.lower()) except ValueError: continue - parsed_offers.append([index] + list(parsed_offer)) + parsed_offers.append([index, parsed_offer]) return parsed_offers @@ -892,11 +892,10 @@ def acceptable_offers(self, offers): lowercased_offers_parsed = self._parse_and_normalize_offers(offers) acceptable_offers_n_quality_factors = {} - for ( - offer_index, offer_type, offer_subtype, offer_media_type_params - ) in lowercased_offers_parsed: + for offer_index, parsed_offer in lowercased_offers_parsed: offer = offers[offer_index] - offer_is_range = '*' in offer + offer_is_range = parsed_offer.is_range + offer_type, offer_subtype, offer_media_type_params = parsed_offer for ( range_type_subtype, range_qvalue, range_media_type_params, __, ) in lowercased_ranges: @@ -1369,7 +1368,7 @@ def acceptable_offers(self, offers): """ return [ (offers[offer_index], 1.0) - for offer_index, _, _, _ + for offer_index, _ # avoid returning any offers that don't match the grammar so # that the return values here are consistent with what would be # returned in AcceptValidHeader diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index 2a175003..f3b099d7 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -1090,6 +1090,11 @@ def test_acceptable_offers__invalid_offers( ['text/*', '*/*', 'text/html', 'text/html;level=1', 'image/*'], [('text/*', 0.7), ('*/*', 0.7), ('text/html;level=1', 0.7)], ), + ( + '*/*', + ['text/*'], + [('text/*', 1.0)], + ), ( '', ['text/*', '*/*', 'text/html', 'text/html;level=1', 'image/*'], From 869d8af3f70d9822db9debefc2698b1f05009942 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 2 Oct 2018 02:16:48 -0500 Subject: [PATCH 6/8] fix comment --- src/webob/acceptparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 6d402440..12c0c095 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -909,7 +909,7 @@ def acceptable_offers(self, offers): # Accept: anything, offer=*/* (offer_type == '*' and offer_subtype == '*') - # Accept: text/anything, offer=*/* + # Accept: text/anything, offer=text/* or (offer_type == range_type and offer_subtype == '*') # Accept: */*, offer=anything From 14442df672e2af56d5ecc62039bcbf376a6ca8b3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 2 Oct 2018 02:19:16 -0500 Subject: [PATCH 7/8] update changelog --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 69df1dd4..05c65128 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -11,6 +11,12 @@ Feature The algorithm for matching offer ranges against header ranges is described in the documentation. See https://github.com/Pylons/webob/pull/370 +- 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``. + See https://github.com/Pylons/webob/pull/376 + Compatibility ~~~~~~~~~~~~~ From d0d538f1795c80fed9da83dda58f42921f932014 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 9 Oct 2018 20:58:46 -0500 Subject: [PATCH 8/8] remove media range support for offers see the multitude of reasons in https://github.com/Pylons/pyramid/pull/3326 the short answer is that they are fundamentally broken in that media ranges cannot properly match against any accept header with q=0 content --- CHANGES.txt | 5 --- src/webob/acceptparse.py | 88 ++++----------------------------------- tests/test_acceptparse.py | 25 ++++------- 3 files changed, 17 insertions(+), 101 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 05c65128..93ed9543 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,11 +6,6 @@ Feature - Add Request.remote_host, exposing REMOTE_HOST environment variable. -- Added support for media ranges as offers when matching against the - ``Accept`` header via ``acceptparse.AcceptValidHeader.acceptable_offers``. - The algorithm for matching offer ranges against header ranges is described - in the documentation. See https://github.com/Pylons/webob/pull/370 - - Added ``acceptparse.Accept.parse_offer`` to codify what types of offers are compatible with ``acceptparse.AcceptValidHeader.acceptable_offers``, ``acceptparse.AcceptMissingHeader.acceptable_offers``, and diff --git a/src/webob/acceptparse.py b/src/webob/acceptparse.py index 12c0c095..033bef89 100644 --- a/src/webob/acceptparse.py +++ b/src/webob/acceptparse.py @@ -75,28 +75,7 @@ def _list_1_or_more__compiled_re(element_re): ) -class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])): - __slots__ = () - - SPECIFICITY_NONE = 1 # */* - SPECIFICITY_TYPE = 2 # text/* - SPECIFICITY_SUBTYPE = 3 # text/html - SPECIFICITY_PARAMS = 4 # text/html;charset=utf8 - - @property - def is_range(self): - return self.type == '*' or self.subtype == '*' - - @property - def specificity(self): - if self.params: - return self.SPECIFICITY_PARAMS - elif self.subtype != '*': - return self.SPECIFICITY_SUBTYPE - elif self.type != '*': - return self.SPECIFICITY_TYPE - else: - return self.SPECIFICITY_NONE +AcceptOffer = namedtuple('AcceptOffer', ['type', 'subtype', 'params']) class Accept(object): @@ -444,15 +423,10 @@ def parse_offer(cls, offer): | *params* is a list containing ``(*parameter name*, *value*)`` values. - | The result also supports ``is_range`` and ``specificity`` - properties. Specificity is a value from 1 to 4 where ``*/*`` - is 1, ``text/*`` is 2, ``text/html`` is 3 and - ``text/html;charset=utf8`` is 4. - :raises ValueError: If the offer does not match the required format. """ - match = cls.media_type_compiled_re.match(offer) + match = cls.media_type_compiled_re.match(offer.lower()) if not match: raise ValueError('Invalid value for an Accept offer.') @@ -461,21 +435,7 @@ def parse_offer(cls, offer): offer_params = cls._parse_media_type_params( media_type_params_segment=groups[1], ) - # offer_type, offer_subtype, offer_params, invalid, example - # == * == * true Y/N - # N N N N a/b - # N N Y N a/b;x=y - # N Y N N a/* - # N Y Y Y a/*;x=y - # Y N N Y */b - # Y N Y Y */b;x=y - # Y Y N N */* - # Y Y Y Y */*;x=y - # simplifies to (A and not B or B and C) - if ( - (offer_type == '*' and offer_subtype != '*') - or (offer_subtype == '*' and offer_params) - ): + if offer_type == '*' or offer_subtype == '*': raise ValueError('Invalid value for an Accept offer.') return AcceptOffer(offer_type, offer_subtype, offer_params) @@ -491,7 +451,7 @@ def _parse_and_normalize_offers(cls, offers): parsed_offers = [] for index, offer in enumerate(offers): try: - parsed_offer = cls.parse_offer(offer.lower()) + parsed_offer = cls.parse_offer(offer) except ValueError: continue parsed_offers.append([index, parsed_offer]) @@ -860,12 +820,8 @@ 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 do not match the media type grammar will be ignored. - - This function also supports media ranges (without media type - parameters) but without any specificity. An offered media range is - assigned the highest q-value of any media range from the header that - would match any media type that could be derived from the offer. + 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) @@ -894,39 +850,12 @@ def acceptable_offers(self, offers): acceptable_offers_n_quality_factors = {} for offer_index, parsed_offer in lowercased_offers_parsed: offer = offers[offer_index] - offer_is_range = parsed_offer.is_range offer_type, offer_subtype, offer_media_type_params = parsed_offer for ( range_type_subtype, range_qvalue, range_media_type_params, __, ) in lowercased_ranges: range_type, range_subtype = range_type_subtype.split('/', 1) - # if a media range is supplied as an offer then specificity is - # unimportant, we'll just compare for match and use the - # highest matching qvalue - if offer_is_range: - if ( - # Accept: anything, offer=*/* - (offer_type == '*' and offer_subtype == '*') - - # Accept: text/anything, offer=text/* - or (offer_type == range_type and offer_subtype == '*') - - # Accept: */*, offer=anything - or ( - range_type == '*' and range_subtype == '*' - and range_media_type_params == [] - ) - ): - prev_match = acceptable_offers_n_quality_factors.get(offer) - if not prev_match or prev_match[0] < range_qvalue: - acceptable_offers_n_quality_factors[offer] = ( - range_qvalue, # qvalue of matched range - offer_index, - 4, # unused for offers that are media ranges - ) - continue - # The specificity values below are based on the list in the # example in RFC 7231 section 5.3.2 explaining how "media # ranges can be overridden by more specific media ranges or @@ -934,7 +863,7 @@ def acceptable_offers(self, offers): # items in reverse order, so specificity 4, 3, 2, 1 correspond # to 1, 2, 3, 4 in the list, respectively (so that higher # specificity has higher precedence). - elif ( + if ( offer_type == range_type and offer_subtype == range_subtype ): @@ -1356,7 +1285,8 @@ def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. - Any offers that do not match the media type grammar will be ignored. + 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) diff --git a/tests/test_acceptparse.py b/tests/test_acceptparse.py index a1c5c06a..3b0975be 100644 --- a/tests/test_acceptparse.py +++ b/tests/test_acceptparse.py @@ -382,26 +382,20 @@ def test_parse__valid_header(self, value, expected_list): list_of_returned = list(returned) assert list_of_returned == expected_list - @pytest.mark.parametrize('offer, expected_return, is_range, specificity', [ - ['text/html', ('text', 'html', []), False, 3], + @pytest.mark.parametrize('offer, expected_return', [ + ['text/html', ('text', 'html', [])], [ 'text/html;charset=utf8', ('text', 'html', [('charset', 'utf8')]), - False, 4, ], [ 'text/html;charset=utf8;x-version=1', ('text', 'html', [('charset', 'utf8'), ('x-version', '1')]), - False, 4, ], - ['text/*', ('text', '*', []), True, 2], - ['*/*', ('*', '*', []), True, 1], ]) - def test_parse_offer__valid(self, offer, expected_return, is_range, specificity): + def test_parse_offer__valid(self, offer, expected_return): result = Accept.parse_offer(offer) assert result == expected_return - assert result.is_range == is_range - assert result.specificity == specificity @pytest.mark.parametrize('offer', [ '', @@ -412,6 +406,8 @@ def test_parse_offer__valid(self, offer, expected_return, is_range, specificity) '*/plain;charset=utf8;x-version=1', '*/*;charset=utf8', 'text/*;charset=utf8', + 'text/*', + '*/*', ]) def test_parse_offer__invalid(self, offer): with pytest.raises(ValueError): @@ -1082,25 +1078,20 @@ def test_acceptable_offers__invalid_offers( ('text/plain', 0.3), ], ), - ( - 'text/*;q=0.3, text/html;q=0.5, text/html;level=1;q=0.7', - ['*/*', 'text/*', 'text/html', 'image/*'], - [('*/*', 0.7), ('text/*', 0.7), ('text/html', 0.5)], - ), ( 'text/*;q=0.3, text/html;q=0.5, text/html;level=1;q=0.7', ['text/*', '*/*', 'text/html', 'image/*'], - [('text/*', 0.7), ('*/*', 0.7), ('text/html', 0.5)], + [('text/html', 0.5)], ), ( 'text/html;level=1;q=0.7', ['text/*', '*/*', 'text/html', 'text/html;level=1', 'image/*'], - [('text/*', 0.7), ('*/*', 0.7), ('text/html;level=1', 0.7)], + [('text/html;level=1', 0.7)], ), ( '*/*', ['text/*'], - [('text/*', 1.0)], + [], ), ( '',