Skip to content

Commit aa7048c

Browse files
authored
Speed up querystring callback dispatch (#316)
1 parent a034d65 commit aa7048c

3 files changed

Lines changed: 76 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
* Speed up querystring callback dispatch [#316](https://github.com/Kludex/python-multipart/pull/316).
6+
57
## 0.0.32 (2026-06-04)
68

79
* Speed up partial-boundary scanning for CR/LF-dense part data [#300](https://github.com/Kludex/python-multipart/pull/300).

‎python_multipart/multipart.py‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class FormParserConfig(FileConfig):
7878
_missing = object()
7979

8080

81+
def _noop_event() -> None:
82+
pass
83+
84+
85+
def _noop_data(_data: bytes, _start: int, _end: int) -> None:
86+
pass
87+
88+
8189
class QuerystringState(IntEnum):
8290
"""Querystring parser states.
8391
@@ -836,6 +844,19 @@ def _internal_write(self, data: bytes, length: int) -> int:
836844
state = self.state
837845
strict_parsing = self.strict_parsing
838846
found_sep = self._found_sep
847+
callbacks = cast("QuerystringCallbacks", self.callbacks)
848+
on_field_start = callbacks.get("on_field_start")
849+
on_field_name = callbacks.get("on_field_name")
850+
on_field_data = callbacks.get("on_field_data")
851+
on_field_end = callbacks.get("on_field_end")
852+
if on_field_start is None:
853+
on_field_start = _noop_event
854+
if on_field_name is None:
855+
on_field_name = _noop_data
856+
if on_field_data is None:
857+
on_field_data = _noop_data
858+
if on_field_end is None:
859+
on_field_end = _noop_event
839860

840861
i = 0
841862
while i < length:
@@ -865,7 +886,7 @@ def _internal_write(self, data: bytes, length: int) -> int:
865886
# Emit a field-start event, and go to that state. Also,
866887
# reset the "found_sep" flag, for the next time we get to
867888
# this state.
868-
self.callback("field_start")
889+
on_field_start()
869890
i -= 1
870891
state = QuerystringState.FIELD_NAME
871892
found_sep = False
@@ -885,7 +906,8 @@ def _internal_write(self, data: bytes, length: int) -> int:
885906

886907
if equals_pos != -1:
887908
# Emit this name.
888-
self.callback("field_name", data, i, equals_pos)
909+
if i != equals_pos:
910+
on_field_name(data, i, equals_pos)
889911

890912
# Jump i to this position. Note that it will then have 1
891913
# added to it below, which means the next iteration of this
@@ -900,15 +922,17 @@ def _internal_write(self, data: bytes, length: int) -> int:
900922
# end - there's no data callback at all (not even with
901923
# a blank value).
902924
if sep_pos != -1:
903-
self.callback("field_name", data, i, sep_pos)
904-
self.callback("field_end")
925+
if i != sep_pos:
926+
on_field_name(data, i, sep_pos)
927+
on_field_end()
905928

906929
i = sep_pos - 1
907930
state = QuerystringState.BEFORE_FIELD
908931
else:
909932
# Otherwise, no separator in this block, so the
910933
# rest of this chunk must be a name.
911-
self.callback("field_name", data, i, length)
934+
if i != length:
935+
on_field_name(data, i, length)
912936
i = length
913937

914938
else:
@@ -924,7 +948,8 @@ def _internal_write(self, data: bytes, length: int) -> int:
924948

925949
# No separator in the rest of this chunk, so it's just
926950
# a field name.
927-
self.callback("field_name", data, i, length)
951+
if i != length:
952+
on_field_name(data, i, length)
928953
i = length
929954

930955
elif state == QuerystringState.FIELD_DATA:
@@ -934,8 +959,9 @@ def _internal_write(self, data: bytes, length: int) -> int:
934959
# If we found it, callback this bit as data and then go back
935960
# to expecting to find a field.
936961
if sep_pos != -1:
937-
self.callback("field_data", data, i, sep_pos)
938-
self.callback("field_end")
962+
if i != sep_pos:
963+
on_field_data(data, i, sep_pos)
964+
on_field_end()
939965

940966
# Note that we go to the separator, which brings us to the
941967
# "before field" state. This allows us to properly emit
@@ -946,7 +972,8 @@ def _internal_write(self, data: bytes, length: int) -> int:
946972

947973
# Otherwise, emit the rest as data and finish.
948974
else:
949-
self.callback("field_data", data, i, length)
975+
if i != length:
976+
on_field_data(data, i, length)
950977
i = length
951978

952979
else: # pragma: no cover (error case)
@@ -965,10 +992,17 @@ def finalize(self) -> None:
965992
if we're still in the middle of a field, an on_field_end callback, and
966993
then the on_end callback.
967994
"""
995+
callbacks = cast("QuerystringCallbacks", self.callbacks)
968996
# If we're currently in the middle of a field, we finish it.
969997
if self.state in (QuerystringState.FIELD_DATA, QuerystringState.FIELD_NAME):
970-
self.callback("field_end")
971-
self.callback("end")
998+
on_field_end = callbacks.get("on_field_end")
999+
if on_field_end is None:
1000+
on_field_end = _noop_event
1001+
on_field_end()
1002+
on_end = callbacks.get("on_end")
1003+
if on_end is None:
1004+
on_end = _noop_event
1005+
on_end()
9721006

9731007
def __repr__(self) -> str:
9741008
return "{}(strict_parsing={!r}, max_size={!r})".format(

‎tests/test_multipart.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ def on_foo() -> None:
365365
nonlocal called
366366
called += 1
367367

368+
def on_data(data: bytes, start: int, end: int) -> None:
369+
nonlocal called
370+
called += 1
371+
368372
self.b.set_callback("foo", on_foo) # type: ignore[arg-type]
369373
self.b.callback("foo") # type: ignore[arg-type]
370374
self.assertEqual(called, 1)
@@ -373,6 +377,12 @@ def on_foo() -> None:
373377
self.b.callback("foo") # type: ignore[arg-type]
374378
self.assertEqual(called, 1)
375379

380+
self.b.set_callback("data", on_data)
381+
self.b.callback("data", b"", 0, 0)
382+
self.assertEqual(called, 1)
383+
self.b.callback("data", b"x", 0, 1)
384+
self.assertEqual(called, 2)
385+
376386

377387
class TestQuerystringParser(unittest.TestCase):
378388
def assert_fields(self, *args: tuple[bytes, bytes], **kwargs: Any) -> None:
@@ -413,6 +423,25 @@ def test_simple_querystring(self) -> None:
413423

414424
self.assert_fields((b"foo", b"bar"))
415425

426+
def test_no_callbacks(self) -> None:
427+
parser = QuerystringParser()
428+
429+
self.assertEqual(parser.write(b"foo=bar"), 7)
430+
parser.finalize()
431+
432+
def test_none_callbacks(self) -> None:
433+
callbacks: Any = {
434+
"on_field_start": None,
435+
"on_field_name": None,
436+
"on_field_data": None,
437+
"on_field_end": None,
438+
"on_end": None,
439+
}
440+
parser = QuerystringParser(callbacks)
441+
442+
self.assertEqual(parser.write(b"foo=bar"), 7)
443+
parser.finalize()
444+
416445
def test_querystring_blank_beginning(self) -> None:
417446
self.p.write(b"&foo=bar")
418447

0 commit comments

Comments
 (0)