Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support passing around AcceptOffer objects #379

Merged
merged 2 commits into from
Oct 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments needs updating.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meh, whatever.

# 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