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

Reply via email to