Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-hpack for openSUSE:Factory 
checked in at 2025-02-05 12:39:54
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-hpack (Old)
 and      /work/SRC/openSUSE:Factory/.python-hpack.new.2316 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-hpack"

Wed Feb  5 12:39:54 2025 rev:9 rq:1243134 version:4.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-hpack/python-hpack.changes        
2023-04-22 22:01:31.677599388 +0200
+++ /work/SRC/openSUSE:Factory/.python-hpack.new.2316/python-hpack.changes      
2025-02-05 12:40:05.759733354 +0100
@@ -1,0 +2,27 @@
+Tue Feb  4 11:51:51 UTC 2025 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to 4.1.0
+  * API Changes (Backward Incompatible)**
+    - Support for Python 3.6 has been removed.
+    - Support for Python 3.7 has been removed.
+    - Support for Python 3.8 has been removed.
+    - Renamed `InvalidTableIndex` exception to `InvalidTableIndexError`.
+  * API Changes (Backward Compatible)**
+    - Support for Python 3.9 has been added.
+    - Support for Python 3.10 has been added.
+    - Support for Python 3.11 has been added.
+    - Support for Python 3.12 has been added.
+    - Support for Python 3.13 has been added.
+    - Optimized bytes encoding of headers.
+    - Updated packaging and testing infrastructure.
+    - Code cleanup and linting.
+    - Added type hints.
+- Refresh healthcheck.patch
+- Refresh test_fixtures.tar.xz
+- Switch build system from setuptools to pyproject.toml
+  * Add python-pip and python-wheel to BuildRequires
+  * Replace %python_build with %pyproject_wheel
+  * Replace %python_install with %pyproject_install
+  * Update name for dist directory in %files section
+
+-------------------------------------------------------------------

Old:
----
  hpack-4.0.0.tar.gz

New:
----
  hpack-4.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-hpack.spec ++++++
--- /var/tmp/diff_new_pack.1FJrYO/_old  2025-02-05 12:40:07.323797903 +0100
+++ /var/tmp/diff_new_pack.1FJrYO/_new  2025-02-05 12:40:07.327798068 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-hpack
 #
-# Copyright (c) 2023 SUSE LLC
+# Copyright (c) 2025 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %{?sle15_python_module_pythons}
 Name:           python-hpack
-Version:        4.0.0
+Version:        4.1.0
 Release:        0
 Summary:        Pure-Python HPACK header compression
 License:        MIT
@@ -29,8 +29,10 @@
 Source1:        test_fixtures.tar.xz
 Patch0:         healthcheck.patch
 BuildRequires:  %{python_module hypothesis}
+BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module setuptools}
+BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 BuildArch:      noarch
@@ -46,11 +48,11 @@
 
 %build
 export LC_ALL="en_US.UTF-8"
-%python_build
+%pyproject_wheel
 
 %install
 export LC_ALL="en_US.UTF-8"
-%python_install
+%pyproject_install
 %python_expand %fdupes %{buildroot}%{$python_sitelib}
 
 %check
@@ -60,5 +62,5 @@
 %license LICENSE
 %doc CHANGELOG.rst CONTRIBUTORS.rst README.rst
 %{python_sitelib}/hpack
-%{python_sitelib}/hpack-%{version}-py%{python_version}.egg-info
+%{python_sitelib}/hpack-%{version}.dist-info
 

++++++ healthcheck.patch ++++++
--- /var/tmp/diff_new_pack.1FJrYO/_old  2025-02-05 12:40:07.347798893 +0100
+++ /var/tmp/diff_new_pack.1FJrYO/_new  2025-02-05 12:40:07.351799059 +0100
@@ -1,6 +1,7 @@
---- a/test/test_hpack.py
-+++ b/test/test_hpack.py
-@@ -2,7 +2,7 @@
+diff -Nru hpack-4.1.0.orig/tests/test_hpack.py hpack-4.1.0/tests/test_hpack.py
+--- hpack-4.1.0.orig/tests/test_hpack.py       2024-11-23 09:35:36.000000000 
+0100
++++ hpack-4.1.0/tests/test_hpack.py    2025-02-04 12:22:54.679696648 +0100
+@@ -1,7 +1,7 @@
  import itertools
  import pytest
  
@@ -9,7 +10,7 @@
  from hypothesis.strategies import text, binary, sets, one_of
  
  from hpack import (
-@@ -760,6 +760,7 @@ class TestDictToIterable:
+@@ -767,6 +767,7 @@
          binary().filter(lambda k: k and not k.startswith(b':'))
      )
  
@@ -17,7 +18,7 @@
      @given(
          special_keys=sets(keys),
          boring_keys=sets(keys),
-@@ -797,6 +798,7 @@ class TestDictToIterable:
+@@ -804,6 +805,7 @@
          assert special_keys == received_special
          assert boring_keys == received_boring
  

++++++ hpack-4.0.0.tar.gz -> hpack-4.1.0.tar.gz ++++++
++++ 4662 lines of diff (skipped)

++++++ test_fixtures.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/conftest.py new/tests/conftest.py
--- old/test/conftest.py        1970-01-01 01:00:00.000000000 +0100
+++ new/tests/conftest.py       2025-02-04 12:38:50.885994211 +0100
@@ -0,0 +1,43 @@
+import pytest
+import os
+import json
+
+from hypothesis.strategies import text
+
+# We need to grab one text example from hypothesis to prime its cache.
+text().example()
+
+# This pair of generator expressions are pretty lame, but building lists is a
+# bad idea as I plan to have a substantial number of tests here.
+story_directories = (
+    os.path.join('tests/test_fixtures', d)
+    for d in os.listdir('tests/test_fixtures')
+)
+story_files = (
+    os.path.join(storydir, name)
+    for storydir in story_directories
+    for name in os.listdir(storydir)
+    if 'raw-data' not in storydir
+)
+raw_story_files = (
+    os.path.join('tests/test_fixtures/raw-data', name)
+    for name in os.listdir('tests/test_fixtures/raw-data')
+)
+
+
+@pytest.fixture(scope='class', params=story_files)
+def story(request):
+    """
+    Provides a detailed HPACK story to test with.
+    """
+    with open(request.param, 'r', encoding='utf-8') as f:
+        return json.load(f)
+
+
+@pytest.fixture(scope='class', params=raw_story_files)
+def raw_story(request):
+    """
+    Provides a detailed HPACK story to test the encoder with.
+    """
+    with open(request.param, 'r', encoding='utf-8') as f:
+        return json.load(f)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/test_encode_decode.py 
new/tests/test_encode_decode.py
--- old/test/test_encode_decode.py      1970-01-01 01:00:00.000000000 +0100
+++ new/tests/test_encode_decode.py     2025-02-04 12:38:50.886931637 +0100
@@ -0,0 +1,140 @@
+"""
+Test for the integer encoding/decoding functionality in the HPACK library.
+"""
+import pytest
+
+from hypothesis import given
+from hypothesis.strategies import integers, binary, one_of
+
+from hpack import HPACKDecodingError
+from hpack.hpack import encode_integer, decode_integer
+
+
+class TestIntegerEncoding:
+    # These tests are stolen from the HPACK spec.
+    def test_encoding_10_with_5_bit_prefix(self):
+        val = encode_integer(10, 5)
+        assert len(val) == 1
+        assert val == bytearray(b'\x0a')
+
+    def test_encoding_1337_with_5_bit_prefix(self):
+        val = encode_integer(1337, 5)
+        assert len(val) == 3
+        assert val == bytearray(b'\x1f\x9a\x0a')
+
+    def test_encoding_42_with_8_bit_prefix(self):
+        val = encode_integer(42, 8)
+        assert len(val) == 1
+        assert val == bytearray(b'\x2a')
+
+
+class TestIntegerDecoding:
+    # These tests are stolen from the HPACK spec.
+    def test_decoding_10_with_5_bit_prefix(self):
+        val = decode_integer(b'\x0a', 5)
+        assert val == (10, 1)
+
+    def test_encoding_1337_with_5_bit_prefix(self):
+        val = decode_integer(b'\x1f\x9a\x0a', 5)
+        assert val == (1337, 3)
+
+    def test_encoding_42_with_8_bit_prefix(self):
+        val = decode_integer(b'\x2a', 8)
+        assert val == (42, 1)
+
+    def test_decode_empty_string_fails(self):
+        with pytest.raises(HPACKDecodingError):
+            decode_integer(b'', 8)
+
+    def test_decode_insufficient_data_fails(self):
+        with pytest.raises(HPACKDecodingError):
+            decode_integer(b'\x1f', 5)
+
+
+class TestEncodingProperties:
+    """
+    Property-based tests for our integer encoder and decoder.
+    """
+    @given(
+        integer=integers(min_value=0),
+        prefix_bits=integers(min_value=1, max_value=8)
+    )
+    def test_encode_positive_integer_always_valid(self, integer, prefix_bits):
+        """
+        So long as the prefix bits are between 1 and 8, any positive integer
+        can be represented.
+        """
+        result = encode_integer(integer, prefix_bits)
+        assert isinstance(result, bytearray)
+        assert len(result) > 0
+
+    @given(
+        integer=integers(max_value=-1),
+        prefix_bits=integers(min_value=1, max_value=8)
+    )
+    def test_encode_fails_for_negative_integers(self, integer, prefix_bits):
+        """
+        If the integer to encode is negative, the encoder fails.
+        """
+        with pytest.raises(ValueError):
+            encode_integer(integer, prefix_bits)
+
+    @given(
+        integer=integers(min_value=0),
+        prefix_bits=one_of(
+            integers(max_value=0),
+            integers(min_value=9)
+        )
+    )
+    def test_encode_fails_for_invalid_prefixes(self, integer, prefix_bits):
+        """
+        If the prefix is out of the range [1,8], the encoder fails.
+        """
+        with pytest.raises(ValueError):
+            encode_integer(integer, prefix_bits)
+
+    @given(
+        prefix_bits=one_of(
+            integers(max_value=0),
+            integers(min_value=9)
+        )
+    )
+    def test_decode_fails_for_invalid_prefixes(self, prefix_bits):
+        """
+        If the prefix is out of the range [1,8], the encoder fails.
+        """
+        with pytest.raises(ValueError):
+            decode_integer(b'\x00', prefix_bits)
+
+    @given(
+        data=binary(),
+        prefix_bits=integers(min_value=1, max_value=8)
+    )
+    def test_decode_either_succeeds_or_raises_error(self, data, prefix_bits):
+        """
+        Attempting to decode data either returns a positive integer or throws a
+        HPACKDecodingError.
+        """
+        try:
+            result, consumed = decode_integer(data, prefix_bits)
+        except HPACKDecodingError:
+            pass
+        else:
+            assert isinstance(result, int)
+            assert result >= 0
+            assert consumed > 0
+
+    @given(
+        integer=integers(min_value=0),
+        prefix_bits=integers(min_value=1, max_value=8)
+    )
+    def test_encode_decode_round_trips(self, integer, prefix_bits):
+        """
+        Given valid data, the encoder and decoder can round trip.
+        """
+        encoded_result = encode_integer(integer, prefix_bits)
+        decoded_integer, consumed = decode_integer(
+            bytes(encoded_result), prefix_bits
+        )
+        assert integer == decoded_integer
+        assert consumed > 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/test_hpack.py new/tests/test_hpack.py
--- old/test/test_hpack.py      1970-01-01 01:00:00.000000000 +0100
+++ new/tests/test_hpack.py     2025-02-04 12:38:51.013997144 +0100
@@ -0,0 +1,846 @@
+import itertools
+import pytest
+
+from hypothesis import given
+from hypothesis.strategies import text, binary, sets, one_of
+
+from hpack import (
+    Encoder,
+    Decoder,
+    HeaderTuple,
+    NeverIndexedHeaderTuple,
+    HPACKDecodingError,
+    InvalidTableIndex,
+    OversizedHeaderListError,
+    InvalidTableSizeError,
+)
+from hpack.hpack import _dict_to_iterable, _to_bytes
+
+
+def test_to_bytes():
+    assert _to_bytes(b"foobar") == b"foobar"
+    assert _to_bytes("foobar") == b"foobar"
+    assert _to_bytes(0xABADBABE) == b'2880289470'
+    assert _to_bytes(True) == b'True'
+    assert _to_bytes(Encoder()).startswith(b"<hpack.hpack.Encoder")
+
+
+class TestHPACKEncoder:
+    # These tests are stolen entirely from the IETF specification examples.
+    def test_literal_header_field_with_indexing(self):
+        """
+        The header field representation uses a literal name and a literal
+        value.
+        """
+        e = Encoder()
+        header_set = {'custom-key': 'custom-header'}
+        result = b'\x40\x0acustom-key\x0dcustom-header'
+
+        assert e.encode(header_set, huffman=False) == result
+        assert list(e.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8'))
+            for n, v in header_set.items()
+        ]
+
+    def test_sensitive_headers(self):
+        """
+        Test encoding header values
+        """
+        e = Encoder()
+        result = (b'\x82\x14\x88\x63\xa1\xa9' +
+                  b'\x32\x08\x73\xd0\xc7\x10' +
+                  b'\x87\x25\xa8\x49\xe9\xea' +
+                  b'\x5f\x5f\x89\x41\x6a\x41' +
+                  b'\x92\x6e\xe5\x35\x52\x9f')
+        header_set = [
+            (':method', 'GET', True),
+            (':path', '/jimiscool/', True),
+            ('customkey', 'sensitiveinfo', True),
+        ]
+        assert e.encode(header_set, huffman=True) == result
+
+    def test_non_sensitive_headers_with_header_tuples(self):
+        """
+        A header field stored in a HeaderTuple emits a representation that
+        allows indexing.
+        """
+        e = Encoder()
+        result = (b'\x82\x44\x88\x63\xa1\xa9' +
+                  b'\x32\x08\x73\xd0\xc7\x40' +
+                  b'\x87\x25\xa8\x49\xe9\xea' +
+                  b'\x5f\x5f\x89\x41\x6a\x41' +
+                  b'\x92\x6e\xe5\x35\x52\x9f')
+        header_set = [
+            HeaderTuple(':method', 'GET'),
+            HeaderTuple(':path', '/jimiscool/'),
+            HeaderTuple('customkey', 'sensitiveinfo'),
+        ]
+        assert e.encode(header_set, huffman=True) == result
+
+    def test_sensitive_headers_with_header_tuples(self):
+        """
+        A header field stored in a NeverIndexedHeaderTuple emits a
+        representation that forbids indexing.
+        """
+        e = Encoder()
+        result = (b'\x82\x14\x88\x63\xa1\xa9' +
+                  b'\x32\x08\x73\xd0\xc7\x10' +
+                  b'\x87\x25\xa8\x49\xe9\xea' +
+                  b'\x5f\x5f\x89\x41\x6a\x41' +
+                  b'\x92\x6e\xe5\x35\x52\x9f')
+        header_set = [
+            NeverIndexedHeaderTuple(':method', 'GET'),
+            NeverIndexedHeaderTuple(':path', '/jimiscool/'),
+            NeverIndexedHeaderTuple('customkey', 'sensitiveinfo'),
+        ]
+        assert e.encode(header_set, huffman=True) == result
+
+    def test_header_table_size_getter(self):
+        e = Encoder()
+        assert e.header_table_size == 4096
+
+    def test_indexed_literal_header_field_with_indexing(self):
+        """
+        The header field representation uses an indexed name and a literal
+        value and performs incremental indexing.
+        """
+        e = Encoder()
+        header_set = {':path': '/sample/path'}
+        result = b'\x44\x0c/sample/path'
+
+        assert e.encode(header_set, huffman=False) == result
+        assert list(e.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8'))
+            for n, v in header_set.items()
+        ]
+
+    def test_indexed_header_field(self):
+        """
+        The header field representation uses an indexed header field, from
+        the static table.
+        """
+        e = Encoder()
+        header_set = {':method': 'GET'}
+        result = b'\x82'
+
+        assert e.encode(header_set, huffman=False) == result
+        assert list(e.header_table.dynamic_entries) == []
+
+    def test_indexed_header_field_from_static_table(self):
+        e = Encoder()
+        e.header_table_size = 0
+        header_set = {':method': 'GET'}
+        result = b'\x82'
+
+        # Make sure we don't emit an encoding context update.
+        e.header_table.resized = False
+
+        assert e.encode(header_set, huffman=False) == result
+        assert list(e.header_table.dynamic_entries) == []
+
+    def test_request_examples_without_huffman(self):
+        """
+        This section shows several consecutive header sets, corresponding to
+        HTTP requests, on the same connection.
+        """
+        e = Encoder()
+        first_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com'),
+        ]
+        # We should have :authority in first_header_table since we index it
+        first_header_table = [(':authority', 'www.example.com')]
+        first_result = b'\x82\x86\x84\x41\x0fwww.example.com'
+
+        assert e.encode(first_header_set, huffman=False) == first_result
+        assert list(e.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8'))
+            for n, v in first_header_table
+        ]
+
+        second_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com',),
+            ('cache-control', 'no-cache'),
+        ]
+        second_header_table = [
+            ('cache-control', 'no-cache'),
+            (':authority', 'www.example.com')
+        ]
+        second_result = b'\x82\x86\x84\xbeX\x08no-cache'
+
+        assert e.encode(second_header_set, huffman=False) == second_result
+        assert list(e.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8'))
+            for n, v in second_header_table
+        ]
+
+        third_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'https',),
+            (':path', '/index.html',),
+            (':authority', 'www.example.com',),
+            ('custom-key', 'custom-value'),
+        ]
+        third_result = (
+            b'\x82\x87\x85\xbf@\ncustom-key\x0ccustom-value'
+        )
+
+        assert e.encode(third_header_set, huffman=False) == third_result
+        # Don't check the header table here, it's just too complex to be
+        # reliable. Check its length though.
+        assert len(e.header_table.dynamic_entries) == 3
+
+    def test_request_examples_with_huffman(self):
+        """
+        This section shows the same examples as the previous section, but
+        using Huffman encoding for the literal values.
+        """
+        e = Encoder()
+        first_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com'),
+        ]
+        first_header_table = [(':authority', 'www.example.com')]
+        first_result = (
+            b'\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+        )
+
+        assert e.encode(first_header_set, huffman=True) == first_result
+        assert list(e.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8'))
+            for n, v in first_header_table
+        ]
+
+        second_header_table = [
+            ('cache-control', 'no-cache'),
+            (':authority', 'www.example.com')
+        ]
+        second_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com',),
+            ('cache-control', 'no-cache'),
+        ]
+        second_result = b'\x82\x86\x84\xbeX\x86\xa8\xeb\x10d\x9c\xbf'
+
+        assert e.encode(second_header_set, huffman=True) == second_result
+        assert list(e.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8'))
+            for n, v in second_header_table
+        ]
+
+        third_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'https',),
+            (':path', '/index.html',),
+            (':authority', 'www.example.com',),
+            ('custom-key', 'custom-value'),
+        ]
+        third_result = (
+            b'\x82\x87\x85\xbf'
+            b'@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf'
+        )
+
+        assert e.encode(third_header_set, huffman=True) == third_result
+        assert len(e.header_table.dynamic_entries) == 3
+
+    # These tests are custom, for hyper.
+    def test_resizing_header_table(self):
+        # We need to encode a substantial number of headers, to populate the
+        # header table.
+        e = Encoder()
+        header_set = [
+            (':method', 'GET'),
+            (':scheme', 'https'),
+            (':path', '/some/path'),
+            (':authority', 'www.example.com'),
+            ('custom-key', 'custom-value'),
+            (
+                "user-agent",
+                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) "
+                "Gecko/20100101 Firefox/16.0",
+            ),
+            (
+                "accept",
+                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;"
+                "q=0.8",
+            ),
+            ('X-Lukasa-Test', '88989'),
+        ]
+        e.encode(header_set, huffman=True)
+
+        # Resize the header table to a size so small that nothing can be in it.
+        e.header_table_size = 40
+        assert len(e.header_table.dynamic_entries) == 0
+
+    def test_resizing_header_table_sends_multiple_updates(self):
+        e = Encoder()
+
+        e.header_table_size = 40
+        e.header_table_size = 100
+        e.header_table_size = 40
+
+        header_set = [(':method', 'GET')]
+        out = e.encode(header_set, huffman=True)
+        assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82'
+
+    def test_resizing_header_table_to_same_size_ignored(self):
+        e = Encoder()
+
+        # These size changes should be ignored
+        e.header_table_size = 4096
+        e.header_table_size = 4096
+        e.header_table_size = 4096
+
+        # These size changes should be encoded
+        e.header_table_size = 40
+        e.header_table_size = 100
+        e.header_table_size = 40
+
+        header_set = [(':method', 'GET')]
+        out = e.encode(header_set, huffman=True)
+        assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82'
+
+    def test_resizing_header_table_sends_context_update(self):
+        e = Encoder()
+
+        # Resize the header table to a size so small that nothing can be in it.
+        e.header_table_size = 40
+
+        # Now, encode a header set. Just a small one, with a well-defined
+        # output.
+        header_set = [(':method', 'GET')]
+        out = e.encode(header_set, huffman=True)
+
+        assert out == b'?\t\x82'
+
+    def test_setting_table_size_to_the_same_does_nothing(self):
+        e = Encoder()
+
+        # Set the header table size to the default.
+        e.header_table_size = 4096
+
+        # Now encode a header set. Just a small one, with a well-defined
+        # output.
+        header_set = [(':method', 'GET')]
+        out = e.encode(header_set, huffman=True)
+
+        assert out == b'\x82'
+
+    def test_evicting_header_table_objects(self):
+        e = Encoder()
+
+        # Set the header table size large enough to include one header.
+        e.header_table_size = 66
+        header_set = [('a', 'b'), ('long-custom-header', 'longish value')]
+        e.encode(header_set)
+
+        assert len(e.header_table.dynamic_entries) == 1
+
+    def test_headers_generator(self):
+        e = Encoder()
+
+        def headers_generator():
+            return (("k" + str(i), "v" + str(i)) for i in range(3))
+
+        header_set = headers_generator()
+        out = e.encode(header_set)
+        assert Decoder().decode(out) == list(headers_generator())
+
+
+class TestHPACKDecoder:
+    # These tests are stolen entirely from the IETF specification examples.
+    def test_literal_header_field_with_indexing(self):
+        """
+        The header field representation uses a literal name and a literal
+        value.
+        """
+        d = Decoder()
+        header_set = [('custom-key', 'custom-header')]
+        data = b'\x40\x0acustom-key\x0dcustom-header'
+
+        assert d.decode(data) == header_set
+        assert list(d.header_table.dynamic_entries) == [
+            (n.encode('utf-8'), v.encode('utf-8')) for n, v in header_set
+        ]
+
+    def test_raw_decoding(self):
+        """
+        The header field representation is decoded as a raw byte string instead
+        of UTF-8
+        """
+        d = Decoder()
+        header_set = [
+            (b'\x00\x01\x99\x30\x11\x22\x55\x21\x89\x14', b'custom-header')
+        ]
+        data = (
+            b'\x40\x0a\x00\x01\x99\x30\x11\x22\x55\x21\x89\x14\x0d'
+            b'custom-header'
+        )
+
+        assert d.decode(data, raw=True) == header_set
+
+    def test_literal_header_field_without_indexing(self):
+        """
+        The header field representation uses an indexed name and a literal
+        value.
+        """
+        d = Decoder()
+        header_set = [(':path', '/sample/path')]
+        data = b'\x04\x0c/sample/path'
+
+        assert d.decode(data) == header_set
+        assert list(d.header_table.dynamic_entries) == []
+
+    def test_header_table_size_getter(self):
+        d = Decoder()
+        assert d.header_table_size
+
+    def test_indexed_header_field(self):
+        """
+        The header field representation uses an indexed header field, from
+        the static table.
+        """
+        d = Decoder()
+        header_set = [(':method', 'GET')]
+        data = b'\x82'
+
+        assert d.decode(data) == header_set
+        assert list(d.header_table.dynamic_entries) == []
+
+    def test_request_examples_without_huffman(self):
+        """
+        This section shows several consecutive header sets, corresponding to
+        HTTP requests, on the same connection.
+        """
+        d = Decoder()
+        first_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com'),
+        ]
+        # The first_header_table doesn't contain 'authority'
+        first_data = b'\x82\x86\x84\x01\x0fwww.example.com'
+
+        assert d.decode(first_data) == first_header_set
+        assert list(d.header_table.dynamic_entries) == []
+
+        # This request takes advantage of the differential encoding of header
+        # sets.
+        second_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com',),
+            ('cache-control', 'no-cache'),
+        ]
+        second_data = (
+            b'\x82\x86\x84\x01\x0fwww.example.com\x0f\t\x08no-cache'
+        )
+
+        assert d.decode(second_data) == second_header_set
+        assert list(d.header_table.dynamic_entries) == []
+
+        third_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'https',),
+            (':path', '/index.html',),
+            (':authority', 'www.example.com',),
+            ('custom-key', 'custom-value'),
+        ]
+        third_data = (
+            b'\x82\x87\x85\x01\x0fwww.example.com@\ncustom-key\x0ccustom-value'
+        )
+
+        assert d.decode(third_data) == third_header_set
+        # Don't check the header table here, it's just too complex to be
+        # reliable. Check its length though.
+        assert len(d.header_table.dynamic_entries) == 1
+
+    def test_request_examples_with_huffman(self):
+        """
+        This section shows the same examples as the previous section, but
+        using Huffman encoding for the literal values.
+        """
+        d = Decoder()
+
+        first_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com'),
+        ]
+        first_data = (
+            b'\x82\x86\x84\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+        )
+
+        assert d.decode(first_data) == first_header_set
+        assert list(d.header_table.dynamic_entries) == []
+
+        second_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'http',),
+            (':path', '/',),
+            (':authority', 'www.example.com',),
+            ('cache-control', 'no-cache'),
+        ]
+        second_data = (
+            b'\x82\x86\x84\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+            b'\x0f\t\x86\xa8\xeb\x10d\x9c\xbf'
+        )
+
+        assert d.decode(second_data) == second_header_set
+        assert list(d.header_table.dynamic_entries) == []
+
+        third_header_set = [
+            (':method', 'GET',),
+            (':scheme', 'https',),
+            (':path', '/index.html',),
+            (':authority', 'www.example.com',),
+            ('custom-key', 'custom-value'),
+        ]
+        third_data = (
+            b'\x82\x87\x85\x01\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff@'
+            b'\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf'
+        )
+
+        assert d.decode(third_data) == third_header_set
+        assert len(d.header_table.dynamic_entries) == 1
+
+    # These tests are custom, for hyper.
+    def test_resizing_header_table(self):
+        # We need to decode a substantial number of headers, to populate the
+        # header table. This string isn't magic: it's the output from the
+        # equivalent test for the Encoder.
+        d = Decoder()
+        data = (
+            b'\x82\x87D\x87a\x07\xa4\xacV4\xcfA\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0'
+            b'\xab\x90\xf4\xff@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8'
+            b'\xb4\xbfz\xbc\xd0\x7ff\xa2\x81\xb0\xda\xe0S\xfa\xd02\x1a\xa4\x9d'
+            b'\x13\xfd\xa9\x92\xa4\x96\x854\x0c\x8aj\xdc\xa7\xe2\x81\x02\xef}'
+            b'\xa9g{\x81qp\x7fjb):\x9d\x81\x00 \x00@\x150\x9a\xc2\xca\x7f,\x05'
+            b'\xc5\xc1S\xb0I|\xa5\x89\xd3M\x1fC\xae\xba\x0cA\xa4\xc7\xa9\x8f3'
+            b'\xa6\x9a?\xdf\x9ah\xfa\x1du\xd0b\r&=Ly\xa6\x8f\xbe\xd0\x01w\xfe'
+            b'\xbeX\xf9\xfb\xed\x00\x17{@\x8a\xfc[=\xbdF\x81\xad\xbc\xa8O\x84y'
+            b'\xe7\xde\x7f'
+        )
+        d.decode(data)
+
+        # Resize the header table to a size so small that nothing can be in it.
+        d.header_table_size = 40
+        assert len(d.header_table.dynamic_entries) == 0
+
+    def test_apache_trafficserver(self):
+        # This test reproduces the bug in #110, using exactly the same header
+        # data.
+        d = Decoder()
+        data = (
+            b'\x10\x07:status\x03200@\x06server\tATS/6.0.0'
+            b'@\x04date\x1dTue, 31 Mar 2015 08:09:51 GMT'
+            b'@\x0ccontent-type\ttext/html@\x0econtent-length\x0542468'
+            b'@\rlast-modified\x1dTue, 31 Mar 2015 01:55:51 GMT'
+            b'@\x04vary\x0fAccept-Encoding@\x04etag\x0f"5519fea7-a5e4"'
+            b'@\x08x-served\x05Nginx@\x14x-subdomain-tryfiles\x04True'
+            b'@\x07x-deity\thydra-lts@\raccept-ranges\x05bytes@\x03age\x010'
+            b'@\x19strict-transport-security\rmax-age=86400'
+            b'@\x03via2https/1.1 ATS (ApacheTrafficServer/6.0.0 [cSsNfU])'
+        )
+        expect = [
+            (':status', '200'),
+            ('server', 'ATS/6.0.0'),
+            ('date', 'Tue, 31 Mar 2015 08:09:51 GMT'),
+            ('content-type', 'text/html'),
+            ('content-length', '42468'),
+            ('last-modified', 'Tue, 31 Mar 2015 01:55:51 GMT'),
+            ('vary', 'Accept-Encoding'),
+            ('etag', '"5519fea7-a5e4"'),
+            ('x-served', 'Nginx'),
+            ('x-subdomain-tryfiles', 'True'),
+            ('x-deity', 'hydra-lts'),
+            ('accept-ranges', 'bytes'),
+            ('age', '0'),
+            ('strict-transport-security', 'max-age=86400'),
+            ('via', 'https/1.1 ATS (ApacheTrafficServer/6.0.0 [cSsNfU])'),
+        ]
+
+        result = d.decode(data)
+
+        assert result == expect
+        # The status header shouldn't be indexed.
+        assert len(d.header_table.dynamic_entries) == len(expect) - 1
+
+    def test_utf8_errors_raise_hpack_decoding_error(self):
+        d = Decoder()
+
+        # Invalid UTF-8 data.
+        data = b'\x82\x86\x84\x01\x10www.\x07\xaa\xd7\x95\xd7\xa8\xd7\x94.com'
+
+        with pytest.raises(HPACKDecodingError):
+            d.decode(data)
+
+    def test_invalid_indexed_literal(self):
+        d = Decoder()
+
+        # Refer to an index that is too large.
+        data = b'\x82\x86\x84\x7f\x0a\x0fwww.example.com'
+        with pytest.raises(InvalidTableIndex):
+            d.decode(data)
+
+    def test_invalid_indexed_header(self):
+        d = Decoder()
+
+        # Refer to an indexed header that is too large.
+        data = b'\xBE\x86\x84\x01\x0fwww.example.com'
+        with pytest.raises(InvalidTableIndex):
+            d.decode(data)
+
+    def test_literal_header_field_with_indexing_emits_headertuple(self):
+        """
+        A header field with indexing emits a HeaderTuple.
+        """
+        d = Decoder()
+        data = b'\x00\x0acustom-key\x0dcustom-header'
+
+        headers = d.decode(data)
+        assert len(headers) == 1
+
+        header = headers[0]
+        assert isinstance(header, HeaderTuple)
+        assert not isinstance(header, NeverIndexedHeaderTuple)
+
+    def test_literal_never_indexed_emits_neverindexedheadertuple(self):
+        """
+        A literal header field that must never be indexed emits a
+        NeverIndexedHeaderTuple.
+        """
+        d = Decoder()
+        data = b'\x10\x0acustom-key\x0dcustom-header'
+
+        headers = d.decode(data)
+        assert len(headers) == 1
+
+        header = headers[0]
+        assert isinstance(header, NeverIndexedHeaderTuple)
+
+    def test_indexed_never_indexed_emits_neverindexedheadertuple(self):
+        """
+        A header field with an indexed name that must never be indexed emits a
+        NeverIndexedHeaderTuple.
+        """
+        d = Decoder()
+        data = b'\x14\x0c/sample/path'
+
+        headers = d.decode(data)
+        assert len(headers) == 1
+
+        header = headers[0]
+        assert isinstance(header, NeverIndexedHeaderTuple)
+
+    def test_max_header_list_size(self):
+        """
+        If the header block is larger than the max_header_list_size, the HPACK
+        decoder throws an OversizedHeaderListError.
+        """
+        d = Decoder(max_header_list_size=44)
+        data = b'\x14\x0c/sample/path'
+
+        with pytest.raises(OversizedHeaderListError):
+            d.decode(data)
+
+    def test_can_decode_multiple_header_table_size_changes(self):
+        """
+        If multiple header table size changes are sent in at once, they are
+        successfully decoded.
+        """
+        d = Decoder()
+        data = b'?a?\xe1\x1f\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+        expect = [
+            (':method', 'GET'),
+            (':scheme', 'https'),
+            (':path', '/'),
+            (':authority', '127.0.0.1:8443')
+        ]
+
+        assert d.decode(data) == expect
+
+    def test_header_table_size_change_above_maximum(self):
+        """
+        If a header table size change is received that exceeds the maximum
+        allowed table size, it is rejected.
+        """
+        d = Decoder()
+        d.max_allowed_table_size = 127
+        data = b'?a\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+
+        with pytest.raises(InvalidTableSizeError):
+            d.decode(data)
+
+    def test_table_size_not_adjusting(self):
+        """
+        If the header table size is shrunk, and then the remote peer doesn't
+        join in the shrinking, then an error is raised.
+        """
+        d = Decoder()
+        d.max_allowed_table_size = 128
+        data = b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+
+        with pytest.raises(InvalidTableSizeError):
+            d.decode(data)
+
+    def test_table_size_last_rejected(self):
+        """
+        If a header table size change comes last in the header block, it is
+        forbidden.
+        """
+        d = Decoder()
+        data = b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99?a'
+
+        with pytest.raises(HPACKDecodingError):
+            d.decode(data)
+
+    def test_table_size_middle_rejected(self):
+        """
+        If a header table size change comes anywhere but first in the header
+        block, it is forbidden.
+        """
+        d = Decoder()
+        data = b'\x82?a\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+
+        with pytest.raises(HPACKDecodingError):
+            d.decode(data)
+
+    def test_truncated_header_name(self):
+        """
+        If a header name is truncated an error is raised.
+        """
+        d = Decoder()
+        # This is a simple header block that has a bad ending. The interesting
+        # part begins on the second line. This indicates a string that has
+        # literal name and value. The name is a 5 character huffman-encoded
+        # string that is only three bytes long.
+        data = (
+            b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+            b'\x00\x85\xf2\xb2J'
+        )
+
+        with pytest.raises(HPACKDecodingError):
+            d.decode(data)
+
+    def test_truncated_header_value(self):
+        """
+        If a header value is truncated an error is raised.
+        """
+        d = Decoder()
+        # This is a simple header block that has a bad ending. The interesting
+        # part begins on the second line. This indicates a string that has
+        # literal name and value. The name is a 5 character huffman-encoded
+        # string, but the entire EOS character has been written over the end.
+        # This causes hpack to see the header value as being supposed to be
+        # 622462 bytes long, which it clearly is not, and so this must fail.
+        data = (
+            b'\x82\x87\x84A\x8a\x08\x9d\\\x0b\x81p\xdcy\xa6\x99'
+            b'\x00\x85\xf2\xb2J\x87\xff\xff\xff\xfd%B\x7f'
+        )
+
+        with pytest.raises(HPACKDecodingError):
+            d.decode(data)
+
+
+class TestDictToIterable:
+    """
+    The dict_to_iterable function has some subtle requirements: validates that
+    everything behaves as expected.
+
+    As much as possible this tries to be exhaustive.
+    """
+    keys = one_of(
+        text().filter(lambda k: k and not k.startswith(':')),
+        binary().filter(lambda k: k and not k.startswith(b':'))
+    )
+
+    @given(
+        special_keys=sets(keys),
+        boring_keys=sets(keys),
+    )
+    def test_ordering(self, special_keys, boring_keys):
+        """
+        _dict_to_iterable produces an iterable where all the keys beginning
+        with a colon are emitted first.
+        """
+        def _prepend_colon(k):
+            if isinstance(k, str):
+                return ':' + k
+            else:
+                return b':' + k
+
+        special_keys = set(map(_prepend_colon, special_keys))
+        input_dict = {
+            k: b'testval' for k in itertools.chain(
+                special_keys,
+                boring_keys
+            )
+        }
+        filtered = _dict_to_iterable(input_dict)
+
+        received_special = set()
+        received_boring = set()
+
+        for _ in special_keys:
+            k, _ = next(filtered)
+            received_special.add(k)
+        for _ in boring_keys:
+            k, _ = next(filtered)
+            received_boring.add(k)
+
+        assert special_keys == received_special
+        assert boring_keys == received_boring
+
+    @given(
+        special_keys=sets(keys),
+        boring_keys=sets(keys),
+    )
+    def test_ordering_applies_to_encoding(self, special_keys, boring_keys):
+        """
+        When encoding a dictionary the special keys all appear first.
+        """
+        def _prepend_colon(k):
+            if isinstance(k, str):
+                return ':' + k
+            else:
+                return b':' + k
+
+        special_keys = set(map(_prepend_colon, special_keys))
+        input_dict = {
+            k: b'testval' for k in itertools.chain(
+                special_keys,
+                boring_keys
+            )
+        }
+        e = Encoder()
+        d = Decoder()
+        encoded = e.encode(input_dict)
+        decoded = iter(d.decode(encoded, raw=True))
+
+        received_special = set()
+        received_boring = set()
+        expected_special = set(map(_to_bytes, special_keys))
+        expected_boring = set(map(_to_bytes, boring_keys))
+
+        for _ in special_keys:
+            k, _ = next(decoded)
+            received_special.add(k)
+        for _ in boring_keys:
+            k, _ = next(decoded)
+            received_boring.add(k)
+
+        assert expected_special == received_special
+        assert expected_boring == received_boring
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/test_hpack_integration.py 
new/tests/test_hpack_integration.py
--- old/test/test_hpack_integration.py  1970-01-01 01:00:00.000000000 +0100
+++ new/tests/test_hpack_integration.py 2025-02-04 12:38:51.013997144 +0100
@@ -0,0 +1,74 @@
+"""
+This module defines substantial HPACK integration tests. These can take a very
+long time to run, so they're outside the main test suite, but they need to be
+run before every change to HPACK.
+"""
+from binascii import unhexlify
+from pytest import skip
+
+from hpack import Decoder, Encoder, HeaderTuple
+
+
+class TestHPACKDecoderIntegration:
+    def test_can_decode_a_story(self, story):
+        d = Decoder()
+
+        # We test against draft 9 of the HPACK spec.
+        if story['draft'] != 9:
+            skip("We test against draft 9, not draft %d" % story['draft'])
+
+        for case in story['cases']:
+            try:
+                d.header_table_size = case['header_table_size']
+            except KeyError:
+                pass
+            decoded_headers = d.decode(unhexlify(case['wire']))
+
+            # The correct headers are a list of dicts, which is annoying.
+            correct_headers = [
+                (item[0], item[1])
+                for header in case['headers']
+                for item in header.items()
+            ]
+            correct_headers = correct_headers
+            assert correct_headers == decoded_headers
+            assert all(
+                isinstance(header, HeaderTuple) for header in decoded_headers
+            )
+
+    def test_can_encode_a_story_no_huffman(self, raw_story):
+        d = Decoder()
+        e = Encoder()
+
+        for case in raw_story['cases']:
+            # The input headers are a list of dicts, which is annoying.
+            input_headers = [
+                (item[0], item[1])
+                for header in case['headers']
+                for item in header.items()
+            ]
+
+            encoded = e.encode(input_headers, huffman=False)
+            decoded_headers = d.decode(encoded)
+
+            assert input_headers == decoded_headers
+            assert all(
+                isinstance(header, HeaderTuple) for header in decoded_headers
+            )
+
+    def test_can_encode_a_story_with_huffman(self, raw_story):
+        d = Decoder()
+        e = Encoder()
+
+        for case in raw_story['cases']:
+            # The input headers are a list of dicts, which is annoying.
+            input_headers = [
+                (item[0], item[1])
+                for header in case['headers']
+                for item in header.items()
+            ]
+
+            encoded = e.encode(input_headers, huffman=True)
+            decoded_headers = d.decode(encoded)
+
+            assert input_headers == decoded_headers
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/test_huffman.py new/tests/test_huffman.py
--- old/test/test_huffman.py    1970-01-01 01:00:00.000000000 +0100
+++ new/tests/test_huffman.py   2025-02-04 12:38:51.013997144 +0100
@@ -0,0 +1,54 @@
+from hpack import HPACKDecodingError
+from hpack.huffman import HuffmanEncoder
+from hpack.huffman_constants import REQUEST_CODES, REQUEST_CODES_LENGTH
+from hpack.huffman_table import decode_huffman
+
+from hypothesis import given, example
+from hypothesis.strategies import binary
+
+
+class TestHuffman:
+
+    def test_request_huffman_decoder(self):
+        assert (
+            decode_huffman(b'\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff') ==
+            b"www.example.com"
+        )
+        assert decode_huffman(b'\xa8\xeb\x10d\x9c\xbf') == b"no-cache"
+        assert decode_huffman(b'%\xa8I\xe9[\xa9}\x7f') == b"custom-key"
+        assert (
+            decode_huffman(b'%\xa8I\xe9[\xb8\xe8\xb4\xbf') == b"custom-value"
+        )
+
+    def test_request_huffman_encode(self):
+        encoder = HuffmanEncoder(REQUEST_CODES, REQUEST_CODES_LENGTH)
+        assert (
+            encoder.encode(b"www.example.com") ==
+            b'\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff'
+        )
+        assert encoder.encode(b"no-cache") == b'\xa8\xeb\x10d\x9c\xbf'
+        assert encoder.encode(b"custom-key") == b'%\xa8I\xe9[\xa9}\x7f'
+        assert (
+            encoder.encode(b"custom-value") == b'%\xa8I\xe9[\xb8\xe8\xb4\xbf'
+        )
+
+
+class TestHuffmanDecoder:
+    @given(data=binary())
+    @example(b'\xff')
+    @example(b'\x5f\xff\xff\xff\xff')
+    @example(b'\x00\x3f\xff\xff\xff')
+    def test_huffman_decoder_properly_handles_all_bytestrings(self, data):
+        """
+        When given random bytestrings, either we get HPACKDecodingError or we
+        get a bytestring back.
+        """
+        # The examples aren't special, they're just known to hit specific error
+        # paths through the state machine. Basically, they are strings that are
+        # definitely invalid.
+        try:
+            result = decode_huffman(data)
+        except HPACKDecodingError:
+            result = b''
+
+        assert isinstance(result, bytes)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/test_struct.py new/tests/test_struct.py
--- old/test/test_struct.py     1970-01-01 01:00:00.000000000 +0100
+++ new/tests/test_struct.py    2025-02-04 12:38:51.013997144 +0100
@@ -0,0 +1,70 @@
+import pytest
+
+from hpack import HeaderTuple, NeverIndexedHeaderTuple
+
+
+class TestHeaderTuple:
+    def test_is_tuple(self):
+        """
+        HeaderTuple objects are tuples.
+        """
+        h = HeaderTuple('name', 'value')
+        assert isinstance(h, tuple)
+
+    def test_unpacks_properly(self):
+        """
+        HeaderTuple objects unpack like tuples.
+        """
+        h = HeaderTuple('name', 'value')
+        k, v = h
+
+        assert k == 'name'
+        assert v == 'value'
+
+    def test_header_tuples_are_indexable(self):
+        """
+        HeaderTuple objects can be indexed.
+        """
+        h = HeaderTuple('name', 'value')
+        assert h.indexable
+
+    def test_never_indexed_tuples_are_not_indexable(self):
+        """
+        NeverIndexedHeaderTuple objects cannot be indexed.
+        """
+        h = NeverIndexedHeaderTuple('name', 'value')
+        assert not h.indexable
+
+    @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple))
+    def test_equal_to_tuples(self, cls):
+        """
+        HeaderTuples and NeverIndexedHeaderTuples are equal to equivalent
+        tuples.
+        """
+        t1 = ('name', 'value')
+        t2 = cls('name', 'value')
+
+        assert t1 == t2
+        assert t1 is not t2
+
+    @pytest.mark.parametrize('cls', (HeaderTuple, NeverIndexedHeaderTuple))
+    def test_equal_to_self(self, cls):
+        """
+        HeaderTuples and NeverIndexedHeaderTuples are always equal when
+        compared to the same class.
+        """
+        t1 = cls('name', 'value')
+        t2 = cls('name', 'value')
+
+        assert t1 == t2
+        assert t1 is not t2
+
+    def test_equal_for_different_indexes(self):
+        """
+        HeaderTuples compare equal to equivalent NeverIndexedHeaderTuples.
+        """
+        t1 = HeaderTuple('name', 'value')
+        t2 = NeverIndexedHeaderTuple('name', 'value')
+
+        assert t1 == t2
+        assert t1 is not t2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/test_table.py new/tests/test_table.py
--- old/test/test_table.py      1970-01-01 01:00:00.000000000 +0100
+++ new/tests/test_table.py     2025-02-04 12:38:51.013997144 +0100
@@ -0,0 +1,143 @@
+import pytest
+
+from hpack import InvalidTableIndex
+from hpack.table import HeaderTable, table_entry_size
+
+
+class TestPackageFunctions:
+    def test_table_entry_size(self):
+        res = table_entry_size(b'TestName', b'TestValue')
+        assert res == 49
+
+
+class TestHeaderTable:
+    def test_get_by_index_dynamic_table(self):
+        tbl = HeaderTable()
+        off = len(HeaderTable.STATIC_TABLE)
+        val = (b'TestName', b'TestValue')
+        tbl.add(*val)
+        res = tbl.get_by_index(off + 1)
+        assert res == val
+
+    def test_get_by_index_static_table(self):
+        tbl = HeaderTable()
+        exp = (b':authority', b'')
+        res = tbl.get_by_index(1)
+        assert res == exp
+        idx = len(HeaderTable.STATIC_TABLE)
+        exp = (b'www-authenticate', b'')
+        res = tbl.get_by_index(idx)
+        assert res == exp
+
+    def test_get_by_index_zero_index(self):
+        tbl = HeaderTable()
+        with pytest.raises(InvalidTableIndex):
+            tbl.get_by_index(0)
+
+    def test_get_by_index_out_of_range(self):
+        tbl = HeaderTable()
+        off = len(HeaderTable.STATIC_TABLE)
+        tbl.add(b'TestName', b'TestValue')
+        with pytest.raises(InvalidTableIndex) as e:
+            tbl.get_by_index(off + 2)
+
+        assert (
+            "Invalid table index %d" % (off + 2) in str(e.value)
+        )
+
+    def test_repr(self):
+        tbl = HeaderTable()
+        tbl.add(b'TestName1', b'TestValue1')
+        tbl.add(b'TestName2', b'TestValue2')
+        tbl.add(b'TestName2', b'TestValue2')
+        exp = (
+            "HeaderTable(4096, False, deque(["
+            "(b'TestName2', b'TestValue2'), "
+            "(b'TestName2', b'TestValue2'), "
+            "(b'TestName1', b'TestValue1')"
+            "]))"
+        )
+        res = repr(tbl)
+        assert res == exp
+
+    def test_add_to_large(self):
+        tbl = HeaderTable()
+        # Max size to small to hold the value we specify
+        tbl.maxsize = 1
+        tbl.add(b'TestName', b'TestValue')
+        # Table length should be 0
+        assert len(tbl.dynamic_entries) == 0
+
+    def test_search_in_static_full(self):
+        tbl = HeaderTable()
+        itm = (b':authority', b'')
+        exp = (1, itm[0], itm[1])
+        res = tbl.search(itm[0], itm[1])
+        assert res == exp
+
+    def test_search_in_static_partial(self):
+        tbl = HeaderTable()
+        exp = (1, b':authority', None)
+        res = tbl.search(b':authority', b'NotInTable')
+        assert res == exp
+
+    def test_search_in_dynamic_full(self):
+        tbl = HeaderTable()
+        idx = len(HeaderTable.STATIC_TABLE) + 1
+        tbl.add(b'TestName', b'TestValue')
+        exp = (idx, b'TestName', b'TestValue')
+        res = tbl.search(b'TestName', b'TestValue')
+        assert res == exp
+
+    def test_search_in_dynamic_partial(self):
+        tbl = HeaderTable()
+        idx = len(HeaderTable.STATIC_TABLE) + 1
+        tbl.add(b'TestName', b'TestValue')
+        exp = (idx, b'TestName', None)
+        res = tbl.search(b'TestName', b'NotInTable')
+        assert res == exp
+
+    def test_search_no_match(self):
+        tbl = HeaderTable()
+        tbl.add(b'TestName', b'TestValue')
+        res = tbl.search(b'NotInTable', b'NotInTable')
+        assert res is None
+
+    def test_maxsize_prop_getter(self):
+        tbl = HeaderTable()
+        assert tbl.maxsize == HeaderTable.DEFAULT_SIZE
+
+    def test_maxsize_prop_setter(self):
+        tbl = HeaderTable()
+        exp = int(HeaderTable.DEFAULT_SIZE / 2)
+        tbl.maxsize = exp
+        assert tbl.resized is True
+        assert tbl.maxsize == exp
+        tbl.resized = False
+        tbl.maxsize = exp
+        assert tbl.resized is False
+        assert tbl.maxsize == exp
+
+    def test_size(self):
+        tbl = HeaderTable()
+        for i in range(3):
+            tbl.add(b'TestName', b'TestValue')
+        res = tbl._current_size
+        assert res == 147
+
+    def test_shrink_maxsize_is_zero(self):
+        tbl = HeaderTable()
+        tbl.add(b'TestName', b'TestValue')
+        assert len(tbl.dynamic_entries) == 1
+        tbl.maxsize = 0
+        assert len(tbl.dynamic_entries) == 0
+
+    def test_shrink_maxsize(self):
+        tbl = HeaderTable()
+        for i in range(3):
+            tbl.add(b'TestName', b'TestValue')
+
+        assert tbl._current_size == 147
+        tbl.maxsize = 146
+        assert len(tbl.dynamic_entries) == 2
+        assert tbl._current_size == 98

Reply via email to