Package: python-falcon Version: 3.1.1-4 Severity: normal Tags: patch pending
Dear maintainer, I've prepared an NMU for python-falcon (versioned as 3.1.1-4.1) and uploaded it to DELAYED/10. Please feel free to tell me if I should delay it longer. I expect the Python 3.13 transition to kick off very soon, raising this bug to RC, and I plan to reschedule this to 0-day, if that happens. Regards Stefano
diff -Nru python-falcon-3.1.1/debian/changelog python-falcon-3.1.1/debian/changelog --- python-falcon-3.1.1/debian/changelog 2024-05-01 23:37:02.000000000 -0700 +++ python-falcon-3.1.1/debian/changelog 2024-11-09 11:10:04.000000000 -0800 @@ -1,3 +1,10 @@ +python-falcon (3.1.1-4.1) unstable; urgency=medium + + * Non-maintainer upload. + * Patch: Python 3.13 support. (Closes: #1081629, #1084623). + + -- Stefano Rivera <stefa...@debian.org> Sat, 09 Nov 2024 11:10:04 -0800 + python-falcon (3.1.1-4) unstable; urgency=medium * Drop extraneous python3-six dependency (Closes: #1070180). diff -Nru python-falcon-3.1.1/debian/patches/python3.13.patch python-falcon-3.1.1/debian/patches/python3.13.patch --- python-falcon-3.1.1/debian/patches/python3.13.patch 1969-12-31 16:00:00.000000000 -0800 +++ python-falcon-3.1.1/debian/patches/python3.13.patch 2024-11-09 11:10:04.000000000 -0800 @@ -0,0 +1,363 @@ +From: Vytautas Liuolia <vytautas.liuo...@gmail.com> +Date: Wed, 3 Apr 2024 22:27:30 +0200 +Subject: feat(parse_header): provide our own implementation of + `parse_header()` (#2217) + +* feat(parse_header): provide our own implementation of `parse_header()` + +* docs(newsfragments): add a newsfragment ++ address 1 review comment + +* test(test_mediatypes.py): add tests for multiple parameters + +Bug-Debian: https://bugs.debian.org/1081629 +Bug-Upstream: https://github.com/falconry/falcon/issues/2066 +Origin: upstream, https://github.com/falconry/falcon/pull/2217 +--- + docs/api/util.rst | 5 ++ + docs/user/recipes/pretty-json.rst | 3 +- + falcon/__init__.py | 1 + + falcon/asgi/multipart.py | 5 +- + falcon/media/multipart.py | 10 ++-- + falcon/testing/helpers.py | 4 +- + falcon/util/__init__.py | 1 + + falcon/util/mediatypes.py | 89 ++++++++++++++++++++++++++++++++++++ + falcon/vendor/mimeparse/mimeparse.py | 4 +- + tests/test_mediatypes.py | 41 +++++++++++++++++ + 10 files changed, 149 insertions(+), 14 deletions(-) + create mode 100644 falcon/util/mediatypes.py + create mode 100644 tests/test_mediatypes.py + +diff --git a/docs/api/util.rst b/docs/api/util.rst +index 53216ae..97759dd 100644 +--- a/docs/api/util.rst ++++ b/docs/api/util.rst +@@ -34,6 +34,11 @@ HTTP Status + .. autofunction:: falcon.code_to_http_status + .. autofunction:: falcon.get_http_status + ++Media types ++----------- ++ ++.. autofunction:: falcon.parse_header ++ + Async + ----- + +diff --git a/docs/user/recipes/pretty-json.rst b/docs/user/recipes/pretty-json.rst +index b6e5e4d..5faf59e 100644 +--- a/docs/user/recipes/pretty-json.rst ++++ b/docs/user/recipes/pretty-json.rst +@@ -52,7 +52,6 @@ implemented with a :ref:`custom media handler <custom-media-handler-type>`: + + .. code:: python + +- import cgi + import json + + import falcon +@@ -66,7 +65,7 @@ implemented with a :ref:`custom media handler <custom-media-handler-type>`: + return json.loads(data.decode()) + + def serialize(self, media, content_type): +- _, params = cgi.parse_header(content_type) ++ _, params = falcon.parse_header(content_type) + indent = params.get('indent') + if indent is not None: + try: +diff --git a/falcon/__init__.py b/falcon/__init__.py +index ff30097..a2a557e 100644 +--- a/falcon/__init__.py ++++ b/falcon/__init__.py +@@ -75,6 +75,7 @@ from falcon.util import http_status_to_code + from falcon.util import IS_64_BITS + from falcon.util import is_python_func + from falcon.util import misc ++from falcon.util import parse_header + from falcon.util import reader + from falcon.util import runs_sync + from falcon.util import secure_filename +diff --git a/falcon/asgi/multipart.py b/falcon/asgi/multipart.py +index d58069f..57561f4 100644 +--- a/falcon/asgi/multipart.py ++++ b/falcon/asgi/multipart.py +@@ -14,11 +14,10 @@ + + """ASGI multipart form media handler components.""" + +-import cgi +- + from falcon.asgi.reader import BufferedReader + from falcon.errors import DelimiterError + from falcon.media import multipart ++from falcon.util.mediatypes import parse_header + + _ALLOWED_CONTENT_HEADERS = multipart._ALLOWED_CONTENT_HEADERS + _CRLF = multipart._CRLF +@@ -54,7 +53,7 @@ class BodyPart(multipart.BodyPart): + return self._media + + async def get_text(self): +- content_type, options = cgi.parse_header(self.content_type) ++ content_type, options = parse_header(self.content_type) + if content_type != 'text/plain': + return None + +diff --git a/falcon/media/multipart.py b/falcon/media/multipart.py +index a194f1d..778cd66 100644 +--- a/falcon/media/multipart.py ++++ b/falcon/media/multipart.py +@@ -14,7 +14,6 @@ + + """Multipart form media handler.""" + +-import cgi + import re + from urllib.parse import unquote_to_bytes + +@@ -24,6 +23,7 @@ from falcon.stream import BoundedStream + from falcon.util import BufferedReader + from falcon.util import misc + from falcon.util.deprecation import deprecated_args ++from falcon.util.mediatypes import parse_header + + + # TODO(vytas): +@@ -279,7 +279,7 @@ class BodyPart: + str: The part decoded as a text string provided the part is + encoded as ``text/plain``, ``None`` otherwise. + """ +- content_type, options = cgi.parse_header(self.content_type) ++ content_type, options = parse_header(self.content_type) + if content_type != 'text/plain': + return None + +@@ -305,7 +305,7 @@ class BodyPart: + + if self._content_disposition is None: + value = self._headers.get(b'content-disposition', b'') +- self._content_disposition = cgi.parse_header(value.decode()) ++ self._content_disposition = parse_header(value.decode()) + + _, params = self._content_disposition + +@@ -341,7 +341,7 @@ class BodyPart: + + if self._content_disposition is None: + value = self._headers.get(b'content-disposition', b'') +- self._content_disposition = cgi.parse_header(value.decode()) ++ self._content_disposition = parse_header(value.decode()) + + _, params = self._content_disposition + self._name = params.get('name') +@@ -526,7 +526,7 @@ class MultipartFormHandler(BaseHandler): + if not form_cls: + raise NotImplementedError + +- _, options = cgi.parse_header(content_type) ++ _, options = parse_header(content_type) + try: + boundary = options['boundary'] + except KeyError: +diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py +index b11f7f6..c3c4b13 100644 +--- a/falcon/testing/helpers.py ++++ b/falcon/testing/helpers.py +@@ -23,7 +23,6 @@ directly from the `testing` package:: + """ + + import asyncio +-import cgi + from collections import defaultdict + from collections import deque + import contextlib +@@ -50,6 +49,7 @@ from falcon.asgi_spec import WSCloseCode + from falcon.constants import SINGLETON_HEADERS + import falcon.request + from falcon.util import uri ++from falcon.util.mediatypes import parse_header + + # NOTE(kgriffs): Changed in 3.0 from 'curl/7.24.0 (x86_64-apple-darwin12.0)' + DEFAULT_UA = 'falcon-client/' + falcon.__version__ +@@ -798,7 +798,7 @@ def get_encoding_from_headers(headers): + if not content_type: + return None + +- content_type, params = cgi.parse_header(content_type) ++ content_type, params = parse_header(content_type) + + if 'charset' in params: + return params['charset'].strip('\'"') +diff --git a/falcon/util/__init__.py b/falcon/util/__init__.py +index cd3eca3..4232149 100644 +--- a/falcon/util/__init__.py ++++ b/falcon/util/__init__.py +@@ -25,6 +25,7 @@ import sys + # Hoist misc. utils + from falcon.constants import PYTHON_VERSION + from falcon.util.deprecation import deprecated ++from falcon.util.mediatypes import parse_header + from falcon.util.misc import code_to_http_status + from falcon.util.misc import dt_to_http + from falcon.util.misc import get_argnames +diff --git a/falcon/util/mediatypes.py b/falcon/util/mediatypes.py +new file mode 100644 +index 0000000..c0dca51 +--- /dev/null ++++ b/falcon/util/mediatypes.py +@@ -0,0 +1,89 @@ ++# Copyright 2023-2024 by Vytautas Liuolia. ++# ++# Licensed under the Apache License, Version 2.0 (the "License"); ++# you may not use this file except in compliance with the License. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++ ++"""Media (aka MIME) type parsing and matching utilities.""" ++ ++import typing ++ ++ ++def _parse_param_old_stdlib(s): # type: ignore ++ while s[:1] == ';': ++ s = s[1:] ++ end = s.find(';') ++ while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: ++ end = s.find(';', end + 1) ++ if end < 0: ++ end = len(s) ++ f = s[:end] ++ yield f.strip() ++ s = s[end:] ++ ++ ++def _parse_header_old_stdlib(line): # type: ignore ++ """Parse a Content-type like header. ++ ++ Return the main content-type and a dictionary of options. ++ ++ Note: ++ This method has been copied (almost) verbatim from CPython 3.8 stdlib. ++ It is slated for removal from the stdlib in 3.13. ++ """ ++ parts = _parse_param_old_stdlib(';' + line) ++ key = parts.__next__() ++ pdict = {} ++ for p in parts: ++ i = p.find('=') ++ if i >= 0: ++ name = p[:i].strip().lower() ++ value = p[i + 1 :].strip() ++ if len(value) >= 2 and value[0] == value[-1] == '"': ++ value = value[1:-1] ++ value = value.replace('\\\\', '\\').replace('\\"', '"') ++ pdict[name] = value ++ return key, pdict ++ ++ ++def parse_header(line: str) -> typing.Tuple[str, dict]: ++ """Parse a Content-type like header. ++ ++ Return the main content-type and a dictionary of options. ++ ++ Args: ++ line: A header value to parse. ++ ++ Returns: ++ tuple: (the main content-type, dictionary of options). ++ ++ Note: ++ This function replaces an equivalent method previously available in the ++ stdlib as ``cgi.parse_header()``. ++ It was removed from the stdlib in Python 3.13. ++ """ ++ if '"' not in line and '\\' not in line: ++ key, semicolon, parts = line.partition(';') ++ if not semicolon: ++ return (key.strip(), {}) ++ ++ pdict = {} ++ for part in parts.split(';'): ++ name, equals, value = part.partition('=') ++ if equals: ++ pdict[name.strip().lower()] = value.strip() ++ ++ return (key.strip(), pdict) ++ ++ return _parse_header_old_stdlib(line) ++ ++ ++__all__ = ['parse_header'] +diff --git a/falcon/vendor/mimeparse/mimeparse.py b/falcon/vendor/mimeparse/mimeparse.py +index 0218553..f96e633 100755 +--- a/falcon/vendor/mimeparse/mimeparse.py ++++ b/falcon/vendor/mimeparse/mimeparse.py +@@ -1,4 +1,4 @@ +-import cgi ++from falcon.util.mediatypes import parse_header + + __version__ = '1.6.0' + __author__ = 'Joe Gregorio' +@@ -23,7 +23,7 @@ def parse_mime_type(mime_type): + + :rtype: (str,str,dict) + """ +- full_type, params = cgi.parse_header(mime_type) ++ full_type, params = parse_header(mime_type) + # Java URLConnection class sends an Accept header that includes a + # single '*'. Turn it into a legal wildcard. + if full_type == '*': +diff --git a/tests/test_mediatypes.py b/tests/test_mediatypes.py +new file mode 100644 +index 0000000..0fae79b +--- /dev/null ++++ b/tests/test_mediatypes.py +@@ -0,0 +1,41 @@ ++import pytest ++ ++from falcon.util import mediatypes ++ ++ ++@pytest.mark.parametrize( ++ 'value,expected', ++ [ ++ ('', ('', {})), ++ ('strange', ('strange', {})), ++ ('text/plain', ('text/plain', {})), ++ ('text/plain ', ('text/plain', {})), ++ (' text/plain', ('text/plain', {})), ++ (' text/plain ', ('text/plain', {})), ++ (' text/plain ', ('text/plain', {})), ++ ( ++ 'falcon/peregrine; key1; key2=value; key3', ++ ('falcon/peregrine', {'key2': 'value'}), ++ ), ++ ( ++ 'audio/pcm;rate=48000;encoding=float;bits=32', ++ ('audio/pcm', {'bits': '32', 'encoding': 'float', 'rate': '48000'}), ++ ), ++ ( ++ 'falcon/*; genus=falco; family=falconidae; class=aves; ', ++ ('falcon/*', {'class': 'aves', 'family': 'falconidae', 'genus': 'falco'}), ++ ), ++ ('"falcon/peregrine" ; key="value"', ('"falcon/peregrine"', {'key': 'value'})), ++ ('falcon/peregrine; empty=""', ('falcon/peregrine', {'empty': ''})), ++ ('falcon/peregrine; quote="', ('falcon/peregrine', {'quote': '"'})), ++ ('text/plain; charset=utf-8', ('text/plain', {'charset': 'utf-8'})), ++ ('stuff/strange; missing-value; missing-another', ('stuff/strange', {})), ++ ('stuff/strange; missing-value\\missing-another', ('stuff/strange', {})), ++ ( ++ 'application/falcon; P1 = "key; value"; P2="\\""', ++ ('application/falcon', {'p1': 'key; value', 'p2': '"'}), ++ ), ++ ], ++) ++def test_parse_header(value, expected): ++ assert mediatypes.parse_header(value) == expected diff -Nru python-falcon-3.1.1/debian/patches/series python-falcon-3.1.1/debian/patches/series --- python-falcon-3.1.1/debian/patches/series 2024-05-01 23:37:02.000000000 -0700 +++ python-falcon-3.1.1/debian/patches/series 2024-11-09 11:10:04.000000000 -0800 @@ -1,2 +1,3 @@ fix-non-ascii-in-doc.patch remove-test_cythonized_asgi.py.patch +python3.13.patch