Skip to content

Commit 9ce7f89

Browse files
committed
fix(httpv2): accept a plain dict of SETTINGS (#2223)
HTTP._make_http_settings took a dict branch but called settings.items(multi=True), so a plain dict raised a stdlib TypeError. Call items(multi=True) only on a MultiDict, and plain items() otherwise. Widen the settings annotation and Args to name dict. Test: a plain dict builds the same octets as a list of pairs, keeps insertion order and parses back; an OrderedMultiDict keeps repeated ids.
1 parent 91147a1 commit 9ce7f89

2 files changed

Lines changed: 80 additions & 4 deletions

File tree

‎pcapkit/protocols/application/httpv2.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from pcapkit.const.http.error_code import ErrorCode as Enum_ErrorCode
4242
from pcapkit.const.http.frame import Frame as Enum_Frame
4343
from pcapkit.const.http.setting import Setting as Enum_Setting
44-
from pcapkit.corekit.multidict import OrderedMultiDict
44+
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict
4545
from pcapkit.protocols.application.http import HTTP as HTTPBase
4646
from pcapkit.protocols.data.application.httpv2 import HTTP as Data_HTTP
4747
from pcapkit.protocols.data.application.httpv2 import ContinuationFrame as Data_ContinuationFrame
@@ -1282,15 +1282,16 @@ def _make_http_rst_stream(self, frame: 'Optional[Data_RSTStreamFrame]' = None, *
12821282

12831283
def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *,
12841284
ack: 'bool' = False,
1285-
settings: 'Optional[OrderedMultiDict[Enum_Setting, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting, int]]]' = None, # pylint: disable=line-too-long
1285+
settings: 'Optional[OrderedMultiDict[Enum_Setting, int] | dict[Enum_Setting | int, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting, int]]]' = None, # pylint: disable=line-too-long
12861286
**kwargs: 'Any') -> 'tuple[Schema_SettingsFrame, Flags]':
12871287
"""Make HTTP/2 ``SETTINGS`` frame.
12881288
12891289
Args:
12901290
frame: Frame data model.
12911291
ack: Acknowledge flag.
12921292
settings: Settings, as raw octets, an
1293-
:class:`~pcapkit.corekit.multidict.OrderedMultiDict` or a list of
1293+
:class:`~pcapkit.corekit.multidict.OrderedMultiDict`, a plain
1294+
:obj:`dict` of identifier to value or a list of
12941295
``(identifier, value)`` pairs.
12951296
**kwargs: Arbitrary keyword arguments.
12961297
@@ -1314,7 +1315,9 @@ def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *,
13141315
settings_val = settings # type: bytes | list[Schema_SettingPair]
13151316
elif isinstance(settings, dict):
13161317
settings_val = []
1317-
for key, val in settings.items(multi=True):
1318+
# A plain ``dict`` has no ``multi`` keyword (#2223).
1319+
items = settings.items(multi=True) if isinstance(settings, MultiDict) else settings.items()
1320+
for key, val in items:
13181321
settings_val.append(Schema_SettingPair(
13191322
id=key,
13201323
value=val,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
"""HTTP/2 ``SETTINGS`` frames build from a plain :obj:`dict`.
3+
4+
GitHub issue #2223: ``HTTP._make_http_settings`` accepted a :obj:`dict` but
5+
called ``settings.items(multi=True)``, which only
6+
:class:`~pcapkit.corekit.multidict.MultiDict` supports, so a plain :obj:`dict`
7+
raised a stdlib :exc:`TypeError` instead of building the frame. A plain
8+
:obj:`dict` builds the same octets as the equivalent list of pairs, and an
9+
:class:`~pcapkit.corekit.multidict.OrderedMultiDict` keeps its repeated
10+
identifiers.
11+
12+
Every case builds its own octets in memory and reads no capture. Classes are
13+
imported inside each test, after
14+
:func:`~tests._support.reimport_once_per_class`.
15+
16+
"""
17+
18+
import unittest
19+
20+
from tests._support import reimport_once_per_class
21+
22+
#: ``HEADER_TABLE_SIZE = 2`` as a ``SETTINGS`` frame on stream 0.
23+
ONE_PAIR = bytes.fromhex('000006040000000000000100000002')
24+
25+
26+
class TestHTTPv2SettingsPlainDict(unittest.TestCase):
27+
"""Pin ``SETTINGS`` construction from each accepted mapping type."""
28+
29+
def setUp(self) -> None:
30+
reimport_once_per_class(self)
31+
32+
def build(self, settings: 'object') -> 'bytes':
33+
"""Build a ``SETTINGS`` frame from ``settings``."""
34+
from pcapkit.const.http.frame import Frame
35+
from pcapkit.protocols.application.httpv2 import HTTP
36+
37+
return bytes(HTTP(type=Frame.SETTINGS, frame={'settings': settings}))
38+
39+
def test_plain_dict_builds_the_list_octets(self) -> None:
40+
from pcapkit.const.http.setting import Setting
41+
42+
self.assertEqual(self.build([(Setting.HEADER_TABLE_SIZE, 2)]), ONE_PAIR)
43+
for name, settings in (('int key', {1: 2}),
44+
('enum key', {Setting.HEADER_TABLE_SIZE: 2})):
45+
with self.subTest(settings=name):
46+
self.assertEqual(self.build(settings), ONE_PAIR)
47+
48+
def test_plain_dict_keeps_insertion_order(self) -> None:
49+
settings = {3: 100, 1: 4096, 4: 65535}
50+
self.assertEqual(self.build(settings), self.build(list(settings.items())))
51+
52+
def test_plain_dict_frame_parses_back(self) -> None:
53+
import io
54+
55+
from pcapkit.protocols.application.httpv2 import HTTP
56+
57+
raw = self.build({1: 4096, 3: 100})
58+
info = HTTP(io.BytesIO(raw), len(raw)).info
59+
self.assertEqual([(int(k), v) for k, v in info.settings.items(multi=True)],
60+
[(1, 4096), (3, 100)])
61+
62+
def test_ordered_multidict_keeps_repeated_identifiers(self) -> None:
63+
from pcapkit.corekit.multidict import OrderedMultiDict
64+
65+
pairs = [(1, 4096), (1, 2)]
66+
self.assertEqual(self.build(OrderedMultiDict(pairs)), self.build(pairs))
67+
68+
def test_empty_plain_dict_builds_an_empty_frame(self) -> None:
69+
self.assertEqual(self.build({}), bytes.fromhex('000000040000000000'))
70+
71+
72+
if __name__ == '__main__':
73+
unittest.main()

0 commit comments

Comments
 (0)