python-multipart 0.0.21__tar.gz → 0.0.23__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.21 → python_multipart-0.0.23}/CHANGELOG.md +10 -0
  2. {python_multipart-0.0.21 → python_multipart-0.0.23}/PKG-INFO +3 -2
  3. {python_multipart-0.0.21 → python_multipart-0.0.23}/multipart/__init__.py +1 -1
  4. {python_multipart-0.0.21 → python_multipart-0.0.23}/pyproject.toml +13 -11
  5. {python_multipart-0.0.21 → python_multipart-0.0.23}/python_multipart/__init__.py +1 -5
  6. {python_multipart-0.0.21 → python_multipart-0.0.23}/python_multipart/multipart.py +5 -9
  7. python_multipart-0.0.23/tests/test_file.py +26 -0
  8. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_multipart.py +1 -0
  9. {python_multipart-0.0.21 → python_multipart-0.0.23}/.gitignore +0 -0
  10. {python_multipart-0.0.21 → python_multipart-0.0.23}/LICENSE.txt +0 -0
  11. {python_multipart-0.0.21 → python_multipart-0.0.23}/README.md +0 -0
  12. {python_multipart-0.0.21 → python_multipart-0.0.23}/multipart/decoders.py +0 -0
  13. {python_multipart-0.0.21 → python_multipart-0.0.23}/multipart/exceptions.py +0 -0
  14. {python_multipart-0.0.21 → python_multipart-0.0.23}/multipart/multipart.py +0 -0
  15. {python_multipart-0.0.21 → python_multipart-0.0.23}/python_multipart/decoders.py +0 -0
  16. {python_multipart-0.0.21 → python_multipart-0.0.23}/python_multipart/exceptions.py +0 -0
  17. {python_multipart-0.0.21 → python_multipart-0.0.23}/python_multipart/py.typed +0 -0
  18. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/__init__.py +0 -0
  19. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/compat.py +0 -0
  20. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/CRLF_in_header.http +0 -0
  21. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/CRLF_in_header.yaml +0 -0
  22. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/CR_in_header.http +0 -0
  23. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/CR_in_header.yaml +0 -0
  24. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/CR_in_header_value.http +0 -0
  25. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  26. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary.http +0 -0
  27. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  28. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  29. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  30. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  31. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  32. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  33. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  34. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/bad_end_of_headers.http +0 -0
  35. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  36. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/bad_header_char.http +0 -0
  37. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/bad_header_char.yaml +0 -0
  38. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/bad_initial_boundary.http +0 -0
  39. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  40. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/base64_encoding.http +0 -0
  41. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/base64_encoding.yaml +0 -0
  42. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/empty_header.http +0 -0
  43. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/empty_header.yaml +0 -0
  44. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/empty_message.http +0 -0
  45. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/empty_message.yaml +0 -0
  46. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/empty_message_with_bad_end.http +0 -0
  47. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/empty_message_with_bad_end.yaml +0 -0
  48. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/header_with_number.http +0 -0
  49. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/header_with_number.yaml +0 -0
  50. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
  51. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
  52. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/multiple_fields.http +0 -0
  53. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/multiple_fields.yaml +0 -0
  54. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/multiple_files.http +0 -0
  55. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/multiple_files.yaml +0 -0
  56. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  57. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  58. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field.http +0 -0
  59. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field.yaml +0 -0
  60. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_blocks.http +0 -0
  61. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_blocks.yaml +0 -0
  62. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_longer.http +0 -0
  63. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_longer.yaml +0 -0
  64. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_single_file.http +0 -0
  65. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_single_file.yaml +0 -0
  66. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  67. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  68. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_file.http +0 -0
  69. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/single_file.yaml +0 -0
  70. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/utf8_filename.http +0 -0
  71. {python_multipart-0.0.21 → python_multipart-0.0.23}/tests/test_data/http/utf8_filename.yaml +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.23 (2026-04-05)
4
+
5
+ * Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#196](https://github.com/Kludex/python-multipart/pull/196).
6
+ * Return processed length from `QuerystringParser._internal_write` [#229](https://github.com/Kludex/python-multipart/pull/229).
7
+ * Cleanup metadata dunders from `__init__.py` [#227](https://github.com/Kludex/python-multipart/pull/227).
8
+
9
+ ## 0.0.22 (2026-01-25)
10
+
11
+ * Drop directory path from filename in `File` [9433f4b](https://github.com/Kludex/python-multipart/commit/9433f4bbc9652bdde82bbe380984e32f8cfc89c4).
12
+
3
13
  ## 0.0.21 (2025-12-17)
4
14
 
5
15
  * Add support for Python 3.14 and drop EOL 3.8 and 3.9 [#216](https://github.com/Kludex/python-multipart/pull/216).
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-multipart
3
- Version: 0.0.21
3
+ Version: 0.0.23
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/
7
7
  Project-URL: Changelog, https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md
8
8
  Project-URL: Source, https://github.com/Kludex/python-multipart
9
- Author-email: Andrew Dunham <andrew@du.nham.ca>, Marcelo Trylesinski <marcelotryle@gmail.com>
9
+ Author-email: Andrew Dunham <andrew@du.nham.ca>
10
+ Maintainer-email: Marcelo Trylesinski <marcelotryle@gmail.com>
10
11
  License-Expression: Apache-2.0
11
12
  License-File: LICENSE.txt
12
13
  Classifier: Development Status :: 5 - Production/Stable
@@ -21,4 +21,4 @@ for p in sys.path:
21
21
  else:
22
22
  warnings.warn("Please use `import python_multipart` instead.", PendingDeprecationWarning, stacklevel=2)
23
23
  from python_multipart import *
24
- from python_multipart import __all__, __author__, __copyright__, __license__, __version__
24
+ from python_multipart import __all__, __version__
@@ -11,6 +11,8 @@ license = "Apache-2.0"
11
11
  requires-python = ">=3.10"
12
12
  authors = [
13
13
  { name = "Andrew Dunham", email = "andrew@du.nham.ca" },
14
+ ]
15
+ maintainers = [
14
16
  { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
15
17
  ]
16
18
  classifiers = [
@@ -33,18 +35,18 @@ dependencies = []
33
35
  [dependency-groups]
34
36
  dev = [
35
37
  "atomicwrites==1.4.1",
36
- "attrs==23.2.0",
37
- "coverage==7.4.4",
38
- "more-itertools==10.2.0",
39
- "pbr==6.0.0",
40
- "pluggy==1.4.0",
38
+ "attrs==26.1.0",
39
+ "coverage==7.13.5",
40
+ "more-itertools==10.8.0",
41
+ "pbr==7.0.3",
42
+ "pluggy==1.6.0",
41
43
  "py==1.11.0",
42
- "pytest==8.1.1",
43
- "pytest-cov==5.0.0",
44
- "PyYAML==6.0.1",
45
- "invoke==2.2.0",
46
- "pytest-timeout==2.3.1",
47
- "ruff==0.11.7",
44
+ "pytest==9.0.2",
45
+ "pytest-cov==7.1.0",
46
+ "PyYAML==6.0.3",
47
+ "invoke==2.2.1",
48
+ "pytest-timeout==2.4.0",
49
+ "ruff==0.15.8",
48
50
  "mypy",
49
51
  "types-PyYAML",
50
52
  "atheris==2.3.0; python_version <= '3.11'",
@@ -1,8 +1,4 @@
1
- # This is the canonical package information.
2
- __author__ = "Andrew Dunham"
3
- __license__ = "Apache"
4
- __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham"
5
- __version__ = "0.0.21"
1
+ __version__ = "0.0.23"
6
2
 
7
3
  from .multipart import (
8
4
  BaseParser,
@@ -375,7 +375,9 @@ class File:
375
375
 
376
376
  # Split the extension from the filename.
377
377
  if file_name is not None:
378
- 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)
379
381
  self._file_base = base
380
382
  self._ext = ext
381
383
 
@@ -934,7 +936,7 @@ class QuerystringParser(BaseParser):
934
936
 
935
937
  self.state = state
936
938
  self._found_sep = found_sep
937
- return len(data)
939
+ return length
938
940
 
939
941
  def finalize(self) -> None:
940
942
  """Finalize this parser, which signals to that we are finished parsing,
@@ -1782,7 +1784,6 @@ def create_form_parser(
1782
1784
  headers: dict[str, bytes],
1783
1785
  on_field: OnFieldCallback | None,
1784
1786
  on_file: OnFileCallback | None,
1785
- trust_x_headers: bool = False,
1786
1787
  config: dict[Any, Any] = {},
1787
1788
  ) -> FormParser:
1788
1789
  """This function is a helper function to aid in creating a FormParser
@@ -1795,8 +1796,6 @@ def create_form_parser(
1795
1796
  headers: A dictionary-like object of HTTP headers. The only required header is Content-Type.
1796
1797
  on_field: Callback to call with each parsed field.
1797
1798
  on_file: Callback to call with each parsed file.
1798
- trust_x_headers: Whether or not to trust information received from certain X-Headers - for example, the file
1799
- name from X-File-Name.
1800
1799
  config: Configuration variables to pass to the FormParser.
1801
1800
  """
1802
1801
  content_type: str | bytes | None = headers.get("Content-Type")
@@ -1812,11 +1811,8 @@ def create_form_parser(
1812
1811
  # We need content_type to be a string, not a bytes object.
1813
1812
  content_type = content_type.decode("latin-1")
1814
1813
 
1815
- # File names are optional.
1816
- file_name = headers.get("X-File-Name")
1817
-
1818
1814
  # Instantiate a form parser.
1819
- form_parser = FormParser(content_type, on_field, on_file, boundary=boundary, file_name=file_name, config=config)
1815
+ form_parser = FormParser(content_type, on_field, on_file, boundary=boundary, config=config)
1820
1816
 
1821
1817
  # Return our parser.
1822
1818
  return form_parser
@@ -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"
@@ -1371,6 +1371,7 @@ class TestFormParser(unittest.TestCase):
1371
1371
  self.assertEqual(calls, 3)
1372
1372
 
1373
1373
 
1374
+ @parametrize_class
1374
1375
  class TestHelperFunctions(unittest.TestCase):
1375
1376
  def test_create_form_parser(self) -> None:
1376
1377
  r = create_form_parser({"Content-Type": b"application/octet-stream"}, None, None)