Skip to content

Commit

Permalink
Merge pull request #379 from mmerickel/accept-offer-object
Browse files Browse the repository at this point in the history
support passing around AcceptOffer objects
  • Loading branch information
digitalresistor authored Oct 10, 2018
2 parents 3fc7590 + 06eda14 commit 3f6f47a
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 15 deletions.
6 changes: 4 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ Feature
- 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
``acceptparse.AcceptInvalidHeader.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

Compatibility
~~~~~~~~~~~~~
Expand Down
3 changes: 3 additions & 0 deletions docs/api/webob.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ methods:
.. autoclass:: Accept
:members: parse

.. autoclass:: AcceptOffer
:members: __str__

.. autoclass:: AcceptValidHeader
:members: parse, header_value, parsed, __init__, __add__, __bool__,
__contains__, __iter__, __nonzero__, __radd__, __repr__, __str__,
Expand Down
48 changes: 40 additions & 8 deletions src/webob/acceptparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,25 @@ def _list_1_or_more__compiled_re(element_re):
)


AcceptOffer = namedtuple('AcceptOffer', ['type', 'subtype', 'params'])
class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])):
"""
A pre-parsed offer tuple represeting a value in the format
``type/subtype;param0=value0;param1=value1``.
:ivar type: The media type's root category.
:ivar subtype: The media type's subtype.
:ivar params: A tuple of 2-tuples containing parameter names and values.
"""
__slots__ = ()

def __str__(self):
"""
Return the properly quoted media type string.
"""
value = self.type + '/' + self.subtype
return Accept._form_media_range(value, self.params)


class Accept(object):
Expand Down Expand Up @@ -426,7 +444,9 @@ def parse_offer(cls, offer):
:raises ValueError: If the offer does not match the required format.
"""
match = cls.media_type_compiled_re.match(offer.lower())
if isinstance(offer, AcceptOffer):
return offer
match = cls.media_type_compiled_re.match(offer)
if not match:
raise ValueError('Invalid value for an Accept offer.')

Expand All @@ -437,7 +457,11 @@ def parse_offer(cls, offer):
)
if offer_type == '*' or offer_subtype == '*':
raise ValueError('Invalid value for an Accept offer.')
return AcceptOffer(offer_type, offer_subtype, offer_params)
return AcceptOffer(
offer_type.lower(),
offer_subtype.lower(),
tuple((name.lower(), value) for name, value in offer_params),
)

@classmethod
def _parse_and_normalize_offers(cls, offers):
Expand Down Expand Up @@ -824,7 +848,8 @@ def acceptable_offers(self, offers):
:meth:`.Accept.parse_offer` will be ignored.
:param offers: ``iterable`` of ``str`` media types (media types can
include media type parameters)
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
Expand All @@ -838,9 +863,16 @@ def acceptable_offers(self, offers):
# the semantics of the parameter name."
lowercased_ranges = [
(
media_range.partition(';')[0].lower(), qvalue,
[(name.lower(), value) for name, value in media_type_params],
[(name.lower(), value) for name, value in extension_params],
media_range.partition(';')[0].lower(),
qvalue,
tuple(
(name.lower(), value)
for name, value in media_type_params
),
tuple(
(name.lower(), value)
for name, value in extension_params
),
)
for media_range, qvalue, media_type_params, extension_params in
parsed
Expand All @@ -867,7 +899,7 @@ def acceptable_offers(self, offers):
offer_type == range_type
and offer_subtype == range_subtype
):
if range_media_type_params == []:
if range_media_type_params == ():
# If offer_media_type_params == [], the offer and the
# range match exactly, with neither having media type
# parameters.
Expand Down
26 changes: 21 additions & 5 deletions tests/test_acceptparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,20 +382,29 @@ 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', [])],
@pytest.mark.parametrize('offer, expected_return, expected_str', [
['text/html', ('text', 'html', ()), 'text/html'],
[
'text/html;charset=utf8',
('text', 'html', [('charset', 'utf8')]),
('text', 'html', (('charset', 'utf8'),)),
'text/html;charset=utf8',
],
[
'text/html;charset=utf8;x-version=1',
('text', 'html', [('charset', 'utf8'), ('x-version', '1')]),
('text', 'html', (('charset', 'utf8'), ('x-version', '1'))),
'text/html;charset=utf8;x-version=1',
],
[
'text/HtMl;cHaRseT=UtF-8;X-Version=1',
('text', 'html', (('charset', 'UtF-8'), ('x-version', '1'))),
'text/html;charset=UtF-8;x-version=1',
],
])
def test_parse_offer__valid(self, offer, expected_return):
def test_parse_offer__valid(self, offer, expected_return, expected_str):
result = Accept.parse_offer(offer)
assert result == expected_return
assert str(result) == expected_str
assert result is Accept.parse_offer(result)

@pytest.mark.parametrize('offer', [
'',
Expand Down Expand Up @@ -1106,6 +1115,13 @@ def test_acceptable_offers__valid_offers(
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')
Expand Down

0 comments on commit 3f6f47a

Please sign in to comment.