diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4544ef..1a02de2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,9 @@ dev **Bugfixes** +- Preserve header fields when copying or pickling ``HeaderTuple`` and + ``NeverIndexedHeaderTuple`` objects. + 4.2.0 (2026-06-22) ------------------ diff --git a/src/hpack/struct.py b/src/hpack/struct.py index 9a4cfb1..a816974 100644 --- a/src/hpack/struct.py +++ b/src/hpack/struct.py @@ -31,6 +31,9 @@ class HeaderTuple(tuple[bytes, bytes]): def __new__(cls, *args: Any) -> Self: return tuple.__new__(cls, args) + def __getnewargs__(self) -> tuple[bytes, ...]: + return tuple(self) + class NeverIndexedHeaderTuple(HeaderTuple): """ diff --git a/tests/test_struct.py b/tests/test_struct.py index 9beec35..c978779 100644 --- a/tests/test_struct.py +++ b/tests/test_struct.py @@ -1,6 +1,9 @@ +import copy +import pickle + import pytest -from hpack import HeaderTuple, NeverIndexedHeaderTuple +from hpack import Decoder, Encoder, HeaderTuple, NeverIndexedHeaderTuple class TestHeaderTuple: @@ -68,3 +71,42 @@ def test_equal_for_different_indexes(self): assert t1 == t2 assert t1 is not t2 + + @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple)) + @pytest.mark.parametrize('copier', (copy.copy, copy.deepcopy)) + def test_copy_preserves_header(self, cls, copier): + """ + Copying a header preserves its fields and concrete type. + """ + header = cls(b'name', b'\xff') + + restored = copier(header) + + assert restored == header + assert type(restored) is cls + assert restored.indexable == header.indexable + + @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple)) + @pytest.mark.parametrize('protocol', range(pickle.HIGHEST_PROTOCOL + 1)) + def test_pickle_preserves_header(self, cls, protocol): + """ + Every pickle protocol preserves the fields and concrete type. + """ + header = cls(b'name', b'\xff') + + restored = pickle.loads(pickle.dumps(header, protocol=protocol)) + + assert restored == header + assert type(restored) is cls + assert restored.indexable == header.indexable + + @pytest.mark.parametrize('raw', (False, True)) + def test_deepcopied_decoded_headers_can_be_encoded(self, raw): + """ + A copied decoder result can be passed back to the encoder. + """ + headers = Decoder().decode(Encoder().encode([('x-demo', 'value')]), raw=raw) + + encoded = Encoder().encode(copy.deepcopy(headers)) + + assert Decoder().decode(encoded, raw=raw) == headers