--- Begin Message ---
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 <[email protected]> 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 <[email protected]>
+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
++
++
[email protected](
++ '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
--- End Message ---