Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6

Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dynamic = ["version"]
description = "A streaming multipart parser for Python"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.8"
requires-python = ">=3.10"
authors = [
{ name = "Andrew Dunham", email = "andrew@du.nham.ca" },
{ name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
Expand All @@ -21,12 +21,11 @@ classifiers = [
'Operating System :: OS Independent',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Topic :: Software Development :: Libraries :: Python Modules',
]
dependencies = []
Expand Down
17 changes: 8 additions & 9 deletions python_multipart/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

if TYPE_CHECKING: # pragma: no cover
from typing import Any, Callable, Literal, Protocol, TypedDict

from typing_extensions import TypeAlias
from collections.abc import Callable
from typing import Any, Literal, Protocol, TypeAlias, TypedDict

class SupportsRead(Protocol):
def read(self, __n: int) -> bytes: ...
Expand Down Expand Up @@ -332,7 +331,7 @@ def __repr__(self) -> str:
else:
v = repr(self.value)

return "{}(field_name={!r}, value={})".format(self.__class__.__name__, self.field_name, v)
return f"{self.__class__.__name__}(field_name={self.field_name!r}, value={v})"


class File:
Expand Down Expand Up @@ -570,7 +569,7 @@ def close(self) -> None:
self._fileobj.close()

def __repr__(self) -> str:
return "{}(file_name={!r}, field_name={!r})".format(self.__class__.__name__, self.file_name, self.field_name)
return f"{self.__class__.__name__}(file_name={self.file_name!r}, field_name={self.field_name!r})"


class BaseParser:
Expand Down Expand Up @@ -1241,7 +1240,7 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No
elif state == MultipartState.HEADER_VALUE_ALMOST_DONE:
# The last character should be a LF. If not, it's an error.
if c != LF:
msg = "Did not find LF character at end of header (found %r)" % (c,)
msg = f"Did not find LF character at end of header (found {c!r})"
self.logger.warning(msg)
e = MultipartParseError(msg)
e.offset = i
Expand Down Expand Up @@ -1715,7 +1714,7 @@ def on_headers_finished() -> None:
else:
self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding)
if self.config["UPLOAD_ERROR_ON_BAD_CTE"]:
raise FormParserError('Unknown Content-Transfer-Encoding "{!r}"'.format(transfer_encoding))
raise FormParserError(f'Unknown Content-Transfer-Encoding "{transfer_encoding!r}"')
else:
# If we aren't erroring, then we just treat this as an
# unencoded Content-Transfer-Encoding.
Expand Down Expand Up @@ -1746,7 +1745,7 @@ def _on_end() -> None:

else:
self.logger.warning("Unknown Content-Type: %r", content_type)
raise FormParserError("Unknown Content-Type: {}".format(content_type))
raise FormParserError(f"Unknown Content-Type: {content_type}")

self.parser = parser

Expand Down Expand Up @@ -1776,7 +1775,7 @@ def close(self) -> None:
self.parser.close()

def __repr__(self) -> str:
return "{}(content_type={!r}, parser={!r})".format(self.__class__.__name__, self.content_type, self.parser)
return f"{self.__class__.__name__}(content_type={self.content_type!r}, parser={self.parser!r})"


def create_form_parser(
Expand Down
3 changes: 2 additions & 1 deletion tests/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Callable
from collections.abc import Callable
from typing import Any


def ensure_in_path(path: str) -> None:
Expand Down
7 changes: 5 additions & 2 deletions tests/test_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
from .compat import parametrize, parametrize_class

if TYPE_CHECKING:
from typing import Any, Iterator, TypedDict
from collections.abc import Iterator
from typing import Any, TypedDict

from python_multipart.multipart import FieldProtocol, FileConfig, FileProtocol

Expand Down Expand Up @@ -1069,7 +1070,9 @@ def test_bad_start_boundary(self) -> None:

self.make("boundary")
data = b"--Boundary\r\nfoobar"
with self.assertRaisesRegex(MultipartParseError, "Expected boundary character %r, got %r" % (b"b"[0], b"B"[0])):
with self.assertRaisesRegex(
MultipartParseError, "Expected boundary character {!r}, got {!r}".format(b"b"[0], b"B"[0])
):
self.f.write(data)

def test_octet_stream(self) -> None:
Expand Down
Loading