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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Remove custom FormParser classes by Kludex · Pull Request #257 · Kludex/python-multipart · GitHub
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
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,17 +111,9 @@ omit = ["tests/*"]
fail_under = 100
skip_covered = true
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"def __str__",
exclude_also = [
"def __repr__",
"if 0:",
"if False:",
"if __name__ == .__main__.:",
"if self\\.config\\['DEBUG'\\]:",
"if self\\.debug:",
"except ImportError:",
]

[tool.check-sdist]
Expand Down
75 changes: 21 additions & 54 deletions python_multipart/multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
from .decoders import Base64Decoder, QuotedPrintableDecoder
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

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

Expand DownExpand Up@@ -55,24 +55,6 @@ class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float

class _FormProtocol(Protocol):
def write(self, data: bytes) -> int: ...

def finalize(self) -> None: ...

def close(self) -> None: ...

class FieldProtocol(_FormProtocol, Protocol):
def __init__(self, name: bytes | None) -> None: ...

def set_none(self) -> None: ...

class FileProtocol(_FormProtocol, Protocol):
def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ...

OnFieldCallback = Callable[[FieldProtocol], None]
OnFileCallback = Callable[[FileProtocol], None]

CallbackName: TypeAlias = Literal[
"start",
"data",
Expand DownExpand Up@@ -1483,19 +1465,6 @@ class FormParser:
file_name: If the request is of type application/octet-stream, then the body of the request will not contain any
information about the uploaded file. In such cases, you can provide the file name of the uploaded file
manually.
FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class
if you wish to customize behaviour. The class will be instantiated as
FileClass(file_name, field_name, config=config), and it must provide the following functions::
- file_instance.write(data)
- file_instance.finalize()
- file_instance.close()
FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own
class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it
must provide the following functions::
- field_instance.write(data)
- field_instance.finalize()
- field_instance.close()
- field_instance.set_none()
config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value,
and then any keys present in this dictionary will overwrite the default values.
"""
Expand All@@ -1516,13 +1485,11 @@ class if you wish to customize behaviour. The class will be instantiated as Fie
def __init__(
self,
content_type: str,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
on_end: Callable[[], None] | None = None,
boundary: bytes | str | None = None,
file_name: bytes | None = None,
FileClass: type[FileProtocol] = File,
FieldClass: type[FieldProtocol] = Field,
config: dict[Any, Any] = {},
) -> None:
self.logger = logging.getLogger(__name__)
Expand All@@ -1538,10 +1505,6 @@ def __init__(
self.on_file = on_file
self.on_end = on_end

# Save classes.
self.FileClass = File
self.FieldClass = Field
Comment on lines -1541 to -1543

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole thing was non-used for 13 years. 👀


# Set configuration options.
self.config: FormParserConfig = self.DEFAULT_CONFIG.copy()
self.config.update(config) # type: ignore[typeddict-item]
Expand All@@ -1550,18 +1513,20 @@ def __init__(

# Depending on the Content-Type, we instantiate the correct parser.
if content_type == "application/octet-stream":
file: FileProtocol = None # type: ignore
file: File | None = None

def on_start() -> None:
nonlocal file
file = FileClass(file_name, None, config=self.config)
file = File(file_name, None, config=self.config)

def on_data(data: bytes, start: int, end: int) -> None:
nonlocal file
assert file is not None
file.write(data[start:end])

def _on_end() -> None:
nonlocal file
assert file is not None
# Finalize the file itself.
file.finalize()

Expand All@@ -1582,7 +1547,7 @@ def _on_end() -> None:
elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded":
name_buffer: list[bytes] = []

f: FieldProtocol | None = None
f: Field | None = None

def on_field_start() -> None:
pass
Expand All@@ -1593,7 +1558,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None:
def on_field_data(data: bytes, start: int, end: int) -> None:
nonlocal f
if f is None:
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.write(data[start:end])

Expand All@@ -1603,7 +1568,7 @@ def on_field_end() -> None:
if f is None:
# If we get here, it's because there was no field data.
# We create a field, set it to None, and then continue.
f = FieldClass(b"".join(name_buffer))
f = Field(b"".join(name_buffer))
del name_buffer[:]
f.set_none()

Expand DownExpand Up@@ -1637,8 +1602,8 @@ def _on_end() -> None:
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
f_multi: File | Field | None = None
writer: File | Field | Base64Decoder | QuotedPrintableDecoder | None = None
is_file = False

def on_part_begin() -> None:
Expand All@@ -1658,10 +1623,12 @@ def on_part_end() -> None:
f_multi.finalize()
if is_file:
if on_file:
assert isinstance(f_multi, File)
on_file(f_multi)
else:
if on_field:
on_field(cast("FieldProtocol", f_multi))
assert isinstance(f_multi, Field)
on_field(f_multi)

def on_header_field(data: bytes, start: int, end: int) -> None:
header_name.append(data[start:end])
Expand DownExpand Up@@ -1690,9 +1657,9 @@ def on_headers_finished() -> None:

# Create the proper class.
if file_name is None:
f_multi = FieldClass(field_name)
f_multi = Field(field_name)
else:
f_multi = FileClass(file_name, field_name, config=self.config)
f_multi = File(file_name, field_name, config=self.config)
is_file = True

# Parse the given Content-Transfer-Encoding to determine what
Expand DownExpand Up@@ -1778,8 +1745,8 @@ def __repr__(self) -> str:

def create_form_parser(
headers: dict[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
config: dict[Any, Any] = {},
) -> FormParser:
"""This function is a helper function to aid in creating a FormParser
Expand DownExpand Up@@ -1817,8 +1784,8 @@ def create_form_parser(
def parse_form(
headers: dict[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
on_field: Callable[[Field], None] | None,
on_file: Callable[[File], None] | None,
chunk_size: int = 1048576,
) -> None:
"""This function is useful if you just want to parse a request body,
Expand Down
52 changes: 26 additions & 26 deletions tests/test_multipart.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@
import tempfile
import unittest
from io import BytesIO
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand DownExpand Up@@ -40,7 +40,7 @@
from collections.abc import Iterator
from typing import Any, TypedDict

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

class TestParams(TypedDict):
name: str
Expand DownExpand Up@@ -753,11 +753,11 @@ def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None:
self.files: list[File] = []
self.fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
self.fields.append(cast(Field, f))
def on_field(f: Field) -> None:
self.fields.append(f)

def on_file(f: FileProtocol) -> None:
self.files.append(cast(File, f))
def on_file(f: File) -> None:
self.files.append(f)

def on_end() -> None:
self.ended = True
Expand DownExpand Up@@ -1115,8 +1115,8 @@ def test_bad_start_boundary(self) -> None:
def test_octet_stream(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1137,8 +1137,8 @@ def on_file(f: FileProtocol) -> None:
def test_querystring(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1208,8 +1208,8 @@ def test_bad_content_transfer_encoding(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand All@@ -1233,8 +1233,8 @@ def on_file(f: FileProtocol) -> None:
def test_handles_None_fields(self) -> None:
fields: list[Field] = []

def on_field(f: FieldProtocol) -> None:
fields.append(cast(Field, f))
def on_field(f: Field) -> None:
fields.append(f)

on_file = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1265,8 +1265,8 @@ def test_multipart_parser_newlines_before_first_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1284,8 +1284,8 @@ def test_multipart_parser_data_after_last_boundary(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
f.write(data.encode("latin-1"))
Expand All@@ -1306,8 +1306,8 @@ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:

files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
with self._caplog.at_level(logging.WARNING):
Expand DownExpand Up@@ -1354,8 +1354,8 @@ def test_max_size_form_parser(self) -> None:
def test_octet_stream_max_size(self) -> None:
files: list[File] = []

def on_file(f: FileProtocol) -> None:
files.append(cast(File, f))
def on_file(f: File) -> None:
files.append(f)

on_field = Mock()
on_end = Mock()
Expand DownExpand Up@@ -1432,12 +1432,12 @@ def test_parse_form(self) -> None:
self.assertEqual(on_file.call_args[0][0].size, 15)

def test_parse_form_content_length(self) -> None:
files: list[FileProtocol] = []
files: list[File] = []

def on_field(field: FieldProtocol) -> None:
def on_field(field: Field) -> None:
pass

def on_file(file: FileProtocol) -> None:
def on_file(file: File) -> None:
files.append(file)

parse_form(
Expand All@@ -1448,7 +1448,7 @@ def on_file(file: FileProtocol) -> None:
)

self.assertEqual(len(files), 1)
self.assertEqual(files[0].size, 10) # type: ignore[attr-defined]
self.assertEqual(files[0].size, 10)

def test_parse_form_invalid_chunk_size(self) -> None:
with self.assertRaisesRegex(ValueError, "chunk_size must be a positive number, not 0"):
Expand Down
Loading