python-multipart 0.0.20__tar.gz → 0.0.21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {python_multipart-0.0.20 → python_multipart-0.0.21}/CHANGELOG.md +4 -0
  2. {python_multipart-0.0.20 → python_multipart-0.0.21}/PKG-INFO +4 -4
  3. {python_multipart-0.0.20 → python_multipart-0.0.21}/pyproject.toml +6 -6
  4. {python_multipart-0.0.20 → python_multipart-0.0.21}/python_multipart/__init__.py +1 -1
  5. {python_multipart-0.0.20 → python_multipart-0.0.21}/python_multipart/decoders.py +1 -1
  6. {python_multipart-0.0.20 → python_multipart-0.0.21}/python_multipart/multipart.py +8 -9
  7. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/compat.py +2 -1
  8. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_multipart.py +5 -2
  9. {python_multipart-0.0.20 → python_multipart-0.0.21}/.gitignore +0 -0
  10. {python_multipart-0.0.20 → python_multipart-0.0.21}/LICENSE.txt +0 -0
  11. {python_multipart-0.0.20 → python_multipart-0.0.21}/README.md +0 -0
  12. {python_multipart-0.0.20 → python_multipart-0.0.21}/multipart/__init__.py +0 -0
  13. {python_multipart-0.0.20 → python_multipart-0.0.21}/multipart/decoders.py +0 -0
  14. {python_multipart-0.0.20 → python_multipart-0.0.21}/multipart/exceptions.py +0 -0
  15. {python_multipart-0.0.20 → python_multipart-0.0.21}/multipart/multipart.py +0 -0
  16. {python_multipart-0.0.20 → python_multipart-0.0.21}/python_multipart/exceptions.py +0 -0
  17. {python_multipart-0.0.20 → python_multipart-0.0.21}/python_multipart/py.typed +0 -0
  18. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/__init__.py +0 -0
  19. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/CRLF_in_header.http +0 -0
  20. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/CRLF_in_header.yaml +0 -0
  21. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/CR_in_header.http +0 -0
  22. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/CR_in_header.yaml +0 -0
  23. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/CR_in_header_value.http +0 -0
  24. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  25. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary.http +0 -0
  26. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  27. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  28. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  29. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  30. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  31. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  32. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  33. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/bad_end_of_headers.http +0 -0
  34. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  35. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/bad_header_char.http +0 -0
  36. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/bad_header_char.yaml +0 -0
  37. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/bad_initial_boundary.http +0 -0
  38. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  39. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/base64_encoding.http +0 -0
  40. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/base64_encoding.yaml +0 -0
  41. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/empty_header.http +0 -0
  42. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/empty_header.yaml +0 -0
  43. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/empty_message.http +0 -0
  44. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/empty_message.yaml +0 -0
  45. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/empty_message_with_bad_end.http +0 -0
  46. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/empty_message_with_bad_end.yaml +0 -0
  47. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/header_with_number.http +0 -0
  48. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/header_with_number.yaml +0 -0
  49. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
  50. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
  51. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/multiple_fields.http +0 -0
  52. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/multiple_fields.yaml +0 -0
  53. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/multiple_files.http +0 -0
  54. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/multiple_files.yaml +0 -0
  55. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  56. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  57. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field.http +0 -0
  58. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field.yaml +0 -0
  59. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_blocks.http +0 -0
  60. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_blocks.yaml +0 -0
  61. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_longer.http +0 -0
  62. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_longer.yaml +0 -0
  63. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_single_file.http +0 -0
  64. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_single_file.yaml +0 -0
  65. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  66. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  67. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_file.http +0 -0
  68. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/single_file.yaml +0 -0
  69. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/utf8_filename.http +0 -0
  70. {python_multipart-0.0.20 → python_multipart-0.0.21}/tests/test_data/http/utf8_filename.yaml +0 -0
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.21 (2025-12-17)
4
+
5
+ * Add support for Python 3.14 and drop EOL 3.8 and 3.9 [#216](https://github.com/Kludex/python-multipart/pull/216).
6
+
3
7
  ## 0.0.20 (2024-12-16)
4
8
 
5
9
  * Handle messages containing only end boundary [#142](https://github.com/Kludex/python-multipart/pull/142).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-multipart
3
- Version: 0.0.20
3
+ Version: 0.0.21
4
4
  Summary: A streaming multipart parser for Python
5
5
  Project-URL: Homepage, https://github.com/Kludex/python-multipart
6
6
  Project-URL: Documentation, https://kludex.github.io/python-multipart/
@@ -16,13 +16,13 @@ Classifier: License :: OSI Approved :: Apache Software License
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3 :: Only
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Programming Language :: Python :: 3.9
21
19
  Classifier: Programming Language :: Python :: 3.10
22
20
  Classifier: Programming Language :: Python :: 3.11
23
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
- Requires-Python: >=3.8
25
+ Requires-Python: >=3.10
26
26
  Description-Content-Type: text/markdown
27
27
 
28
28
  # [Python-Multipart](https://kludex.github.io/python-multipart/)
@@ -8,7 +8,7 @@ dynamic = ["version"]
8
8
  description = "A streaming multipart parser for Python"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
- requires-python = ">=3.8"
11
+ requires-python = ">=3.10"
12
12
  authors = [
13
13
  { name = "Andrew Dunham", email = "andrew@du.nham.ca" },
14
14
  { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
@@ -21,17 +21,17 @@ classifiers = [
21
21
  'Operating System :: OS Independent',
22
22
  'Programming Language :: Python :: 3 :: Only',
23
23
  'Programming Language :: Python :: 3',
24
- 'Programming Language :: Python :: 3.8',
25
- 'Programming Language :: Python :: 3.9',
26
24
  'Programming Language :: Python :: 3.10',
27
25
  'Programming Language :: Python :: 3.11',
28
26
  'Programming Language :: Python :: 3.12',
27
+ 'Programming Language :: Python :: 3.13',
28
+ 'Programming Language :: Python :: 3.14',
29
29
  'Topic :: Software Development :: Libraries :: Python Modules',
30
30
  ]
31
31
  dependencies = []
32
32
 
33
- [tool.uv]
34
- dev-dependencies = [
33
+ [dependency-groups]
34
+ dev = [
35
35
  "atomicwrites==1.4.1",
36
36
  "attrs==23.2.0",
37
37
  "coverage==7.4.4",
@@ -44,7 +44,7 @@ dev-dependencies = [
44
44
  "PyYAML==6.0.1",
45
45
  "invoke==2.2.0",
46
46
  "pytest-timeout==2.3.1",
47
- "ruff==0.8.0",
47
+ "ruff==0.11.7",
48
48
  "mypy",
49
49
  "types-PyYAML",
50
50
  "atheris==2.3.0; python_version <= '3.11'",
@@ -2,7 +2,7 @@
2
2
  __author__ = "Andrew Dunham"
3
3
  __license__ = "Apache"
4
4
  __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham"
5
- __version__ = "0.0.20"
5
+ __version__ = "0.0.21"
6
6
 
7
7
  from .multipart import (
8
8
  BaseParser,
@@ -62,7 +62,7 @@ class Base64Decoder:
62
62
 
63
63
  # Prepend any cache info to our data.
64
64
  if len(self.cache) > 0:
65
- data = self.cache + data
65
+ data = bytes(self.cache) + data
66
66
 
67
67
  # Slice off a string that's a multiple of 4.
68
68
  decode_len = (len(data) // 4) * 4
@@ -15,9 +15,8 @@ from .decoders import Base64Decoder, QuotedPrintableDecoder
15
15
  from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError
16
16
 
17
17
  if TYPE_CHECKING: # pragma: no cover
18
- from typing import Any, Callable, Literal, Protocol, TypedDict
19
-
20
- from typing_extensions import TypeAlias
18
+ from collections.abc import Callable
19
+ from typing import Any, Literal, Protocol, TypeAlias, TypedDict
21
20
 
22
21
  class SupportsRead(Protocol):
23
22
  def read(self, __n: int) -> bytes: ...
@@ -332,7 +331,7 @@ class Field:
332
331
  else:
333
332
  v = repr(self.value)
334
333
 
335
- return "{}(field_name={!r}, value={})".format(self.__class__.__name__, self.field_name, v)
334
+ return f"{self.__class__.__name__}(field_name={self.field_name!r}, value={v})"
336
335
 
337
336
 
338
337
  class File:
@@ -570,7 +569,7 @@ class File:
570
569
  self._fileobj.close()
571
570
 
572
571
  def __repr__(self) -> str:
573
- return "{}(file_name={!r}, field_name={!r})".format(self.__class__.__name__, self.file_name, self.field_name)
572
+ return f"{self.__class__.__name__}(file_name={self.file_name!r}, field_name={self.field_name!r})"
574
573
 
575
574
 
576
575
  class BaseParser:
@@ -1241,7 +1240,7 @@ class MultipartParser(BaseParser):
1241
1240
  elif state == MultipartState.HEADER_VALUE_ALMOST_DONE:
1242
1241
  # The last character should be a LF. If not, it's an error.
1243
1242
  if c != LF:
1244
- msg = "Did not find LF character at end of header " "(found %r)" % (c,)
1243
+ msg = f"Did not find LF character at end of header (found {c!r})"
1245
1244
  self.logger.warning(msg)
1246
1245
  e = MultipartParseError(msg)
1247
1246
  e.offset = i
@@ -1715,7 +1714,7 @@ class FormParser:
1715
1714
  else:
1716
1715
  self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding)
1717
1716
  if self.config["UPLOAD_ERROR_ON_BAD_CTE"]:
1718
- raise FormParserError('Unknown Content-Transfer-Encoding "{!r}"'.format(transfer_encoding))
1717
+ raise FormParserError(f'Unknown Content-Transfer-Encoding "{transfer_encoding!r}"')
1719
1718
  else:
1720
1719
  # If we aren't erroring, then we just treat this as an
1721
1720
  # unencoded Content-Transfer-Encoding.
@@ -1746,7 +1745,7 @@ class FormParser:
1746
1745
 
1747
1746
  else:
1748
1747
  self.logger.warning("Unknown Content-Type: %r", content_type)
1749
- raise FormParserError("Unknown Content-Type: {}".format(content_type))
1748
+ raise FormParserError(f"Unknown Content-Type: {content_type}")
1750
1749
 
1751
1750
  self.parser = parser
1752
1751
 
@@ -1776,7 +1775,7 @@ class FormParser:
1776
1775
  self.parser.close()
1777
1776
 
1778
1777
  def __repr__(self) -> str:
1779
- return "{}(content_type={!r}, parser={!r})".format(self.__class__.__name__, self.content_type, self.parser)
1778
+ return f"{self.__class__.__name__}(content_type={self.content_type!r}, parser={self.parser!r})"
1780
1779
 
1781
1780
 
1782
1781
  def create_form_parser(
@@ -8,7 +8,8 @@ import types
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  if TYPE_CHECKING:
11
- from typing import Any, Callable
11
+ from collections.abc import Callable
12
+ from typing import Any
12
13
 
13
14
 
14
15
  def ensure_in_path(path: str) -> None:
@@ -37,7 +37,8 @@ from python_multipart.multipart import (
37
37
  from .compat import parametrize, parametrize_class
38
38
 
39
39
  if TYPE_CHECKING:
40
- from typing import Any, Iterator, TypedDict
40
+ from collections.abc import Iterator
41
+ from typing import Any, TypedDict
41
42
 
42
43
  from python_multipart.multipart import FieldProtocol, FileConfig, FileProtocol
43
44
 
@@ -1069,7 +1070,9 @@ class TestFormParser(unittest.TestCase):
1069
1070
 
1070
1071
  self.make("boundary")
1071
1072
  data = b"--Boundary\r\nfoobar"
1072
- with self.assertRaisesRegex(MultipartParseError, "Expected boundary character %r, got %r" % (b"b"[0], b"B"[0])):
1073
+ with self.assertRaisesRegex(
1074
+ MultipartParseError, "Expected boundary character {!r}, got {!r}".format(b"b"[0], b"B"[0])
1075
+ ):
1073
1076
  self.f.write(data)
1074
1077
 
1075
1078
  def test_octet_stream(self) -> None: