Skip to content

Commit

Permalink
feat(mediatypes): improve docstring, simplify behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
vytas7 committed Oct 1, 2024
1 parent 9fdc9ce commit 7912924
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 6 deletions.
50 changes: 44 additions & 6 deletions falcon/util/mediatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class _MediaRange:

__slots__ = ('main_type', 'subtype', 'quality', 'params')

_NOT_MATCHING = (-1, -1, -1, 0.0)
_NOT_MATCHING = (-1, -1, -1, -1, 0.0)

_Q_VALUE_ERROR_MESSAGE = (
'If provided, the q parameter must be a real number '
Expand Down Expand Up @@ -186,7 +186,7 @@ def parse(cls, media_range: str) -> _MediaRange:

return cls(main_type, subtype, quality, params)

Check warning on line 187 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L187

Added line #L187 was not covered by tests

def match_score(self, media_type: _MediaType) -> Tuple[int, int, int, float]:
def match_score(self, media_type: _MediaType) -> Tuple[int, int, int, int, float]:
if self.main_type == '*' or media_type.main_type == '*':
main_matches = 0

Check warning on line 191 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L191

Added line #L191 was not covered by tests
elif self.main_type != media_type.main_type:
Expand All @@ -203,14 +203,15 @@ def match_score(self, media_type: _MediaType) -> Tuple[int, int, int, float]:

mr_pnames = frozenset(self.params)
mt_pnames = frozenset(media_type.params)

Check warning on line 205 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L204-L205

Added lines #L204 - L205 were not covered by tests
param_score = -len(mr_pnames.symmetric_difference(mt_pnames))

exact_match = 0 if mr_pnames.symmetric_difference(mt_pnames) else 1

Check warning on line 207 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L207

Added line #L207 was not covered by tests

matching = mr_pnames & mt_pnames

Check warning on line 209 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L209

Added line #L209 was not covered by tests
for pname in matching:
if self.params[pname] != media_type.params[pname]:
return self._NOT_MATCHING

Check warning on line 212 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L212

Added line #L212 was not covered by tests
param_score += len(matching)

return (main_matches, sub_matches, param_score, self.quality)
return (main_matches, sub_matches, exact_match, len(matching), self.quality)

Check warning on line 214 in falcon/util/mediatypes.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/mediatypes.py#L214

Added line #L214 was not covered by tests

def __repr__(self) -> str:
q = self.quality
Expand All @@ -235,6 +236,42 @@ def quality(media_type: str, header: str) -> float:
Media-ranges are parsed from the provided `header` value according to
RFC 9110, Section 12.5.1 (the ``Accept`` header).
The provided `media_type` is matched against each of the parsed media
ranges, and the fitness of each match is assessed as follows
(in the decreasing priority list of criteria):
1. Do the main types (as in ``type/subtype``) match?
The types must either match exactly, or as wildcard (``*``).
The matches involving a wildcard are prioritized lower.
2. Do the subtypes (as in ``type/subtype``) match?
The subtypes must either match exactly, or as wildcard (``*``).
The matches involving a wildcard are prioritized lower.
3. Do the parameters match exactly?
If all the parameter names and values (if any) between the media range
and media type match exactly, such match is prioritized higher than
matches involving extraneous parameters on either side.
Note that if parameter names match, the corresponding values must also
be equal, or the provided media type is considered not to match the
media range in question at all.
4. The number of matching parameters.
5. Finally, if two or more best media range matches are equally fit
according to all of the above criteria (1) through (4), the highest
quality (i.e., the value of the ``q`` parameter) of these is returned.
Note:
With the exception of evaluating the exact parameter match (3), the
number of extraneous parameters (i.e. where the names are only present
in the media type, or only in the media range) currently does not
influence the described specificity sort order.
Args:
media_type: The Internet media type to match against the provided
HTTP ``Accept`` header value.
Expand All @@ -254,7 +291,8 @@ def quality(media_type: str, header: str) -> float:


def best_match(media_types: Iterable[str], header: str) -> Optional[str]:
"""Choose media type with the highest quality from a list of candidates.
"""Choose media type with the highest :func:`quality` from a list of
candidates.
Args:
media_types: An iterable over one or more Internet media types
Expand Down
9 changes: 9 additions & 0 deletions tests/test_mediatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ def test_quality_rfc_examples(accept, media_type, quality_value):
'text/plain; format=flowed',
0.33,
),
(
# NOTE(vytas): Same as one of the RFC 7231 examples, just with some
# media ranges reordered. python-mimeparse fails to yield the
# correct result in this specific case.
'text/*;q=0.3, text/html;level=1, text/html;q=0.7, '
'text/html;level=2;q=0.4, */*;q=0.5',
'text/html; level=3',
0.7,
),
],
)
def test_quality(accept, media_type, quality_value):
Expand Down

0 comments on commit 7912924

Please sign in to comment.