python-multipart 0.0.20__tar.gz → 0.0.22__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 (71) hide show
  1. {python_multipart-0.0.20 → python_multipart-0.0.22}/CHANGELOG.md +8 -0
  2. {python_multipart-0.0.20 → python_multipart-0.0.22}/PKG-INFO +4 -4
  3. {python_multipart-0.0.20 → python_multipart-0.0.22}/pyproject.toml +6 -6
  4. {python_multipart-0.0.20 → python_multipart-0.0.22}/python_multipart/__init__.py +1 -1
  5. {python_multipart-0.0.20 → python_multipart-0.0.22}/python_multipart/decoders.py +1 -1
  6. {python_multipart-0.0.20 → python_multipart-0.0.22}/python_multipart/multipart.py +11 -10
  7. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/compat.py +2 -1
  8. python_multipart-0.0.22/tests/test_file.py +26 -0
  9. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_multipart.py +5 -2
  10. {python_multipart-0.0.20 → python_multipart-0.0.22}/.gitignore +0 -0
  11. {python_multipart-0.0.20 → python_multipart-0.0.22}/LICENSE.txt +0 -0
  12. {python_multipart-0.0.20 → python_multipart-0.0.22}/README.md +0 -0
  13. {python_multipart-0.0.20 → python_multipart-0.0.22}/multipart/__init__.py +0 -0
  14. {python_multipart-0.0.20 → python_multipart-0.0.22}/multipart/decoders.py +0 -0
  15. {python_multipart-0.0.20 → python_multipart-0.0.22}/multipart/exceptions.py +0 -0
  16. {python_multipart-0.0.20 → python_multipart-0.0.22}/multipart/multipart.py +0 -0
  17. {python_multipart-0.0.20 → python_multipart-0.0.22}/python_multipart/exceptions.py +0 -0
  18. {python_multipart-0.0.20 → python_multipart-0.0.22}/python_multipart/py.typed +0 -0
  19. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/__init__.py +0 -0
  20. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/CRLF_in_header.http +0 -0
  21. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/CRLF_in_header.yaml +0 -0
  22. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/CR_in_header.http +0 -0
  23. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/CR_in_header.yaml +0 -0
  24. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/CR_in_header_value.http +0 -0
  25. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  26. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary.http +0 -0
  27. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  28. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  29. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  30. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  31. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  32. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  33. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  34. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/bad_end_of_headers.http +0 -0
  35. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  36. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/bad_header_char.http +0 -0
  37. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/bad_header_char.yaml +0 -0
  38. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/bad_initial_boundary.http +0 -0
  39. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  40. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/base64_encoding.http +0 -0
  41. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/base64_encoding.yaml +0 -0
  42. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/empty_header.http +0 -0
  43. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/empty_header.yaml +0 -0
  44. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/empty_message.http +0 -0
  45. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/empty_message.yaml +0 -0
  46. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/empty_message_with_bad_end.http +0 -0
  47. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/empty_message_with_bad_end.yaml +0 -0
  48. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/header_with_number.http +0 -0
  49. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/header_with_number.yaml +0 -0
  50. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
  51. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
  52. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/multiple_fields.http +0 -0
  53. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/multiple_fields.yaml +0 -0
  54. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/multiple_files.http +0 -0
  55. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/multiple_files.yaml +0 -0
  56. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  57. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  58. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field.http +0 -0
  59. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field.yaml +0 -0
  60. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_blocks.http +0 -0
  61. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_blocks.yaml +0 -0
  62. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_longer.http +0 -0
  63. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_longer.yaml +0 -0
  64. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_single_file.http +0 -0
  65. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_single_file.yaml +0 -0
  66. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  67. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  68. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_file.http +0 -0
  69. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/single_file.yaml +0 -0
  70. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/utf8_filename.http +0 -0
  71. {python_multipart-0.0.20 → python_multipart-0.0.22}/tests/test_data/http/utf8_filename.yaml +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.22 (2026-01-25)
4
+
5
+ * Drop directory path from filename in `File` [9433f4b](https://github.com/Kludex/python-multipart/commit/9433f4bbc9652bdde82bbe380984e32f8cfc89c4).
6
+
7
+ ## 0.0.21 (2025-12-17)
8
+
9
+ * Add support for Python 3.14 and drop EOL 3.8 and 3.9 [#216](https://github.com/Kludex/python-multipart/pull/216).
10
+
3
11
  ## 0.0.20 (2024-12-16)
4
12
 
5
13
  * 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.22
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.22"
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:
@@ -376,7 +375,9 @@ class File:
376
375
 
377
376
  # Split the extension from the filename.
378
377
  if file_name is not None:
379
- base, ext = os.path.splitext(file_name)
378
+ # Extract just the basename to avoid directory traversal
379
+ basename = os.path.basename(file_name)
380
+ base, ext = os.path.splitext(basename)
380
381
  self._file_base = base
381
382
  self._ext = ext
382
383
 
@@ -570,7 +571,7 @@ class File:
570
571
  self._fileobj.close()
571
572
 
572
573
  def __repr__(self) -> str:
573
- return "{}(file_name={!r}, field_name={!r})".format(self.__class__.__name__, self.file_name, self.field_name)
574
+ return f"{self.__class__.__name__}(file_name={self.file_name!r}, field_name={self.field_name!r})"
574
575
 
575
576
 
576
577
  class BaseParser:
@@ -1241,7 +1242,7 @@ class MultipartParser(BaseParser):
1241
1242
  elif state == MultipartState.HEADER_VALUE_ALMOST_DONE:
1242
1243
  # The last character should be a LF. If not, it's an error.
1243
1244
  if c != LF:
1244
- msg = "Did not find LF character at end of header " "(found %r)" % (c,)
1245
+ msg = f"Did not find LF character at end of header (found {c!r})"
1245
1246
  self.logger.warning(msg)
1246
1247
  e = MultipartParseError(msg)
1247
1248
  e.offset = i
@@ -1715,7 +1716,7 @@ class FormParser:
1715
1716
  else:
1716
1717
  self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding)
1717
1718
  if self.config["UPLOAD_ERROR_ON_BAD_CTE"]:
1718
- raise FormParserError('Unknown Content-Transfer-Encoding "{!r}"'.format(transfer_encoding))
1719
+ raise FormParserError(f'Unknown Content-Transfer-Encoding "{transfer_encoding!r}"')
1719
1720
  else:
1720
1721
  # If we aren't erroring, then we just treat this as an
1721
1722
  # unencoded Content-Transfer-Encoding.
@@ -1746,7 +1747,7 @@ class FormParser:
1746
1747
 
1747
1748
  else:
1748
1749
  self.logger.warning("Unknown Content-Type: %r", content_type)
1749
- raise FormParserError("Unknown Content-Type: {}".format(content_type))
1750
+ raise FormParserError(f"Unknown Content-Type: {content_type}")
1750
1751
 
1751
1752
  self.parser = parser
1752
1753
 
@@ -1776,7 +1777,7 @@ class FormParser:
1776
1777
  self.parser.close()
1777
1778
 
1778
1779
  def __repr__(self) -> str:
1779
- return "{}(content_type={!r}, parser={!r})".format(self.__class__.__name__, self.content_type, self.parser)
1780
+ return f"{self.__class__.__name__}(content_type={self.content_type!r}, parser={self.parser!r})"
1780
1781
 
1781
1782
 
1782
1783
  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:
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+
3
+ from python_multipart.multipart import File
4
+
5
+
6
+ def test_upload_dir_with_leading_slash_in_filename(tmp_path: Path) -> None:
7
+ upload_dir = tmp_path / "upload"
8
+ upload_dir.mkdir()
9
+
10
+ # When the file_name provided has a leading slash, we should only use the basename.
11
+ # This is to avoid directory traversal.
12
+ to_upload = tmp_path / "foo.txt"
13
+
14
+ file = File(
15
+ bytes(to_upload),
16
+ config={
17
+ "UPLOAD_DIR": bytes(upload_dir),
18
+ "UPLOAD_KEEP_FILENAME": True,
19
+ "UPLOAD_KEEP_EXTENSIONS": True,
20
+ "MAX_MEMORY_FILE_SIZE": 10,
21
+ },
22
+ )
23
+ file.write(b"123456789012")
24
+ assert not file.in_memory
25
+ assert Path(upload_dir / "foo.txt").exists()
26
+ assert Path(upload_dir / "foo.txt").read_bytes() == b"123456789012"
@@ -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: