Skip to content

Commit 7912924

Browse files
committed
feat(mediatypes): improve docstring, simplify behaviour
1 parent 9fdc9ce commit 7912924

File tree

2 files changed

+53
-6
lines changed

2 files changed

+53
-6
lines changed

falcon/util/mediatypes.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class _MediaRange:
140140

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

143-
_NOT_MATCHING = (-1, -1, -1, 0.0)
143+
_NOT_MATCHING = (-1, -1, -1, -1, 0.0)
144144

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

187187
return cls(main_type, subtype, quality, params)
188188

189-
def match_score(self, media_type: _MediaType) -> Tuple[int, int, int, float]:
189+
def match_score(self, media_type: _MediaType) -> Tuple[int, int, int, int, float]:
190190
if self.main_type == '*' or media_type.main_type == '*':
191191
main_matches = 0
192192
elif self.main_type != media_type.main_type:
@@ -203,14 +203,15 @@ def match_score(self, media_type: _MediaType) -> Tuple[int, int, int, float]:
203203

204204
mr_pnames = frozenset(self.params)
205205
mt_pnames = frozenset(media_type.params)
206-
param_score = -len(mr_pnames.symmetric_difference(mt_pnames))
206+
207+
exact_match = 0 if mr_pnames.symmetric_difference(mt_pnames) else 1
208+
207209
matching = mr_pnames & mt_pnames
208210
for pname in matching:
209211
if self.params[pname] != media_type.params[pname]:
210212
return self._NOT_MATCHING
211-
param_score += len(matching)
212213

213-
return (main_matches, sub_matches, param_score, self.quality)
214+
return (main_matches, sub_matches, exact_match, len(matching), self.quality)
214215

215216
def __repr__(self) -> str:
216217
q = self.quality
@@ -235,6 +236,42 @@ def quality(media_type: str, header: str) -> float:
235236
Media-ranges are parsed from the provided `header` value according to
236237
RFC 9110, Section 12.5.1 (the ``Accept`` header).
237238
239+
The provided `media_type` is matched against each of the parsed media
240+
ranges, and the fitness of each match is assessed as follows
241+
(in the decreasing priority list of criteria):
242+
243+
1. Do the main types (as in ``type/subtype``) match?
244+
245+
The types must either match exactly, or as wildcard (``*``).
246+
The matches involving a wildcard are prioritized lower.
247+
248+
2. Do the subtypes (as in ``type/subtype``) match?
249+
250+
The subtypes must either match exactly, or as wildcard (``*``).
251+
The matches involving a wildcard are prioritized lower.
252+
253+
3. Do the parameters match exactly?
254+
255+
If all the parameter names and values (if any) between the media range
256+
and media type match exactly, such match is prioritized higher than
257+
matches involving extraneous parameters on either side.
258+
259+
Note that if parameter names match, the corresponding values must also
260+
be equal, or the provided media type is considered not to match the
261+
media range in question at all.
262+
263+
4. The number of matching parameters.
264+
265+
5. Finally, if two or more best media range matches are equally fit
266+
according to all of the above criteria (1) through (4), the highest
267+
quality (i.e., the value of the ``q`` parameter) of these is returned.
268+
269+
Note:
270+
With the exception of evaluating the exact parameter match (3), the
271+
number of extraneous parameters (i.e. where the names are only present
272+
in the media type, or only in the media range) currently does not
273+
influence the described specificity sort order.
274+
238275
Args:
239276
media_type: The Internet media type to match against the provided
240277
HTTP ``Accept`` header value.
@@ -254,7 +291,8 @@ def quality(media_type: str, header: str) -> float:
254291

255292

256293
def best_match(media_types: Iterable[str], header: str) -> Optional[str]:
257-
"""Choose media type with the highest quality from a list of candidates.
294+
"""Choose media type with the highest :func:`quality` from a list of
295+
candidates.
258296
259297
Args:
260298
media_types: An iterable over one or more Internet media types

tests/test_mediatypes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ def test_quality_rfc_examples(accept, media_type, quality_value):
159159
'text/plain; format=flowed',
160160
0.33,
161161
),
162+
(
163+
# NOTE(vytas): Same as one of the RFC 7231 examples, just with some
164+
# media ranges reordered. python-mimeparse fails to yield the
165+
# correct result in this specific case.
166+
'text/*;q=0.3, text/html;level=1, text/html;q=0.7, '
167+
'text/html;level=2;q=0.4, */*;q=0.5',
168+
'text/html; level=3',
169+
0.7,
170+
),
162171
],
163172
)
164173
def test_quality(accept, media_type, quality_value):

0 commit comments

Comments
 (0)