From 27e8a86300b61a2bdbf91f13faf7b5d2c8612f1b Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Fri, 20 Dec 2024 18:35:32 +0100 Subject: [PATCH] lint++ --- CHANGELOG.rst | 1 + docs/source/api.rst | 2 + pyproject.toml | 38 +++++- src/hpack/__init__.py | 36 +++--- src/hpack/exceptions.py | 25 ++-- src/hpack/hpack.py | 204 ++++++++++++++++++--------------- src/hpack/huffman.py | 18 ++- src/hpack/huffman_constants.py | 3 - src/hpack/huffman_table.py | 31 ++--- src/hpack/struct.py | 11 +- 10 files changed, 211 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index adc0a90..f2125cb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ dev - 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)** diff --git a/docs/source/api.rst b/docs/source/api.rst index f9f8e24..7c0b87e 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -19,6 +19,8 @@ This document provides the HPACK API. .. autoclass:: hpack.HPACKDecodingError +.. autoclass:: hpack.InvalidTableIndexError + .. autoclass:: hpack.InvalidTableIndex .. autoclass:: hpack.OversizedHeaderListError diff --git a/pyproject.toml b/pyproject.toml index db11975..d03e920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ testing = [ linting = [ "ruff>=0.8.0,<1", "mypy>=1.13.0,<2", + "typing_extensions>=4.12.2", ] packaging = [ @@ -86,8 +87,43 @@ hpack = [ "py.typed" ] version = { attr = "hpack.__version__" } [tool.ruff] -line-length = 140 +line-length = 150 target-version = "py39" +format.preview = true +format.docstring-code-line-length = 100 +format.docstring-code-format = true +lint.select = [ + "ALL", +] +lint.ignore = [ + "ANN401", # kwargs with typing.Any + "CPY", # not required + "D101", # docs readability + "D102", # docs readability + "D105", # docs readability + "D107", # docs readability + "D200", # docs readability + "D205", # docs readability + "D205", # docs readability + "D203", # docs readability + "D212", # docs readability + "D400", # docs readability + "D401", # docs readability + "D415", # docs readability + "PLR2004", # readability + "SIM108", # readability + "RUF012", # readability + "FBT001", # readability + "FBT002", # readability + "PGH003", # readability + "PGH004", # readability + "PYI034", # PEP 673 not yet available in Python 3.9 - only in 3.11+ +] +lint.isort.required-imports = [ "from __future__ import annotations" ] + +[tool.mypy] +show_error_codes = true +strict = true [tool.pytest.ini_options] testpaths = [ "tests" ] diff --git a/src/hpack/__init__.py b/src/hpack/__init__.py index d89b13b..fc07152 100644 --- a/src/hpack/__init__.py +++ b/src/hpack/__init__.py @@ -1,29 +1,23 @@ """ -hpack -~~~~~ - HTTP/2 header encoding for Python. """ -from .hpack import Encoder, Decoder +from __future__ import annotations + +from .exceptions import HPACKDecodingError, HPACKError, InvalidTableIndex, InvalidTableIndexError, InvalidTableSizeError, OversizedHeaderListError +from .hpack import Decoder, Encoder from .struct import HeaderTuple, NeverIndexedHeaderTuple -from .exceptions import ( - HPACKError, - HPACKDecodingError, - InvalidTableIndex, - OversizedHeaderListError, - InvalidTableSizeError -) __all__ = [ - 'Encoder', - 'Decoder', - 'HeaderTuple', - 'NeverIndexedHeaderTuple', - 'HPACKError', - 'HPACKDecodingError', - 'InvalidTableIndex', - 'OversizedHeaderListError', - 'InvalidTableSizeError', + "Decoder", + "Encoder", + "HPACKDecodingError", + "HPACKError", + "HeaderTuple", + "InvalidTableIndex", + "InvalidTableIndexError", + "InvalidTableSizeError", + "NeverIndexedHeaderTuple", + "OversizedHeaderListError", ] -__version__ = '4.1.0+dev' +__version__ = "4.1.0+dev" diff --git a/src/hpack/exceptions.py b/src/hpack/exceptions.py index 20148b7..51f5083 100644 --- a/src/hpack/exceptions.py +++ b/src/hpack/exceptions.py @@ -1,30 +1,37 @@ """ -hyper/http20/exceptions -~~~~~~~~~~~~~~~~~~~~~~~ - -This defines exceptions used in the HTTP/2 portion of hyper. +Exceptions used in hpack. """ +from __future__ import annotations class HPACKError(Exception): """ The base class for all ``hpack`` exceptions. """ - pass + class HPACKDecodingError(HPACKError): """ An error has been encountered while performing HPACK decoding. """ - pass -class InvalidTableIndex(HPACKDecodingError): + +class InvalidTableIndexError(HPACKDecodingError): """ An invalid table index was received. + + .. versionadded:: 4.1.0 + """ + +class InvalidTableIndex(InvalidTableIndexError): # noqa: N818 + """ + An invalid table index was received. + + .. deprecated:: 4.1.0 + Renamed to :class:`InvalidTableIndexError`, use it instead. """ - pass class OversizedHeaderListError(HPACKDecodingError): @@ -34,7 +41,6 @@ class OversizedHeaderListError(HPACKDecodingError): .. versionadded:: 2.3.0 """ - pass class InvalidTableSizeError(HPACKDecodingError): @@ -45,4 +51,3 @@ class InvalidTableSizeError(HPACKDecodingError): .. versionadded:: 3.0.0 """ - pass diff --git a/src/hpack/hpack.py b/src/hpack/hpack.py index 6184493..caa018c 100644 --- a/src/hpack/hpack.py +++ b/src/hpack/hpack.py @@ -1,29 +1,26 @@ """ -hpack/hpack -~~~~~~~~~~~ - -Implements the HPACK header compression algorithm as detailed by the IETF. +Implements the HPACK header compression algorithm as detailed by RFC 7541. """ +from __future__ import annotations + import logging -from typing import Any, Generator, Iterable, Optional, Union +from typing import TYPE_CHECKING, Any -from .table import HeaderTable, table_entry_size -from .exceptions import ( - HPACKDecodingError, OversizedHeaderListError, InvalidTableSizeError -) +from .exceptions import HPACKDecodingError, InvalidTableSizeError, OversizedHeaderListError from .huffman import HuffmanEncoder -from .huffman_constants import ( - REQUEST_CODES, REQUEST_CODES_LENGTH -) +from .huffman_constants import REQUEST_CODES, REQUEST_CODES_LENGTH from .huffman_table import decode_huffman -from .struct import HeaderTuple, NeverIndexedHeaderTuple, HeaderWeaklyTyped +from .struct import HeaderTuple, HeaderWeaklyTyped, NeverIndexedHeaderTuple from .table import HeaderTable, table_entry_size +if TYPE_CHECKING: + from collections.abc import Iterable # pragma: no cover + log = logging.getLogger(__name__) -INDEX_NONE = b'\x00' -INDEX_NEVER = b'\x10' -INDEX_INCREMENTAL = b'\x40' +INDEX_NONE = b"\x00" +INDEX_NEVER = b"\x10" +INDEX_INCREMENTAL = b"\x40" # Precompute 2^i for 1-8 for use in prefix calcs. # Zero index is not used but there to save a subtraction @@ -50,49 +47,45 @@ def _unicode_if_needed(header: HeaderWeaklyTyped, raw: bool) -> HeaderTuple: def encode_integer(integer: int, prefix_bits: int) -> bytearray: """ - This encodes an integer according to the wacky integer encoding rules + Encodes an integer according to the wacky integer encoding rules defined in the HPACK spec. """ log.debug("Encoding %d with %d bits", integer, prefix_bits) if integer < 0: - raise ValueError( - "Can only encode positive integers, got %s" % integer - ) + msg = f"Can only encode positive integers, got {integer}" + raise ValueError(msg) if prefix_bits < 1 or prefix_bits > 8: - raise ValueError( - "Prefix bits must be between 1 and 8, got %s" % prefix_bits - ) + msg = f"Prefix bits must be between 1 and 8, got {prefix_bits}" + raise ValueError(msg) max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits] if integer < max_number: return bytearray([integer]) # Seriously? - else: - elements = [max_number] - integer -= max_number + elements = [max_number] + integer -= max_number - while integer >= 128: - elements.append((integer & 127) + 128) - integer >>= 7 + while integer >= 128: + elements.append((integer & 127) + 128) + integer >>= 7 - elements.append(integer) + elements.append(integer) - return bytearray(elements) + return bytearray(elements) def decode_integer(data: bytes, prefix_bits: int) -> tuple[int, int]: """ - This decodes an integer according to the wacky integer encoding rules + Decodes an integer according to the wacky integer encoding rules defined in the HPACK spec. Returns a tuple of the decoded integer and the number of bytes that were consumed from ``data`` in order to get that integer. """ if prefix_bits < 1 or prefix_bits > 8: - raise ValueError( - "Prefix bits must be between 1 and 8, got %s" % prefix_bits - ) + msg = f"Prefix bits must be between 1 and 8, got {prefix_bits}" + raise ValueError(msg) max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits] index = 1 @@ -113,33 +106,35 @@ def decode_integer(data: bytes, prefix_bits: int) -> tuple[int, int]: break shift += 7 - except IndexError: - raise HPACKDecodingError( - "Unable to decode HPACK integer representation from %r" % data - ) + except IndexError as err: + msg = f"Unable to decode HPACK integer representation from {data!r}" + raise HPACKDecodingError(msg) from err log.debug("Decoded %d, consumed %d bytes", number, index) return number, index -def _dict_to_iterable(header_dict: Union[dict[bytes, bytes], dict[str, str]]) \ - -> Generator[Union[tuple[bytes, bytes, Optional[bool]], tuple[str, str, Optional[bool]]], None, None]: +def _dict_to_iterable(header_dict: dict[bytes | str, bytes | str]) \ + -> Iterable[tuple[bytes | str, bytes | str]]: """ - This converts a dictionary to an iterable of two-tuples. This is a + Converts a dictionary to an iterable of key-value tuples. This is a HPACK-specific function because it pulls "special-headers" out first and then emits them. """ - assert isinstance(header_dict, dict) + if not isinstance(header_dict, dict): # pragma: no cover + msg = f"header_dict not a dict, but {type(header_dict)}" + raise TypeError(msg) + keys = sorted( header_dict.keys(), - key=lambda k: not _to_bytes(k).startswith(b':') + key=lambda k: not _to_bytes(k).startswith(b":"), ) for key in keys: - yield key, header_dict[key] # type: ignore + yield key, header_dict[key] -def _to_bytes(value: Union[bytes, str, Any]) -> bytes: +def _to_bytes(value: bytes | str | Any) -> bytes: """ Convert anything to bytes through a UTF-8 encoded string """ @@ -160,7 +155,7 @@ class Encoder: def __init__(self) -> None: self.header_table = HeaderTable() self.huffman_coder = HuffmanEncoder( - REQUEST_CODES, REQUEST_CODES_LENGTH + REQUEST_CODES, REQUEST_CODES_LENGTH, ) self.table_size_changes: list[int] = [] @@ -178,7 +173,11 @@ def header_table_size(self, value: int) -> None: self.table_size_changes.append(value) def encode(self, - headers: Union[Iterable[tuple[Union[bytes, str], Union[bytes, str], Optional[bool]]], dict[Union[bytes, str], Union[bytes, str, tuple]]], + headers: Iterable[\ + HeaderTuple | \ + tuple[bytes | str, bytes | str] | \ + tuple[bytes | str, bytes | str, bool | None]] | \ + dict[bytes | str, bytes | str], huffman: bool = True) -> bytes: """ Takes a set of headers and encodes them into a HPACK-encoded header @@ -231,30 +230,52 @@ def encode(self, # table. Otherwise, a literal representation will be used. header_block = [] - # Turn the headers into a list of tuples if possible. This is the - # natural way to interact with them in HPACK. Because dictionaries are - # un-ordered, we need to make sure we grab the "special" headers first. - if isinstance(headers, dict): - headers = _dict_to_iterable(headers) - # Before we begin, if the header table size has been changed we need # to signal all changes since last emission appropriately. if self.header_table.resized: header_block.append(self._encode_table_size_change()) self.header_table.resized = False + if isinstance(headers, dict): + # Turn the headers into a list of tuples if possible. This is the + # natural way to interact with them in HPACK. Because dictionaries are + # un-ordered, we need to make sure we grab the "special" headers first. + hpack_headers = _dict_to_iterable(headers) + else: + """ + Assume headers is an iterable of HeaderTuples, or plain 2-tuples, or plain 3-tuples: + + examples: + [ + HeaderTuple(':method', 'GET'), + NeverIndexedHeaderTuple('customkey', 'sensitiveinfo'), + ] + or + [ + (':method', 'GET'), + ('customkey', 'some-data'), + ] + or + [ + (':method', 'GET', True), + ('customkey', 'sensitiveinfo', True), + ] + """ + hpack_headers = iter(headers) # type: ignore + # Add each header to the header block - for header in headers: + for header in hpack_headers: sensitive = False if isinstance(header, HeaderTuple): + # HeaderTuple implies it's a 2-tuple with the sensitive information stored as instance attribute sensitive = not header.indexable elif len(header) > 2: sensitive = header[2] - header = (_to_bytes(header[0]), _to_bytes(header[1])) - header_block.append(self.add(header, sensitive, huffman)) + new_header = (_to_bytes(header[0]), _to_bytes(header[1])) + header_block.append(self.add(new_header, sensitive, huffman)) - encoded = b''.join(header_block) + encoded = b"".join(header_block) log.debug("Encoded header block to %s", encoded) @@ -262,13 +283,13 @@ def encode(self, def add(self, to_add: tuple[bytes, bytes], sensitive: bool, huffman: bool = False) -> bytes: """ - This function takes a header key-value tuple and serializes it. + Serializes a header key-value tuple. """ log.debug( "Adding %s to the header table, sensitive:%s, huffman:%s", to_add, sensitive, - huffman + huffman, ) name, value = to_add @@ -302,7 +323,7 @@ def add(self, to_add: tuple[bytes, bytes], sensitive: bool, huffman: bool = Fals # indexing since they just take space in the table and # pushed out other valuable headers. encoded = self._encode_indexed_literal( - index, value, indexbit, huffman + index, value, indexbit, huffman, ) if not sensitive: self.header_table.add(name, value) @@ -334,8 +355,8 @@ def _encode_literal(self, name: bytes, value: bytes, indexbit: bytes, huffman: b name_len[0] |= 0x80 value_len[0] |= 0x80 - return b''.join( - [indexbit, bytes(name_len), name, bytes(value_len), value] + return b"".join( + [indexbit, bytes(name_len), name, bytes(value_len), value], ) def _encode_indexed_literal(self, index: int, value: bytes, indexbit: bytes, huffman: bool = False) -> bytes: @@ -358,14 +379,14 @@ def _encode_indexed_literal(self, index: int, value: bytes, indexbit: bytes, huf if huffman: value_len[0] |= 0x80 - return b''.join([bytes(prefix), bytes(value_len), value]) + return b"".join([bytes(prefix), bytes(value_len), value]) def _encode_table_size_change(self) -> bytes: """ Produces the encoded form of all header table size change context updates. """ - block = b'' + block = b"" for size_bytes in self.table_size_changes: b = encode_integer(size_bytes, 5) b[0] |= 0x20 @@ -395,6 +416,7 @@ class Decoder: Defaults to 64kB. :type max_header_list_size: ``int`` """ + def __init__(self, max_header_list_size: int = DEFAULT_MAX_HEADER_LIST_SIZE) -> None: self.header_table = HeaderTable() @@ -461,40 +483,39 @@ def decode(self, data: bytes, raw: bool = False) -> Iterable[HeaderTuple]: # Work out what kind of header we're decoding. # If the high bit is 1, it's an indexed field. current = data[current_index] - indexed = True if current & 0x80 else False + indexed = bool(current & 0x80) # Otherwise, if the second-highest bit is 1 it's a field that does # alter the header table. - literal_index = True if current & 0x40 else False + literal_index = bool(current & 0x40) # Otherwise, if the third-highest bit is 1 it's an encoding context # update. - encoding_update = True if current & 0x20 else False + encoding_update = bool(current & 0x20) if indexed: header, consumed = self._decode_indexed( - data_mem[current_index:] + data_mem[current_index:], ) elif literal_index: # It's a literal header that does affect the header table. header, consumed = self._decode_literal_index( - data_mem[current_index:] + data_mem[current_index:], ) elif encoding_update: # It's an update to the encoding context. These are forbidden # in a header block after any actual header. if headers: - raise HPACKDecodingError( - "Table size update not at the start of the block" - ) + msg = "Table size update not at the start of the block" + raise HPACKDecodingError(msg) consumed = self._update_encoding_context( - data_mem[current_index:] + data_mem[current_index:], ) header = None else: # It's a literal header that does not affect the header table. header, consumed = self._decode_literal_no_index( - data_mem[current_index:] + data_mem[current_index:], ) if header: @@ -502,10 +523,8 @@ def decode(self, data: bytes, raw: bool = False) -> Iterable[HeaderTuple]: inflated_size += table_entry_size(header[0], header[1]) if inflated_size > self.max_header_list_size: - raise OversizedHeaderListError( - "A header list larger than %d has been received" % - self.max_header_list_size - ) + msg = f"A header list larger than {self.max_header_list_size} has been received" + raise OversizedHeaderListError(msg) current_index += consumed @@ -516,8 +535,9 @@ def decode(self, data: bytes, raw: bool = False) -> Iterable[HeaderTuple]: try: return [_unicode_if_needed(h, raw) for h in headers] - except UnicodeDecodeError: - raise HPACKDecodingError("Unable to decode headers as UTF-8.") + except UnicodeDecodeError as err: + msg = "Unable to decode headers as UTF-8" + raise HPACKDecodingError(msg) from err def _assert_valid_table_size(self) -> None: """ @@ -525,9 +545,8 @@ def _assert_valid_table_size(self) -> None: we expect to have. """ if self.header_table_size > self.max_allowed_table_size: - raise InvalidTableSizeError( - "Encoder did not shrink table size to within the max" - ) + msg = "Encoder did not shrink table size to within the max" + raise InvalidTableSizeError(msg) def _update_encoding_context(self, data: bytes) -> int: """ @@ -536,9 +555,8 @@ def _update_encoding_context(self, data: bytes) -> int: # We've been asked to resize the header table. new_size, consumed = decode_integer(data, 5) if new_size > self.max_allowed_table_size: - raise InvalidTableSizeError( - "Encoder exceeded max allowable table size" - ) + msg = "Encoder exceeded max allowable table size" + raise InvalidTableSizeError(msg) self.header_table_size = new_size return consumed @@ -552,10 +570,10 @@ def _decode_indexed(self, data: bytes) -> tuple[HeaderTuple, int]: return header, consumed def _decode_literal_no_index(self, data: bytes) -> tuple[HeaderTuple, int]: - return self._decode_literal(data, False) + return self._decode_literal(data, should_index=False) def _decode_literal_index(self, data: bytes) -> tuple[HeaderTuple, int]: - return self._decode_literal(data, True) + return self._decode_literal(data, should_index=True) def _decode_literal(self, data: bytes, should_index: bool) -> tuple[HeaderTuple, int]: """ @@ -592,7 +610,8 @@ def _decode_literal(self, data: bytes, should_index: bool) -> tuple[HeaderTuple, length, consumed = decode_integer(data, 7) name = data[consumed:consumed + length] if len(name) != length: - raise HPACKDecodingError("Truncated header block") + msg = "Truncated header block" + raise HPACKDecodingError(msg) if data[0] & 0x80: name = decode_huffman(name) @@ -604,7 +623,8 @@ def _decode_literal(self, data: bytes, should_index: bool) -> tuple[HeaderTuple, length, consumed = decode_integer(data, 7) value = data[consumed:consumed + length] if len(value) != length: - raise HPACKDecodingError("Truncated header block") + msg = "Truncated header block" + raise HPACKDecodingError(msg) if data[0] & 0x80: value = decode_huffman(value) @@ -628,7 +648,7 @@ def _decode_literal(self, data: bytes, should_index: bool) -> tuple[HeaderTuple, "Decoded %s, total consumed %d bytes, indexed %s", header, total_consumed, - should_index + should_index, ) return header, total_consumed diff --git a/src/hpack/huffman.py b/src/hpack/huffman.py index 14f094e..f5b06c5 100644 --- a/src/hpack/huffman.py +++ b/src/hpack/huffman.py @@ -1,13 +1,8 @@ """ -hpack/huffman_decoder -~~~~~~~~~~~~~~~~~~~~~ - An implementation of a bitwise prefix tree specially built for decoding Huffman-coded content where we already know the Huffman table. """ - - -from typing import Optional +from __future__ import annotations class HuffmanEncoder: @@ -15,18 +10,19 @@ class HuffmanEncoder: Encodes a string according to the Huffman encoding table defined in the HPACK specification. """ + def __init__(self, huffman_code_list: list[int], huffman_code_list_lengths: list[int]) -> None: self.huffman_code_list = huffman_code_list self.huffman_code_list_lengths = huffman_code_list_lengths - def encode(self, bytes_to_encode: Optional[bytes]) -> bytes: + def encode(self, bytes_to_encode: bytes | None) -> bytes: """ Given a string of bytes, encodes them according to the HPACK Huffman specification. """ # If handed the empty string, just immediately return. if not bytes_to_encode: - return b'' + return b"" final_num = 0 final_int_len = 0 @@ -50,10 +46,10 @@ def encode(self, bytes_to_encode: Optional[bytes]) -> bytes: # Convert the number to hex and strip off the leading '0x' and the # trailing 'L', if present. - s = hex(final_num)[2:].rstrip('L') + s = hex(final_num)[2:].rstrip("L") # If this is odd, prepend a zero. - s = '0' + s if len(s) % 2 != 0 else s + s = "0" + s if len(s) % 2 != 0 else s # This number should have twice as many digits as bytes. If not, we're # missing some leading zeroes. Work out how many bytes we want and how @@ -63,6 +59,6 @@ def encode(self, bytes_to_encode: Optional[bytes]) -> bytes: if len(s) != expected_digits: missing_digits = expected_digits - len(s) - s = ('0' * missing_digits) + s + s = ("0" * missing_digits) + s return bytes.fromhex(s) diff --git a/src/hpack/huffman_constants.py b/src/hpack/huffman_constants.py index 535ddac..146ee79 100644 --- a/src/hpack/huffman_constants.py +++ b/src/hpack/huffman_constants.py @@ -1,7 +1,4 @@ """ -hpack/huffman_constants -~~~~~~~~~~~~~~~~~~~~~~~ - Defines the constant Huffman table. This takes up an upsetting amount of space, but c'est la vie. """ diff --git a/src/hpack/huffman_table.py b/src/hpack/huffman_table.py index 69191f4..d6b875a 100644 --- a/src/hpack/huffman_table.py +++ b/src/hpack/huffman_table.py @@ -1,13 +1,11 @@ """ -hpack/huffman_table -~~~~~~~~~~~~~~~~~~~ - -This implementation of a Huffman decoding table for HTTP/2 is essentially a -Python port of the work originally done for nghttp2's Huffman decoding. For -this reason, while this file is made available under the MIT license as is the -rest of this module, this file is undoubtedly a derivative work of the nghttp2 -file ``nghttp2_hd_huffman_data.c``, obtained from -https://github.com/tatsuhiro-t/nghttp2/ at commit +Implementation of a Huffman decoding table for HTTP/2. + +This is essentially a Python port of the work originally done for nghttp2's +Huffman decoding. For this reason, while this file is made available under the +MIT license as is the rest of this module, this file is undoubtedly a +derivative work of the nghttp2 file ``nghttp2_hd_huffman_data.c``, obtained +from https://github.com/tatsuhiro-t/nghttp2/ at commit d2b55ad1a245e1d1964579fa3fac36ebf3939e72. That work is made available under the Apache 2.0 license under the following terms: @@ -69,7 +67,7 @@ loops at the Python-level is not too expensive. The total number of loop iterations is 4x the number of bytes passed to the decoder. """ -from typing import Optional, Union +from __future__ import annotations from .exceptions import HPACKDecodingError @@ -77,13 +75,13 @@ # This defines the state machine "class" at the top of the file. The reason we # do this is to keep the terrifing monster state table at the *bottom* of the # file so you don't have to actually *look* at the damn thing. -def decode_huffman(huffman_string: Optional[Union[bytes, bytearray]]) -> bytes: +def decode_huffman(huffman_string: bytes | bytearray | None) -> bytes: """ Given a bytestring of Huffman-encoded data for HPACK, returns a bytestring of the decompressed data. """ if not huffman_string: - return b'' + return b"" state = 0 flags = 0 @@ -103,7 +101,8 @@ def decode_huffman(huffman_string: Optional[Union[bytes, bytearray]]) -> bytes: state, flags, output_byte = HUFFMAN_TABLE[index] if flags & HUFFMAN_FAIL: - raise HPACKDecodingError("Invalid Huffman String") + msg = "Invalid Huffman string" + raise HPACKDecodingError(msg) if flags & HUFFMAN_EMIT_SYMBOL: decoded_bytes.append(output_byte) @@ -112,13 +111,15 @@ def decode_huffman(huffman_string: Optional[Union[bytes, bytearray]]) -> bytes: state, flags, output_byte = HUFFMAN_TABLE[index] if flags & HUFFMAN_FAIL: - raise HPACKDecodingError("Invalid Huffman String") + msg = "Invalid Huffman string" + raise HPACKDecodingError(msg) if flags & HUFFMAN_EMIT_SYMBOL: decoded_bytes.append(output_byte) if not (flags & HUFFMAN_COMPLETE): - raise HPACKDecodingError("Incomplete Huffman string") + msg = "Incomplete Huffman string" + raise HPACKDecodingError(msg) return bytes(decoded_bytes) diff --git a/src/hpack/struct.py b/src/hpack/struct.py index afb1e19..420c634 100644 --- a/src/hpack/struct.py +++ b/src/hpack/struct.py @@ -1,13 +1,12 @@ """ -hpack/struct -~~~~~~~~~~~~ - Contains structures for representing header fields with associated metadata. """ +from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any -from typing_extensions import Self, TypeAlias +if TYPE_CHECKING: + from typing_extensions import Self, TypeAlias # pragma: no cover class HeaderTuple(tuple[bytes, bytes]): @@ -24,6 +23,7 @@ class HeaderTuple(tuple[bytes, bytes]): This class stores a header that can be added to the compression context. In all other ways it behaves exactly like a tuple. """ + __slots__ = () indexable = True @@ -37,6 +37,7 @@ class NeverIndexedHeaderTuple(HeaderTuple): A data structure that stores a single header field that cannot be added to a HTTP/2 header compression context. """ + __slots__ = () indexable = False