Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: rename import to python_multipart #166

Merged
merged 3 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Kludex marked this conversation as resolved.
Show resolved Hide resolved
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
14 changes: 10 additions & 4 deletions tests/test_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@

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 +37,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