python-multipart 0.0.11__tar.gz → 0.0.13__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 (64) hide show
  1. {python_multipart-0.0.11 → python_multipart-0.0.13}/.gitignore +1 -0
  2. {python_multipart-0.0.11 → python_multipart-0.0.13}/CHANGELOG.md +10 -0
  3. {python_multipart-0.0.11 → python_multipart-0.0.13}/PKG-INFO +1 -10
  4. {python_multipart-0.0.11 → python_multipart-0.0.13}/README.md +0 -9
  5. python_multipart-0.0.13/_python_multipart.pth +1 -0
  6. python_multipart-0.0.13/_python_multipart_loader.py +37 -0
  7. {python_multipart-0.0.11 → python_multipart-0.0.13}/pyproject.toml +15 -6
  8. {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/__init__.py +1 -1
  9. {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/decoders.py +23 -10
  10. {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/multipart.py +113 -85
  11. python_multipart-0.0.13/tests/__init__.py +0 -0
  12. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/compat.py +17 -9
  13. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_multipart.py +198 -170
  14. {python_multipart-0.0.11 → python_multipart-0.0.13}/LICENSE.txt +0 -0
  15. {python_multipart-0.0.11/multipart → python_multipart-0.0.13/python_multipart}/exceptions.py +0 -0
  16. /python_multipart-0.0.11/tests/__init__.py → /python_multipart-0.0.13/python_multipart/py.typed +0 -0
  17. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CRLF_in_header.http +0 -0
  18. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CRLF_in_header.yaml +0 -0
  19. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header.http +0 -0
  20. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header.yaml +0 -0
  21. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header_value.http +0 -0
  22. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  23. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary.http +0 -0
  24. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  25. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  26. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  27. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  28. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  29. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  30. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  31. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_end_of_headers.http +0 -0
  32. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  33. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_header_char.http +0 -0
  34. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_header_char.yaml +0 -0
  35. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_initial_boundary.http +0 -0
  36. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  37. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/base64_encoding.http +0 -0
  38. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/base64_encoding.yaml +0 -0
  39. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/empty_header.http +0 -0
  40. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/empty_header.yaml +0 -0
  41. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/header_with_number.http +0 -0
  42. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/header_with_number.yaml +0 -0
  43. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
  44. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
  45. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_fields.http +0 -0
  46. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_fields.yaml +0 -0
  47. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_files.http +0 -0
  48. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/multiple_files.yaml +0 -0
  49. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  50. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  51. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field.http +0 -0
  52. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field.yaml +0 -0
  53. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_blocks.http +0 -0
  54. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_blocks.yaml +0 -0
  55. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_longer.http +0 -0
  56. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_longer.yaml +0 -0
  57. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_single_file.http +0 -0
  58. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_single_file.yaml +0 -0
  59. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  60. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  61. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_file.http +0 -0
  62. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/single_file.yaml +0 -0
  63. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/utf8_filename.http +0 -0
  64. {python_multipart-0.0.11 → python_multipart-0.0.13}/tests/test_data/http/utf8_filename.yaml +0 -0
@@ -89,6 +89,7 @@ coverage.xml
89
89
  *.py,cover
90
90
  .hypothesis/
91
91
  .pytest_cache/
92
+ .ruff_cache/
92
93
  cover/
93
94
 
94
95
  # Translations
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.13 (2024-10-20)
4
+
5
+ * Rename import to `python_multipart` [#166](https://github.com/Kludex/python-multipart/pull/166).
6
+
7
+ ## 0.0.12 (2024-09-29)
8
+
9
+ * Improve error message when boundary character does not match [#124](https://github.com/Kludex/python-multipart/pull/124).
10
+ * Add mypy strict typing [#140](https://github.com/Kludex/python-multipart/pull/140).
11
+ * Enforce 100% coverage [#159](https://github.com/Kludex/python-multipart/pull/159).
12
+
3
13
  ## 0.0.11 (2024-09-28)
4
14
 
5
15
  * Improve performance, especially in data with many CR-LF [#137](https://github.com/Kludex/python-multipart/pull/137).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-multipart
3
- Version: 0.0.11
3
+ Version: 0.0.13
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/
@@ -38,12 +38,3 @@ Test coverage is currently 100%.
38
38
  ## Why?
39
39
 
40
40
  Because streaming uploads are awesome for large files.
41
-
42
- ## How to Test
43
-
44
- If you want to test:
45
-
46
- ```bash
47
- $ pip install '.[dev]'
48
- $ inv test
49
- ```
@@ -11,12 +11,3 @@ Test coverage is currently 100%.
11
11
  ## Why?
12
12
 
13
13
  Because streaming uploads are awesome for large files.
14
-
15
- ## How to Test
16
-
17
- If you want to test:
18
-
19
- ```bash
20
- $ pip install '.[dev]'
21
- $ inv test
22
- ```
@@ -0,0 +1 @@
1
+ import _python_multipart_loader
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ # The purpose of this file is to allow `import multipart` to continue to work
4
+ # unless `multipart` (the PyPI package) is also installed, in which case
5
+ # a collision is avoided, and `import multipart` is no longer injected.
6
+ import importlib
7
+ import importlib.abc
8
+ import importlib.machinery
9
+ import importlib.util
10
+ import sys
11
+ import warnings
12
+
13
+
14
+ class PythonMultipartCompatFinder(importlib.abc.MetaPathFinder):
15
+ def find_spec(
16
+ self, fullname: str, path: object = None, target: object = None
17
+ ) -> importlib.machinery.ModuleSpec | None:
18
+ if fullname != "multipart":
19
+ return None
20
+ old_sys_meta_path = sys.meta_path
21
+ try:
22
+ sys.meta_path = [p for p in sys.meta_path if not isinstance(p, type(self))]
23
+ if multipart := importlib.util.find_spec("multipart"):
24
+ return multipart
25
+
26
+ warnings.warn("Please use `import python_multipart` instead.", FutureWarning, stacklevel=2)
27
+ sys.modules["multipart"] = importlib.import_module("python_multipart")
28
+ return importlib.util.find_spec("python_multipart")
29
+ finally:
30
+ sys.meta_path = old_sys_meta_path
31
+
32
+
33
+ def install() -> None:
34
+ sys.meta_path.insert(0, PythonMultipartCompatFinder())
35
+
36
+
37
+ install()
@@ -45,6 +45,8 @@ dev-dependencies = [
45
45
  "invoke==2.2.0",
46
46
  "pytest-timeout==2.3.1",
47
47
  "ruff==0.3.4",
48
+ "mypy",
49
+ "types-PyYAML",
48
50
  "atheris==2.3.0; python_version != '3.12'",
49
51
  # Documentation
50
52
  "mkdocs",
@@ -53,6 +55,9 @@ dev-dependencies = [
53
55
  "mkdocs-autorefs",
54
56
  ]
55
57
 
58
+ [tool.uv.pip]
59
+ reinstall-package = ["python-multipart"]
60
+
56
61
  [project.urls]
57
62
  Homepage = "https://github.com/Kludex/python-multipart"
58
63
  Documentation = "https://kludex.github.io/python-multipart/"
@@ -60,13 +65,17 @@ Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md
60
65
  Source = "https://github.com/Kludex/python-multipart"
61
66
 
62
67
  [tool.hatch.version]
63
- path = "multipart/__init__.py"
64
-
65
- [tool.hatch.build.targets.wheel]
66
- packages = ["multipart"]
68
+ path = "python_multipart/__init__.py"
67
69
 
68
70
  [tool.hatch.build.targets.sdist]
69
- include = ["/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"]
71
+ include = ["/python_multipart", "/tests", "CHANGELOG.md", "LICENSE.txt", "_python_multipart.pth", "_python_multipart_loader.py"]
72
+
73
+ [tool.hatch.build.targets.wheel.force-include]
74
+ "_python_multipart.pth" = "_python_multipart.pth"
75
+ "_python_multipart_loader.py" = "_python_multipart_loader.py"
76
+
77
+ [tool.mypy]
78
+ strict = true
70
79
 
71
80
  [tool.ruff]
72
81
  line-length = 120
@@ -87,7 +96,7 @@ branch = false
87
96
  omit = ["tests/*"]
88
97
 
89
98
  [tool.coverage.report]
90
- # fail_under = 100
99
+ fail_under = 100
91
100
  skip_covered = true
92
101
  show_missing = true
93
102
  exclude_lines = [
@@ -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.11"
5
+ __version__ = "0.0.13"
6
6
 
7
7
  from .multipart import (
8
8
  BaseParser,
@@ -1,9 +1,22 @@
1
1
  import base64
2
2
  import binascii
3
- from io import BufferedWriter
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from .exceptions import DecodeError
6
6
 
7
+ if TYPE_CHECKING: # pragma: no cover
8
+ from typing import Protocol, TypeVar
9
+
10
+ _T_contra = TypeVar("_T_contra", contravariant=True)
11
+
12
+ class SupportsWrite(Protocol[_T_contra]):
13
+ def write(self, __b: _T_contra) -> object: ...
14
+
15
+ # No way to specify optional methods. See
16
+ # https://github.com/python/typing/issues/601
17
+ # close() [Optional]
18
+ # finalize() [Optional]
19
+
7
20
 
8
21
  class Base64Decoder:
9
22
  """This object provides an interface to decode a stream of Base64 data. It
@@ -12,7 +25,7 @@ class Base64Decoder:
12
25
  call write() on the underlying object. This is primarily used for decoding
13
26
  form data encoded as Base64, but can be used for other purposes::
14
27
 
15
- from multipart.decoders import Base64Decoder
28
+ from python_multipart.decoders import Base64Decoder
16
29
  fd = open("notb64.txt", "wb")
17
30
  decoder = Base64Decoder(fd)
18
31
  try:
@@ -34,7 +47,7 @@ class Base64Decoder:
34
47
  :param underlying: the underlying object to pass writes to
35
48
  """
36
49
 
37
- def __init__(self, underlying: BufferedWriter):
50
+ def __init__(self, underlying: "SupportsWrite[bytes]") -> None:
38
51
  self.cache = bytearray()
39
52
  self.underlying = underlying
40
53
 
@@ -42,7 +55,7 @@ class Base64Decoder:
42
55
  """Takes any input data provided, decodes it as base64, and passes it
43
56
  on to the underlying object. If the data provided is invalid base64
44
57
  data, then this method will raise
45
- a :class:`multipart.exceptions.DecodeError`
58
+ a :class:`python_multipart.exceptions.DecodeError`
46
59
 
47
60
  :param data: base64 data to decode
48
61
  """
@@ -67,9 +80,9 @@ class Base64Decoder:
67
80
  # Get the remaining bytes and save in our cache.
68
81
  remaining_len = len(data) % 4
69
82
  if remaining_len > 0:
70
- self.cache = data[-remaining_len:]
83
+ self.cache[:] = data[-remaining_len:]
71
84
  else:
72
- self.cache = b""
85
+ self.cache[:] = b""
73
86
 
74
87
  # Return the length of the data to indicate no error.
75
88
  return len(data)
@@ -84,7 +97,7 @@ class Base64Decoder:
84
97
  def finalize(self) -> None:
85
98
  """Finalize this object. This should be called when no more data
86
99
  should be written to the stream. This function can raise a
87
- :class:`multipart.exceptions.DecodeError` if there is some remaining
100
+ :class:`python_multipart.exceptions.DecodeError` if there is some remaining
88
101
  data in the cache.
89
102
 
90
103
  If the underlying object has a `finalize()` method, this function will
@@ -105,14 +118,14 @@ class Base64Decoder:
105
118
  class QuotedPrintableDecoder:
106
119
  """This object provides an interface to decode a stream of quoted-printable
107
120
  data. It is instantiated with an "underlying object", in the same manner
108
- as the :class:`multipart.decoders.Base64Decoder` class. This class behaves
121
+ as the :class:`python_multipart.decoders.Base64Decoder` class. This class behaves
109
122
  in exactly the same way, including maintaining a cache of quoted-printable
110
123
  chunks.
111
124
 
112
125
  :param underlying: the underlying object to pass writes to
113
126
  """
114
127
 
115
- def __init__(self, underlying: BufferedWriter) -> None:
128
+ def __init__(self, underlying: "SupportsWrite[bytes]") -> None:
116
129
  self.cache = b""
117
130
  self.underlying = underlying
118
131
 
@@ -160,7 +173,7 @@ class QuotedPrintableDecoder:
160
173
  call it.
161
174
  """
162
175
  # If we have a cache, write and then remove it.
163
- if len(self.cache) > 0:
176
+ if len(self.cache) > 0: # pragma: no cover
164
177
  self.underlying.write(binascii.a2b_qp(self.cache))
165
178
  self.cache = b""
166
179