Skip to content

Commit a629ae0

Browse files
authored
refactor: rename import to python_multipart (#166)
1 parent 5303590 commit a629ae0

17 files changed

+120
-32
lines changed

.github/workflows/main.yml

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ jobs:
3333
- name: Run tests
3434
run: scripts/test
3535

36+
- name: Run rename test
37+
run: uvx nox -s rename -P ${{ matrix.python-version }}
38+
3639
# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
3740
check:
3841
if: always()

_python_multipart.pth

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import _python_multipart_loader

_python_multipart_loader.py

+37
Original file line numberDiff line numberDiff line change
@@ -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()

docs/api.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
::: multipart
1+
::: python_multipart
22

3-
::: multipart.exceptions
3+
::: python_multipart.exceptions

docs/index.md

+14-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Python-Multipart is a streaming multipart parser for Python.
99
The following example shows a quick example of parsing an incoming request body in a simple WSGI application:
1010

1111
```python
12-
import multipart
12+
import python_multipart
1313

1414
def simple_app(environ, start_response):
1515
ret = []
@@ -31,7 +31,7 @@ def simple_app(environ, start_response):
3131
headers['Content-Length'] = environ['CONTENT_LENGTH']
3232

3333
# Parse the form.
34-
multipart.parse_form(headers, environ['wsgi.input'], on_field, on_file)
34+
python_multipart.parse_form(headers, environ['wsgi.input'], on_field, on_file)
3535

3636
# Return something.
3737
start_response('200 OK', [('Content-type', 'text/plain')])
@@ -67,7 +67,7 @@ In this section, we’ll build an application that computes the SHA-256 hash of
6767
To start, we need a simple WSGI application. We could do this with a framework like Flask, Django, or Tornado, but for now let’s stick to plain WSGI:
6868

6969
```python
70-
import multipart
70+
import python_multipart
7171

7272
def simple_app(environ, start_response):
7373
start_response('200 OK', [('Content-type', 'text/plain')])
@@ -100,8 +100,8 @@ The final code should look like this:
100100

101101
```python
102102
import hashlib
103-
import multipart
104-
from multipart.multipart import parse_options_header
103+
import python_multipart
104+
from python_multipart.multipart import parse_options_header
105105

106106
def simple_app(environ, start_response):
107107
ret = []
@@ -136,7 +136,7 @@ def simple_app(environ, start_response):
136136
}
137137

138138
# Create the parser.
139-
parser = multipart.MultipartParser(boundary, callbacks)
139+
parser = python_multipart.MultipartParser(boundary, callbacks)
140140

141141
# The input stream is from the WSGI environ.
142142
inp = environ['wsgi.input']
@@ -176,3 +176,11 @@ Content-type: text/plain
176176
Hashes:
177177
Part hash: 0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931
178178
```
179+
180+
181+
## Historical note
182+
183+
This package used to be accessed via `import multipart`. This still works for
184+
now (with a warning) as long as the Python package `multipart` is not also
185+
installed. If both are installed, you need to use the full PyPI name
186+
`python_multipart` for this package.

fuzz/fuzz_decoders.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from helpers import EnhancedDataProvider
66

77
with atheris.instrument_imports():
8-
from multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder
8+
from python_multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder
99

1010

1111
def fuzz_base64_decoder(fdp: EnhancedDataProvider) -> None:

fuzz/fuzz_form.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from helpers import EnhancedDataProvider
77

88
with atheris.instrument_imports():
9-
from multipart.exceptions import FormParserError
10-
from multipart.multipart import parse_form
9+
from python_multipart.exceptions import FormParserError
10+
from python_multipart.multipart import parse_form
1111

1212
on_field = Mock()
1313
on_file = Mock()

fuzz/fuzz_options_header.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from helpers import EnhancedDataProvider
55

66
with atheris.instrument_imports():
7-
from multipart.multipart import parse_options_header
7+
from python_multipart.multipart import parse_options_header
88

99

1010
def TestOneInput(data: bytes) -> None:

noxfile.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import nox
2+
3+
nox.needs_version = ">=2024.4.15"
4+
nox.options.default_venv_backend = "uv|virtualenv"
5+
6+
ALL_PYTHONS = [
7+
c.split()[-1]
8+
for c in nox.project.load_toml("pyproject.toml")["project"]["classifiers"]
9+
if c.startswith("Programming Language :: Python :: 3.")
10+
]
11+
12+
13+
@nox.session(python=ALL_PYTHONS)
14+
def rename(session: nox.Session) -> None:
15+
session.install(".")
16+
assert "import python_multipart" in session.run("python", "-c", "import multipart", silent=True)
17+
assert "import python_multipart" in session.run("python", "-c", "import multipart.exceptions", silent=True)
18+
assert "import python_multipart" in session.run("python", "-c", "from multipart import exceptions", silent=True)
19+
assert "import python_multipart" in session.run(
20+
"python", "-c", "from multipart.exceptions import FormParserError", silent=True
21+
)
22+
23+
session.install("multipart")
24+
assert "import python_multipart" not in session.run(
25+
"python", "-c", "import multipart; multipart.parse_form_data", silent=True
26+
)
27+
assert "import python_multipart" not in session.run(
28+
"python", "-c", "import python_multipart; python_multipart.parse_form", silent=True
29+
)

pyproject.toml

+9-5
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,24 @@ dev-dependencies = [
5555
"mkdocs-autorefs",
5656
]
5757

58+
[tool.uv.pip]
59+
reinstall-package = ["python-multipart"]
60+
5861
[project.urls]
5962
Homepage = "https://github.com/Kludex/python-multipart"
6063
Documentation = "https://kludex.github.io/python-multipart/"
6164
Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md"
6265
Source = "https://github.com/Kludex/python-multipart"
6366

6467
[tool.hatch.version]
65-
path = "multipart/__init__.py"
66-
67-
[tool.hatch.build.targets.wheel]
68-
packages = ["multipart"]
68+
path = "python_multipart/__init__.py"
6969

7070
[tool.hatch.build.targets.sdist]
71-
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"
7276

7377
[tool.mypy]
7478
strict = true
File renamed without changes.

multipart/decoders.py renamed to python_multipart/decoders.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Base64Decoder:
2525
call write() on the underlying object. This is primarily used for decoding
2626
form data encoded as Base64, but can be used for other purposes::
2727
28-
from multipart.decoders import Base64Decoder
28+
from python_multipart.decoders import Base64Decoder
2929
fd = open("notb64.txt", "wb")
3030
decoder = Base64Decoder(fd)
3131
try:
@@ -55,7 +55,7 @@ def write(self, data: bytes) -> int:
5555
"""Takes any input data provided, decodes it as base64, and passes it
5656
on to the underlying object. If the data provided is invalid base64
5757
data, then this method will raise
58-
a :class:`multipart.exceptions.DecodeError`
58+
a :class:`python_multipart.exceptions.DecodeError`
5959
6060
:param data: base64 data to decode
6161
"""
@@ -97,7 +97,7 @@ def close(self) -> None:
9797
def finalize(self) -> None:
9898
"""Finalize this object. This should be called when no more data
9999
should be written to the stream. This function can raise a
100-
:class:`multipart.exceptions.DecodeError` if there is some remaining
100+
:class:`python_multipart.exceptions.DecodeError` if there is some remaining
101101
data in the cache.
102102
103103
If the underlying object has a `finalize()` method, this function will
@@ -118,7 +118,7 @@ def __repr__(self) -> str:
118118
class QuotedPrintableDecoder:
119119
"""This object provides an interface to decode a stream of quoted-printable
120120
data. It is instantiated with an "underlying object", in the same manner
121-
as the :class:`multipart.decoders.Base64Decoder` class. This class behaves
121+
as the :class:`python_multipart.decoders.Base64Decoder` class. This class behaves
122122
in exactly the same way, including maintaining a cache of quoted-printable
123123
chunks.
124124
File renamed without changes.

multipart/multipart.py renamed to python_multipart/multipart.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def from_value(cls, name: bytes, value: bytes | None) -> Field:
241241
value: the value of the form field - either a bytestring or None.
242242
243243
Returns:
244-
A new instance of a [`Field`][multipart.Field].
244+
A new instance of a [`Field`][python_multipart.Field].
245245
"""
246246

247247
f = cls(name)
@@ -351,7 +351,7 @@ class File:
351351
| MAX_MEMORY_FILE_SIZE | `int` | 1 MiB | The maximum number of bytes of a File to keep in memory. By default, the contents of a File are kept into memory until a certain limit is reached, after which the contents of the File are written to a temporary file. This behavior can be disabled by setting this value to an appropriately large value (or, for example, infinity, such as `float('inf')`. |
352352
353353
Args:
354-
file_name: The name of the file that this [`File`][multipart.File] represents.
354+
file_name: The name of the file that this [`File`][python_multipart.File] represents.
355355
field_name: The name of the form field that this file was uploaded with. This can be None, if, for example,
356356
the file was uploaded with Content-Type application/octet-stream.
357357
config: The configuration for this File. See above for valid configuration keys and their corresponding values.
@@ -663,7 +663,7 @@ class OctetStreamParser(BaseParser):
663663
| on_end | None | Called when the parser is finished parsing all data.|
664664
665665
Args:
666-
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser].
666+
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser].
667667
max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded.
668668
"""
669669

@@ -733,12 +733,12 @@ class QuerystringParser(BaseParser):
733733
| on_end | None | Called when the parser is finished parsing all data.|
734734
735735
Args:
736-
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser].
736+
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser].
737737
strict_parsing: Whether or not to parse the body strictly. Defaults to False. If this is set to True, then the
738738
behavior of the parser changes as the following: if a field has a value with an equal sign
739739
(e.g. "foo=bar", or "foo="), it is always included. If a field has no equals sign (e.g. "...&name&..."),
740740
it will be treated as an error if 'strict_parsing' is True, otherwise included. If an error is encountered,
741-
then a [`QuerystringParseError`][multipart.exceptions.QuerystringParseError] will be raised.
741+
then a [`QuerystringParseError`][python_multipart.exceptions.QuerystringParseError] will be raised.
742742
max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded.
743743
""" # noqa: E501
744744

@@ -969,7 +969,7 @@ class MultipartParser(BaseParser):
969969
970970
Args:
971971
boundary: The multipart boundary. This is required, and must match what is given in the HTTP request - usually in the Content-Type header.
972-
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser].
972+
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser].
973973
max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded.
974974
""" # noqa: E501
975975

File renamed without changes.

scripts/check

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
set -x
44

5-
SOURCE_FILES="multipart tests"
5+
SOURCE_FILES="python_multipart tests"
66

77
uvx ruff format --check --diff $SOURCE_FILES
88
uvx ruff check $SOURCE_FILES

tests/test_multipart.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@
1111

1212
import yaml
1313

14-
from multipart.decoders import Base64Decoder, QuotedPrintableDecoder
15-
from multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError, QuerystringParseError
16-
from multipart.multipart import (
14+
from python_multipart.decoders import Base64Decoder, QuotedPrintableDecoder
15+
from python_multipart.exceptions import (
16+
DecodeError,
17+
FileError,
18+
FormParserError,
19+
MultipartParseError,
20+
QuerystringParseError,
21+
)
22+
from python_multipart.multipart import (
1723
BaseParser,
1824
Field,
1925
File,
@@ -31,7 +37,7 @@
3137
if TYPE_CHECKING:
3238
from typing import Any, Iterator, TypedDict
3339

34-
from multipart.multipart import FieldProtocol, FileConfig, FileProtocol
40+
from python_multipart.multipart import FieldProtocol, FileConfig, FileProtocol
3541

3642
class TestParams(TypedDict):
3743
name: str

0 commit comments

Comments
 (0)