From cc5079730a50e77590d465563807871b8f230ff5 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:59:11 -0300 Subject: [PATCH] [5.2.x] Fixed #36446 -- Restored "q" in internal MediaType.params property. The "q" key was removed while addressing ticket #36411. Despite `MediaType.params` is undocumented and considered internal, it was used in third-party projects (Zulip reported breakage), so this work restored the `q` key in `params`. Thanks Anders Kaseorg for the report. Regression in c075508b4de8edf9db553b409f8a8ed2f26ecead. Backport of cf5f36bf903a2854f5e395149cee707115b83744 from main. --- django/http/request.py | 19 ++++---- docs/releases/5.2.3.txt | 3 ++ tests/requests_tests/test_accept_header.py | 57 ++++++++++++++++++++++ 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/django/http/request.py b/django/http/request.py index 6438f26268..c26a4954d1 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -694,13 +694,13 @@ class QueryDict(MultiValueDict): class MediaType: def __init__(self, media_type_raw_line): - full_type, self._params = parse_header_parameters( + full_type, self.params = parse_header_parameters( media_type_raw_line if media_type_raw_line else "" ) self.main_type, _, self.sub_type = full_type.partition("/") def __str__(self): - params_str = "".join("; %s=%s" % (k, v) for k, v in self._params.items()) + params_str = "".join("; %s=%s" % (k, v) for k, v in self.params.items()) return "%s%s%s" % ( self.main_type, ("/%s" % self.sub_type) if self.sub_type else "", @@ -711,8 +711,8 @@ class MediaType: return "<%s: %s>" % (self.__class__.__qualname__, self) @cached_property - def params(self): - params = self._params.copy() + def range_params(self): + params = self.params.copy() params.pop("q", None) return params @@ -735,20 +735,19 @@ class MediaType: if this_type != other_type and this_type != "*" and other_type != "*": return False - if bool(self.params) == bool(other.params): + if bool(self.range_params) == bool(other.range_params): # If both have params or neither have params, they must be identical. - result = self.params == other.params + result = self.range_params == other.range_params else: # If self has params and other does not, it's a match. # If other has params and self does not, don't match. - result = bool(self.params or not other.params) - + result = bool(self.range_params or not other.range_params) return result @cached_property def quality(self): try: - quality = float(self._params.get("q", 1)) + quality = float(self.params.get("q", 1)) except ValueError: # Discard invalid values. return 1 @@ -768,7 +767,7 @@ class MediaType: return 0 elif self.sub_type == "*": return 1 - elif not self.params: + elif not self.range_params: return 2 return 3 diff --git a/docs/releases/5.2.3.txt b/docs/releases/5.2.3.txt index 927ec16d34..70aa5aedb0 100644 --- a/docs/releases/5.2.3.txt +++ b/docs/releases/5.2.3.txt @@ -17,3 +17,6 @@ Bugfixes * Fixed a regression in Django 5.2 that caused :meth:`.QuerySet.bulk_update` to incorrectly convert ``None`` to JSON ``null`` instead of SQL ``NULL`` for ``JSONField`` (:ticket:`36419`). + +* Fixed a regression in Django 5.2.2 where the ``q`` parameter was removed from + the internal ``django.http.MediaType.params`` property (:ticket:`36446`). diff --git a/tests/requests_tests/test_accept_header.py b/tests/requests_tests/test_accept_header.py index 37826278e2..608af58b62 100644 --- a/tests/requests_tests/test_accept_header.py +++ b/tests/requests_tests/test_accept_header.py @@ -15,6 +15,10 @@ class MediaTypeTests(TestCase): def test_str(self): self.assertEqual(str(MediaType("*/*; q=0.8")), "*/*; q=0.8") self.assertEqual(str(MediaType("application/xml")), "application/xml") + self.assertEqual( + str(MediaType("application/xml;type=madeup;q=42")), + "application/xml; type=madeup; q=42", + ) def test_repr(self): self.assertEqual(repr(MediaType("*/*; q=0.8")), "") @@ -34,6 +38,7 @@ class MediaTypeTests(TestCase): (" application/xml ", "application/xml"), ("application/xml", " application/xml "), ("text/vcard; version=4.0", "text/vcard; version=4.0"), + ("text/vcard; version=4.0; q=0.7", "text/vcard; version=4.0"), ("text/vcard; version=4.0", "text/vcard"), ] for accepted_type, mime_type in tests: @@ -58,12 +63,64 @@ class MediaTypeTests(TestCase): # All main and sub types are defined, but there is no match. ("application/xml", "application/html"), ("text/vcard; version=4.0", "text/vcard; version=3.0"), + ("text/vcard; q=0.7", "text/vcard; version=3.0"), ("text/vcard", "text/vcard; version=3.0"), ] for accepted_type, mime_type in tests: with self.subTest(accepted_type, mime_type=mime_type): self.assertIs(MediaType(accepted_type).match(mime_type), False) + def test_params(self): + tests = [ + ("text/plain", {}, {}), + ("text/plain;q=0.7", {"q": "0.7"}, {}), + ("text/plain;q=1.5", {"q": "1.5"}, {}), + ("text/plain;q=xyz", {"q": "xyz"}, {}), + ("text/plain;q=0.1234", {"q": "0.1234"}, {}), + ("text/plain;version=2", {"version": "2"}, {"version": "2"}), + ( + "text/plain;version=2;q=0.8", + {"version": "2", "q": "0.8"}, + {"version": "2"}, + ), + ( + "text/plain;q=0.8;version=2", + {"q": "0.8", "version": "2"}, + {"version": "2"}, + ), + ( + "text/plain; charset=UTF-8; q=0.3", + {"charset": "UTF-8", "q": "0.3"}, + {"charset": "UTF-8"}, + ), + ( + "text/plain ; q = 0.5 ; version = 3.0", + {"q": "0.5", "version": "3.0"}, + {"version": "3.0"}, + ), + ("text/plain; format=flowed", {"format": "flowed"}, {"format": "flowed"}), + ( + "text/plain; format=flowed; q=0.4", + {"format": "flowed", "q": "0.4"}, + {"format": "flowed"}, + ), + ("text/*;q=0.2", {"q": "0.2"}, {}), + ("*/json;q=0.9", {"q": "0.9"}, {}), + ("application/json;q=0.9999", {"q": "0.9999"}, {}), + ("text/html;q=0.0001", {"q": "0.0001"}, {}), + ("text/html;q=0", {"q": "0"}, {}), + ("text/html;q=0.", {"q": "0."}, {}), + ("text/html;q=.8", {"q": ".8"}, {}), + ("text/html;q= 0.9", {"q": "0.9"}, {}), + ('text/html ; q = "0.6"', {"q": "0.6"}, {}), + ] + for accepted_type, params, range_params in tests: + media_type = MediaType(accepted_type) + with self.subTest(accepted_type, attr="params"): + self.assertEqual(media_type.params, params) + with self.subTest(accepted_type, attr="range_params"): + self.assertEqual(media_type.range_params, range_params) + def test_quality(self): tests = [ ("*/*; q=0.8", 0.8),