Skip to content

Commit

Permalink
refactor: rename to python_multipart
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii committed Oct 11, 2024
1 parent 5303590 commit d6e30eb
Show file tree
Hide file tree
Showing 17 changed files with 114 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Run tests
run: scripts/test

- name: Run rename test
run: uvx nox -s rename -P ${{ matrix.python-version }}

# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
check:
if: always()
Expand Down
1 change: 1 addition & 0 deletions _python_multipart.pth
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import _python_multipart_loader
37 changes: 37 additions & 0 deletions _python_multipart_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

# The purpose of this file is to allow `import multipart` to continue to work
# unless `multipart` (the PyPI package) is also installed, in which case
# a collision is avoided, and `import multipart` is no longer injected.
import importlib
import importlib.abc
import importlib.machinery
import importlib.util
import sys
import warnings


class PythonMultipartCompatFinder(importlib.abc.MetaPathFinder):
def find_spec(
self, fullname: str, path: object = None, target: object = None
) -> importlib.machinery.ModuleSpec | None:
if fullname != "multipart":
return None
old_sys_meta_path = sys.meta_path
try:
sys.meta_path = [p for p in sys.meta_path if not isinstance(p, type(self))]
if multipart := importlib.util.find_spec("multipart"):
return multipart

warnings.warn("Please use `import python_multipart` instead", FutureWarning, stacklevel=2)
sys.modules["multipart"] = importlib.import_module("python_multipart")
return importlib.util.find_spec("python_multipart")
finally:
sys.meta_path = old_sys_meta_path


def install() -> None:
sys.meta_path.insert(0, PythonMultipartCompatFinder())


install()
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
::: multipart
::: python_multipart

::: multipart.exceptions
::: python_multipart.exceptions
20 changes: 14 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Python-Multipart is a streaming multipart parser for Python.
The following example shows a quick example of parsing an incoming request body in a simple WSGI application:

```python
import multipart
import python_multipart

def simple_app(environ, start_response):
ret = []
Expand All @@ -31,7 +31,7 @@ def simple_app(environ, start_response):
headers['Content-Length'] = environ['CONTENT_LENGTH']

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

# Return something.
start_response('200 OK', [('Content-type', 'text/plain')])
Expand Down Expand Up @@ -67,7 +67,7 @@ In this section, we’ll build an application that computes the SHA-256 hash of
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:

```python
import multipart
import python_multipart

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

```python
import hashlib
import multipart
from multipart.multipart import parse_options_header
import python_multipart
from python_multipart.multipart import parse_options_header

def simple_app(environ, start_response):
ret = []
Expand Down Expand Up @@ -136,7 +136,7 @@ def simple_app(environ, start_response):
}

# Create the parser.
parser = multipart.MultipartParser(boundary, callbacks)
parser = python_multipart.MultipartParser(boundary, callbacks)

# The input stream is from the WSGI environ.
inp = environ['wsgi.input']
Expand Down Expand Up @@ -176,3 +176,11 @@ Content-type: text/plain
Hashes:
Part hash: 0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931
```


## Historical note

This package used to be accessed via `import multipart`. This still works for
now (with a warning) as long as the Python package `multipart` is not also
installed. If both are installed, you need to use the full PyPI name
`python_multipart` for this package.
2 changes: 1 addition & 1 deletion fuzz/fuzz_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from helpers import EnhancedDataProvider

with atheris.instrument_imports():
from multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder
from python_multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder


def fuzz_base64_decoder(fdp: EnhancedDataProvider) -> None:
Expand Down
4 changes: 2 additions & 2 deletions fuzz/fuzz_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from helpers import EnhancedDataProvider

with atheris.instrument_imports():
from multipart.exceptions import FormParserError
from multipart.multipart import parse_form
from python_multipart.exceptions import FormParserError
from python_multipart.multipart import parse_form

on_field = Mock()
on_file = Mock()
Expand Down
2 changes: 1 addition & 1 deletion fuzz/fuzz_options_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from helpers import EnhancedDataProvider

with atheris.instrument_imports():
from multipart.multipart import parse_options_header
from python_multipart.multipart import parse_options_header


def TestOneInput(data: bytes) -> None:
Expand Down
29 changes: 29 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import nox

nox.needs_version = ">=2024.4.15"
nox.options.default_venv_backend = "uv|virtualenv"

ALL_PYTHONS = [
c.split()[-1]
for c in nox.project.load_toml("pyproject.toml")["project"]["classifiers"]
if c.startswith("Programming Language :: Python :: 3.")
]


@nox.session(python=ALL_PYTHONS)
def rename(session: nox.Session) -> None:
session.install(".")
assert "import python_multipart" in session.run("python", "-c", "import multipart", silent=True)
assert "import python_multipart" in session.run("python", "-c", "import multipart.exceptions", silent=True)
assert "import python_multipart" in session.run("python", "-c", "from multipart import exceptions", silent=True)
assert "import python_multipart" in session.run(
"python", "-c", "from multipart.exceptions import FormParserError", silent=True
)

session.install("multipart")
assert "import python_multipart" not in session.run(
"python", "-c", "import multipart; multipart.parse_form_data", silent=True
)
assert "import python_multipart" not in session.run(
"python", "-c", "import python_multipart; python_multipart.parse_form", silent=True
)
14 changes: 9 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,24 @@ dev-dependencies = [
"mkdocs-autorefs",
]

[tool.uv.pip]
reinstall-package = ["python-multipart"]

[project.urls]
Homepage = "https://github.com/Kludex/python-multipart"
Documentation = "https://kludex.github.io/python-multipart/"
Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md"
Source = "https://github.com/Kludex/python-multipart"

[tool.hatch.version]
path = "multipart/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["multipart"]
path = "python_multipart/__init__.py"

[tool.hatch.build.targets.sdist]
include = ["/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"]
include = ["/python_multipart", "/tests", "CHANGELOG.md", "LICENSE.txt", "_python_multipart.pth", "_python_multipart_loader.py"]

[tool.hatch.build.targets.wheel.force-include]
"_python_multipart.pth" = "_python_multipart.pth"
"_python_multipart_loader.py" = "_python_multipart_loader.py"

[tool.mypy]
strict = true
Expand Down
File renamed without changes.
8 changes: 4 additions & 4 deletions multipart/decoders.py → python_multipart/decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Base64Decoder:
call write() on the underlying object. This is primarily used for decoding
form data encoded as Base64, but can be used for other purposes::
from multipart.decoders import Base64Decoder
from python_multipart.decoders import Base64Decoder
fd = open("notb64.txt", "wb")
decoder = Base64Decoder(fd)
try:
Expand Down Expand Up @@ -55,7 +55,7 @@ def write(self, data: bytes) -> int:
"""Takes any input data provided, decodes it as base64, and passes it
on to the underlying object. If the data provided is invalid base64
data, then this method will raise
a :class:`multipart.exceptions.DecodeError`
a :class:`python_multipart.exceptions.DecodeError`
:param data: base64 data to decode
"""
Expand Down Expand Up @@ -97,7 +97,7 @@ def close(self) -> None:
def finalize(self) -> None:
"""Finalize this object. This should be called when no more data
should be written to the stream. This function can raise a
:class:`multipart.exceptions.DecodeError` if there is some remaining
:class:`python_multipart.exceptions.DecodeError` if there is some remaining
data in the cache.
If the underlying object has a `finalize()` method, this function will
Expand All @@ -118,7 +118,7 @@ def __repr__(self) -> str:
class QuotedPrintableDecoder:
"""This object provides an interface to decode a stream of quoted-printable
data. It is instantiated with an "underlying object", in the same manner
as the :class:`multipart.decoders.Base64Decoder` class. This class behaves
as the :class:`python_multipart.decoders.Base64Decoder` class. This class behaves
in exactly the same way, including maintaining a cache of quoted-printable
chunks.
Expand Down
File renamed without changes.
12 changes: 6 additions & 6 deletions multipart/multipart.py → python_multipart/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def from_value(cls, name: bytes, value: bytes | None) -> Field:
value: the value of the form field - either a bytestring or None.
Returns:
A new instance of a [`Field`][multipart.Field].
A new instance of a [`Field`][python_multipart.Field].
"""

f = cls(name)
Expand Down Expand Up @@ -351,7 +351,7 @@ class File:
| 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')`. |
Args:
file_name: The name of the file that this [`File`][multipart.File] represents.
file_name: The name of the file that this [`File`][python_multipart.File] represents.
field_name: The name of the form field that this file was uploaded with. This can be None, if, for example,
the file was uploaded with Content-Type application/octet-stream.
config: The configuration for this File. See above for valid configuration keys and their corresponding values.
Expand Down Expand Up @@ -663,7 +663,7 @@ class OctetStreamParser(BaseParser):
| on_end | None | Called when the parser is finished parsing all data.|
Args:
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser].
callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser].
max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded.
"""

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

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

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion scripts/check
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

set -x

SOURCE_FILES="multipart tests"
SOURCE_FILES="python_multipart tests"

uvx ruff format --check --diff $SOURCE_FILES
uvx ruff check $SOURCE_FILES
Expand Down
8 changes: 4 additions & 4 deletions tests/test_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

import yaml

from multipart.decoders import Base64Decoder, QuotedPrintableDecoder
from multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError, QuerystringParseError
from multipart.multipart import (
from python_multipart.decoders import Base64Decoder, QuotedPrintableDecoder
from python_multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError, QuerystringParseError
from python_multipart.multipart import (
BaseParser,
Field,
File,
Expand All @@ -31,7 +31,7 @@
if TYPE_CHECKING:
from typing import Any, Iterator, TypedDict

from multipart.multipart import FieldProtocol, FileConfig, FileProtocol
from python_multipart.multipart import FieldProtocol, FileConfig, FileProtocol

class TestParams(TypedDict):
name: str
Expand Down

0 comments on commit d6e30eb

Please sign in to comment.