New Upstream Release - python-hyperframe

Ready changes

Summary

Merged new upstream version: 6.0.1 (was: 6.0.0).

Resulting package

Built on 2022-12-30T23:58 (took 2m29s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-hyperframe

Lintian Result

Diff

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 37bac4a..da4061c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,8 +1,16 @@
 Release History
 ===============
 
+6.0.1 (2021-04-17)
+------------------
+
+**API Changes (Backward-compatible)**
+
+- Added support for Python 3.9.
+- Added type hints.
+
 6.0.0 (2020-09-06)
----------
+------------------
 
 **API Changes (Backward-incompatible)**
 
diff --git a/MANIFEST.in b/MANIFEST.in
index e7c846d..200507a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,4 +4,5 @@ graft test
 prune docs/build
 prune test/http2-frame-test-case
 include README.rst LICENSE CHANGELOG.rst CONTRIBUTORS.rst tox.ini .gitmodules
+include src/hyperframe/py.typed
 global-exclude *.pyc *.pyo *.swo *.swp *.map *.yml *.DS_Store
diff --git a/PKG-INFO b/PKG-INFO
index c82bcad..549b385 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: hyperframe
-Version: 6.0.0
+Version: 6.0.1
 Summary: HTTP/2 framing layer for Python
 Home-page: https://github.com/python-hyper/hyperframe/
 Author: Cory Benfield
diff --git a/debian/changelog b/debian/changelog
index d84e720..16e421f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-hyperframe (6.0.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 30 Dec 2022 23:56:08 -0000
+
 python-hyperframe (6.0.0-1) unstable; urgency=medium
 
   * Maintain within the Python team (Closes: #947013).
diff --git a/setup.cfg b/setup.cfg
index 7cc77fa..26f7720 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,6 +20,11 @@ source =
 [flake8]
 max-line-length = 120
 
+[check-manifest]
+ignore = 
+	Makefile
+	test/http2-frame-test-case
+
 [egg_info]
 tag_build = 
 tag_date = 0
diff --git a/setup.py b/setup.py
index 7a5baa1..165e40b 100644
--- a/setup.py
+++ b/setup.py
@@ -23,11 +23,6 @@ with open(os.path.join(PROJECT_ROOT, 'src/hyperframe/__init__.py')) as file_:
     else:
         raise RuntimeError("No version number found!")
 
-# Stealing this from Kenneth Reitz
-if sys.argv[-1] == 'publish':
-    os.system('python setup.py sdist upload')
-    sys.exit()
-
 setup(
     name='hyperframe',
     version=version,
@@ -38,7 +33,7 @@ setup(
     author_email='cory@lukasa.co.uk',
     url='https://github.com/python-hyper/hyperframe/',
     packages=find_packages(where="src"),
-    package_data={'': ['LICENSE', 'README.rst', 'CHANGELOG.rst']},
+    package_data={'': ['LICENSE', 'README.rst', 'CHANGELOG.rst'], "hyperframe": ["py.typed"]},
     package_dir={'': 'src'},
     python_requires='>=3.6.1',
     include_package_data=True,
diff --git a/src/hyperframe.egg-info/PKG-INFO b/src/hyperframe.egg-info/PKG-INFO
index c82bcad..549b385 100644
--- a/src/hyperframe.egg-info/PKG-INFO
+++ b/src/hyperframe.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: hyperframe
-Version: 6.0.0
+Version: 6.0.1
 Summary: HTTP/2 framing layer for Python
 Home-page: https://github.com/python-hyper/hyperframe/
 Author: Cory Benfield
diff --git a/src/hyperframe.egg-info/SOURCES.txt b/src/hyperframe.egg-info/SOURCES.txt
index e0201c8..952449c 100644
--- a/src/hyperframe.egg-info/SOURCES.txt
+++ b/src/hyperframe.egg-info/SOURCES.txt
@@ -17,6 +17,7 @@ src/hyperframe/__init__.py
 src/hyperframe/exceptions.py
 src/hyperframe/flags.py
 src/hyperframe/frame.py
+src/hyperframe/py.typed
 src/hyperframe.egg-info/PKG-INFO
 src/hyperframe.egg-info/SOURCES.txt
 src/hyperframe.egg-info/dependency_links.txt
diff --git a/src/hyperframe/__init__.py b/src/hyperframe/__init__.py
index 885a77e..bf5ba1c 100644
--- a/src/hyperframe/__init__.py
+++ b/src/hyperframe/__init__.py
@@ -5,4 +5,4 @@ hyperframe
 
 A module for providing a pure-Python HTTP/2 framing layer.
 """
-__version__ = '6.0.0'
+__version__ = '6.0.1'
diff --git a/src/hyperframe/exceptions.py b/src/hyperframe/exceptions.py
index 3d41468..d6adab2 100644
--- a/src/hyperframe/exceptions.py
+++ b/src/hyperframe/exceptions.py
@@ -22,14 +22,14 @@ class UnknownFrameError(HyperframeError):
     .. versionchanged:: 6.0.0
         Changed base class from `ValueError` to :class:`HyperframeError`
     """
-    def __init__(self, frame_type, length):
+    def __init__(self, frame_type: int, length: int) -> None:
         #: The type byte of the unknown frame that was received.
         self.frame_type = frame_type
 
         #: The length of the data portion of the unknown frame.
         self.length = length
 
-    def __str__(self):
+    def __str__(self) -> str:
         return (
             "UnknownFrameError: Unknown frame type 0x%X received, "
             "length %d bytes" % (self.frame_type, self.length)
diff --git a/src/hyperframe/flags.py b/src/hyperframe/flags.py
index f3933f8..f00cb99 100644
--- a/src/hyperframe/flags.py
+++ b/src/hyperframe/flags.py
@@ -5,13 +5,16 @@ hyperframe/flags
 
 Defines basic Flag and Flags data structures.
 """
-import collections
 from collections.abc import MutableSet
+from typing import NamedTuple, Iterable, Set, Iterator
 
-Flag = collections.namedtuple("Flag", ["name", "bit"])
 
+class Flag(NamedTuple):
+    name: str
+    bit: int
 
-class Flags(MutableSet):
+
+class Flags(MutableSet):  # type: ignore
     """
     A simple MutableSet implementation that will only accept known flags as
     elements.
@@ -19,26 +22,26 @@ class Flags(MutableSet):
     Will behave like a regular set(), except that a ValueError will be thrown
     when .add()ing unexpected flags.
     """
-    def __init__(self, defined_flags):
+    def __init__(self, defined_flags: Iterable[Flag]):
         self._valid_flags = set(flag.name for flag in defined_flags)
-        self._flags = set()
+        self._flags: Set[str] = set()
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return repr(sorted(list(self._flags)))
 
-    def __contains__(self, x):
+    def __contains__(self, x: object) -> bool:
         return self._flags.__contains__(x)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[str]:
         return self._flags.__iter__()
 
-    def __len__(self):
+    def __len__(self) -> int:
         return self._flags.__len__()
 
-    def discard(self, value):
+    def discard(self, value: str) -> None:
         return self._flags.discard(value)
 
-    def add(self, value):
+    def add(self, value: str) -> None:
         if value not in self._valid_flags:
             raise ValueError(
                 "Unexpected flag: {}. Valid flags are: {}".format(
diff --git a/src/hyperframe/frame.py b/src/hyperframe/frame.py
index d9b3ecf..f4e75e3 100644
--- a/src/hyperframe/frame.py
+++ b/src/hyperframe/frame.py
@@ -14,6 +14,7 @@ from .exceptions import (
     UnknownFrameError, InvalidPaddingError, InvalidFrameError, InvalidDataError
 )
 from .flags import Flag, Flags
+from typing import Optional, Tuple, List, Iterable, Any, Dict, Type
 
 
 # The maximum initial length of a frame. Some frames have shorter maximum
@@ -43,16 +44,16 @@ class Frame:
     The base class for all HTTP/2 frames.
     """
     #: The flags defined on this type of frame.
-    defined_flags = []
+    defined_flags: List[Flag] = []
 
     #: The byte used to define the type of the frame.
-    type = None
+    type: Optional[int] = None
 
     # If 'has-stream', the frame's stream_id must be non-zero. If 'no-stream',
     # it must be zero. If 'either', it's not checked.
-    stream_association = None
+    stream_association: Optional[str] = None
 
-    def __init__(self, stream_id, flags=()):
+    def __init__(self, stream_id: int, flags: Iterable[str] = ()) -> None:
         #: The stream identifier for the stream this frame was received on.
         #: Set to 0 for frames sent on the connection (stream-id 0).
         self.stream_id = stream_id
@@ -82,7 +83,7 @@ class Frame:
                 )
             )
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return (
             "{}(stream_id={}, flags={}): {}"
         ).format(
@@ -92,13 +93,13 @@ class Frame:
             self._body_repr(),
         )
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         # More specific implementation may be provided by subclasses of Frame.
         # This fallback shows the serialized (and truncated) body content.
         return _raw_data_repr(self.serialize_body())
 
     @staticmethod
-    def explain(data):
+    def explain(data: memoryview) -> Tuple["Frame", int]:
         """
         Takes a bytestring and tries to parse a single frame and print it.
 
@@ -115,7 +116,7 @@ class Frame:
         return frame, length
 
     @staticmethod
-    def parse_frame_header(header, strict=False):
+    def parse_frame_header(header: memoryview, strict: bool = False) -> Tuple["Frame", int]:
         """
         Takes a 9-byte frame header and returns a tuple of the appropriate
         Frame object and the length that needs to be read from the socket.
@@ -155,14 +156,14 @@ class Frame:
         frame.parse_flags(flags)
         return (frame, length)
 
-    def parse_flags(self, flag_byte):
+    def parse_flags(self, flag_byte: int) -> Flags:
         for flag, flag_bit in self.defined_flags:
             if flag_byte & flag_bit:
                 self.flags.add(flag)
 
         return self.flags
 
-    def serialize(self):
+    def serialize(self) -> bytes:
         """
         Convert a frame into a bytestring, representing the serialized form of
         the frame.
@@ -188,10 +189,10 @@ class Frame:
 
         return header + body
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         raise NotImplementedError()
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         """
         Given the body of a frame, parses it into frame data. This populates
         the non-header parts of the frame: that is, it does not populate the
@@ -210,19 +211,19 @@ class Padding:
     Mixin for frames that contain padding. Defines extra fields that can be
     used and set by frames that can be padded.
     """
-    def __init__(self, stream_id, pad_length=0, **kwargs):
-        super().__init__(stream_id, **kwargs)
+    def __init__(self, stream_id: int, pad_length: int = 0, **kwargs: Any) -> None:
+        super().__init__(stream_id, **kwargs)  # type: ignore
 
         #: The length of the padding to use.
         self.pad_length = pad_length
 
-    def serialize_padding_data(self):
-        if 'PADDED' in self.flags:
+    def serialize_padding_data(self) -> bytes:
+        if 'PADDED' in self.flags:  # type: ignore
             return _STRUCT_B.pack(self.pad_length)
         return b''
 
-    def parse_padding_data(self, data):
-        if 'PADDED' in self.flags:
+    def parse_padding_data(self, data: memoryview) -> int:
+        if 'PADDED' in self.flags:  # type: ignore
             try:
                 self.pad_length = struct.unpack('!B', data[:1])[0]
             except struct.error:
@@ -233,7 +234,7 @@ class Padding:
     #: .. deprecated:: 5.2.1
     #:    Use self.pad_length instead.
     @property
-    def total_padding(self):  # pragma: no cover
+    def total_padding(self) -> int:  # pragma: no cover
         import warnings
         warnings.warn(
             "total_padding contains the same information as pad_length.",
@@ -248,12 +249,12 @@ class Priority:
     be used and set by frames that contain priority data.
     """
     def __init__(self,
-                 stream_id,
-                 depends_on=0x0,
-                 stream_weight=0x0,
-                 exclusive=False,
-                 **kwargs):
-        super().__init__(stream_id, **kwargs)
+                 stream_id: int,
+                 depends_on: int = 0x0,
+                 stream_weight: int = 0x0,
+                 exclusive: bool = False,
+                 **kwargs: Any) -> None:
+        super().__init__(stream_id, **kwargs)  # type: ignore
 
         #: The stream ID of the stream on which this stream depends.
         self.depends_on = depends_on
@@ -264,13 +265,13 @@ class Priority:
         #: Whether the exclusive bit was set.
         self.exclusive = exclusive
 
-    def serialize_priority_data(self):
+    def serialize_priority_data(self) -> bytes:
         return _STRUCT_LB.pack(
             self.depends_on + (0x80000000 if self.exclusive else 0),
             self.stream_weight
         )
 
-    def parse_priority_data(self, data):
+    def parse_priority_data(self, data: memoryview) -> int:
         try:
             self.depends_on, self.stream_weight = _STRUCT_LB.unpack(data[:5])
         except struct.error:
@@ -298,20 +299,20 @@ class DataFrame(Padding, Frame):
 
     stream_association = _STREAM_ASSOC_HAS_STREAM
 
-    def __init__(self, stream_id, data=b'', **kwargs):
+    def __init__(self, stream_id: int, data: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The data contained on this frame.
         self.data = data
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         padding_data = self.serialize_padding_data()
         padding = b'\0' * self.pad_length
         if isinstance(self.data, memoryview):
             self.data = self.data.tobytes()
         return b''.join([padding_data, self.data, padding])
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         padding_data_length = self.parse_padding_data(data)
         self.data = (
             data[padding_data_length:len(data)-self.pad_length].tobytes()
@@ -322,7 +323,7 @@ class DataFrame(Padding, Frame):
             raise InvalidPaddingError("Padding is too long.")
 
     @property
-    def flow_controlled_length(self):
+    def flow_controlled_length(self) -> int:
         """
         The length of the frame that needs to be accounted for when considering
         flow control.
@@ -342,24 +343,24 @@ class PriorityFrame(Priority, Frame):
     reprioritisation of existing streams.
     """
     #: The flags defined for PRIORITY frames.
-    defined_flags = []
+    defined_flags: List[Flag] = []
 
     #: The type byte defined for PRIORITY frames.
     type = 0x02
 
     stream_association = _STREAM_ASSOC_HAS_STREAM
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "exclusive={}, depends_on={}, stream_weight={}".format(
             self.exclusive,
             self.depends_on,
             self.stream_weight
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         return self.serialize_priority_data()
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         if len(data) > 5:
             raise InvalidFrameError(
                 "PRIORITY must have 5 byte body: actual length %s." %
@@ -380,28 +381,28 @@ class RstStreamFrame(Frame):
     occurred.
     """
     #: The flags defined for RST_STREAM frames.
-    defined_flags = []
+    defined_flags: List[Flag] = []
 
     #: The type byte defined for RST_STREAM frames.
     type = 0x03
 
     stream_association = _STREAM_ASSOC_HAS_STREAM
 
-    def __init__(self, stream_id, error_code=0, **kwargs):
+    def __init__(self, stream_id: int, error_code: int = 0, **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The error code used when resetting the stream.
         self.error_code = error_code
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "error_code={}".format(
             self.error_code,
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         return _STRUCT_L.pack(self.error_code)
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         if len(data) != 4:
             raise InvalidFrameError(
                 "RST_STREAM must have 4 byte body: actual length %s." %
@@ -453,7 +454,7 @@ class SettingsFrame(Frame):
     #: The byte that signals SETTINGS_ENABLE_CONNECT_PROTOCOL setting.
     ENABLE_CONNECT_PROTOCOL = 0x08
 
-    def __init__(self, stream_id=0, settings=None, **kwargs):
+    def __init__(self, stream_id: int = 0, settings: Optional[Dict[int, int]] = None, **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         if settings and "ACK" in kwargs.get("flags", ()):
@@ -464,16 +465,16 @@ class SettingsFrame(Frame):
         #: A dictionary of the setting type byte to the value of the setting.
         self.settings = settings or {}
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "settings={}".format(
             self.settings,
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         return b''.join([_STRUCT_HL.pack(setting & 0xFF, value)
                          for setting, value in self.settings.items()])
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         if 'ACK' in self.flags and len(data) > 0:
             raise InvalidDataError(
                 "SETTINGS ack frame must not have payload: got %s bytes" %
@@ -509,7 +510,7 @@ class PushPromiseFrame(Padding, Frame):
 
     stream_association = _STREAM_ASSOC_HAS_STREAM
 
-    def __init__(self, stream_id, promised_stream_id=0, data=b'', **kwargs):
+    def __init__(self, stream_id: int, promised_stream_id: int = 0, data: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The stream ID that is promised by this frame.
@@ -519,19 +520,19 @@ class PushPromiseFrame(Padding, Frame):
         #: stream.
         self.data = data
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "promised_stream_id={}, data={}".format(
             self.promised_stream_id,
             _raw_data_repr(self.data),
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         padding_data = self.serialize_padding_data()
         padding = b'\0' * self.pad_length
         data = _STRUCT_L.pack(self.promised_stream_id)
         return b''.join([padding_data, data, self.data, padding])
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         padding_data_length = self.parse_padding_data(data)
 
         try:
@@ -570,21 +571,21 @@ class PingFrame(Frame):
 
     stream_association = _STREAM_ASSOC_NO_STREAM
 
-    def __init__(self, stream_id=0, opaque_data=b'', **kwargs):
+    def __init__(self, stream_id: int = 0, opaque_data: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The opaque data sent in this PING frame, as a bytestring.
         self.opaque_data = opaque_data
 
-    def _body_repr(self):
-        return "opaque_data={}".format(
+    def _body_repr(self) -> str:
+        return "opaque_data={!r}".format(
             self.opaque_data,
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         if len(self.opaque_data) > 8:
             raise InvalidFrameError(
-                "PING frame may not have more than 8 bytes of data, got %s" %
+                "PING frame may not have more than 8 bytes of data, got %r" %
                 self.opaque_data
             )
 
@@ -592,7 +593,7 @@ class PingFrame(Frame):
         data += b'\x00' * (8 - len(self.opaque_data))
         return data
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         if len(data) != 8:
             raise InvalidFrameError(
                 "PING frame must have 8 byte length: got %s" % len(data)
@@ -610,7 +611,7 @@ class GoAwayFrame(Frame):
     connection.
     """
     #: The flags defined for GOAWAY frames.
-    defined_flags = []
+    defined_flags: List[Flag] = []
 
     #: The type byte defined for GOAWAY frames.
     type = 0x07
@@ -618,11 +619,11 @@ class GoAwayFrame(Frame):
     stream_association = _STREAM_ASSOC_NO_STREAM
 
     def __init__(self,
-                 stream_id=0,
-                 last_stream_id=0,
-                 error_code=0,
-                 additional_data=b'',
-                 **kwargs):
+                 stream_id: int = 0,
+                 last_stream_id: int = 0,
+                 error_code: int = 0,
+                 additional_data: bytes = b'',
+                 **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The last stream ID definitely seen by the remote peer.
@@ -634,14 +635,14 @@ class GoAwayFrame(Frame):
         #: Any additional data sent in the GOAWAY.
         self.additional_data = additional_data
 
-    def _body_repr(self):
-        return "last_stream_id={}, error_code={}, additional_data={}".format(
+    def _body_repr(self) -> str:
+        return "last_stream_id={}, error_code={}, additional_data={!r}".format(
             self.last_stream_id,
             self.error_code,
             self.additional_data,
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         data = _STRUCT_LL.pack(
             self.last_stream_id & 0x7FFFFFFF,
             self.error_code
@@ -650,7 +651,7 @@ class GoAwayFrame(Frame):
 
         return data
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         try:
             self.last_stream_id, self.error_code = _STRUCT_LL.unpack(
                 data[:8]
@@ -678,28 +679,28 @@ class WindowUpdateFrame(Frame):
     original sender.
     """
     #: The flags defined for WINDOW_UPDATE frames.
-    defined_flags = []
+    defined_flags: List[Flag] = []
 
     #: The type byte defined for WINDOW_UPDATE frames.
     type = 0x08
 
     stream_association = _STREAM_ASSOC_EITHER
 
-    def __init__(self, stream_id, window_increment=0, **kwargs):
+    def __init__(self, stream_id: int, window_increment: int = 0, **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The amount the flow control window is to be incremented.
         self.window_increment = window_increment
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "window_increment={}".format(
             self.window_increment,
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         return _STRUCT_L.pack(self.window_increment & 0x7FFFFFFF)
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         if len(data) > 4:
             raise InvalidFrameError(
                 "WINDOW_UPDATE frame must have 4 byte length: got %s" %
@@ -744,13 +745,13 @@ class HeadersFrame(Padding, Priority, Frame):
 
     stream_association = _STREAM_ASSOC_HAS_STREAM
 
-    def __init__(self, stream_id, data=b'', **kwargs):
+    def __init__(self, stream_id: int, data: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The HPACK-encoded header block.
         self.data = data
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "exclusive={}, depends_on={}, stream_weight={}, data={}".format(
             self.exclusive,
             self.depends_on,
@@ -758,7 +759,7 @@ class HeadersFrame(Padding, Priority, Frame):
             _raw_data_repr(self.data),
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         padding_data = self.serialize_padding_data()
         padding = b'\0' * self.pad_length
 
@@ -769,7 +770,7 @@ class HeadersFrame(Padding, Priority, Frame):
 
         return b''.join([padding_data, priority_data, self.data, padding])
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         padding_data_length = self.parse_padding_data(data)
         data = data[padding_data_length:]
 
@@ -805,21 +806,21 @@ class ContinuationFrame(Frame):
 
     stream_association = _STREAM_ASSOC_HAS_STREAM
 
-    def __init__(self, stream_id, data=b'', **kwargs):
+    def __init__(self, stream_id: int, data: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         #: The HPACK-encoded header block.
         self.data = data
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "data={}".format(
             _raw_data_repr(self.data),
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         return self.data
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         self.data = data.tobytes()
         self.body_len = len(data)
 
@@ -843,7 +844,7 @@ class AltSvcFrame(Frame):
 
     stream_association = _STREAM_ASSOC_EITHER
 
-    def __init__(self, stream_id, origin=b'', field=b'', **kwargs):
+    def __init__(self, stream_id: int, origin: bytes = b'', field: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
 
         if not isinstance(origin, bytes):
@@ -853,17 +854,17 @@ class AltSvcFrame(Frame):
         self.origin = origin
         self.field = field
 
-    def _body_repr(self):
-        return "origin={}, field={}".format(
+    def _body_repr(self) -> str:
+        return "origin={!r}, field={!r}".format(
             self.origin,
             self.field,
         )
 
-    def serialize_body(self):
+    def serialize_body(self) -> bytes:
         origin_len = _STRUCT_H.pack(len(self.origin))
         return b''.join([origin_len, self.origin, self.field])
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         try:
             origin_len = _STRUCT_H.unpack(data[0:2])[0]
             self.origin = data[2:2+origin_len].tobytes()
@@ -896,30 +897,30 @@ class ExtensionFrame(Frame):
 
     stream_association = _STREAM_ASSOC_EITHER
 
-    def __init__(self, type, stream_id, flag_byte=0x0, body=b'', **kwargs):
+    def __init__(self, type: int, stream_id: int, flag_byte: int = 0x0, body: bytes = b'', **kwargs: Any) -> None:
         super().__init__(stream_id, **kwargs)
         self.type = type
         self.flag_byte = flag_byte
         self.body = body
 
-    def _body_repr(self):
+    def _body_repr(self) -> str:
         return "type={}, flag_byte={}, body={}".format(
             self.type,
             self.flag_byte,
             _raw_data_repr(self.body),
         )
 
-    def parse_flags(self, flag_byte):
+    def parse_flags(self, flag_byte: int) -> None:  # type: ignore
         """
         For extension frames, we parse the flags by just storing a flag byte.
         """
         self.flag_byte = flag_byte
 
-    def parse_body(self, data):
+    def parse_body(self, data: memoryview) -> None:
         self.body = data.tobytes()
         self.body_len = len(data)
 
-    def serialize(self):
+    def serialize(self) -> bytes:
         """
         A broad override of the serialize method that ensures that the data
         comes back out exactly as it came in. This should not be used in most
@@ -941,7 +942,7 @@ class ExtensionFrame(Frame):
         return header + self.body
 
 
-def _raw_data_repr(data):
+def _raw_data_repr(data: Optional[bytes]) -> str:
     if not data:
         return "None"
     r = binascii.hexlify(data).decode('ascii')
@@ -950,7 +951,7 @@ def _raw_data_repr(data):
     return "<hex:" + r + ">"
 
 
-_FRAME_CLASSES = [
+_FRAME_CLASSES: List[Type[Frame]] = [
     DataFrame,
     HeadersFrame,
     PriorityFrame,
diff --git a/src/hyperframe/py.typed b/src/hyperframe/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/tox.ini b/tox.ini
index 061e5d4..e9fe970 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,22 +1,23 @@
 [tox]
-envlist = py36, py37, py38, pypy3, lint, docs, packaging
+envlist = py36, py37, py38, py39, pypy3, lint, docs, packaging
 
 [gh-actions]
 python =
     3.6: py36
     3.7: py37
     3.8: py38, lint, docs, packaging
+    3.9: py39
     pypy3: pypy3
 
 [testenv]
 passenv =
     GITHUB_*
 deps =
-    pytest==6.0.1
-    pytest-cov==2.10.1
-    pytest-xdist==2.1.0
+    pytest>=6.0.1,<7
+    pytest-cov>=2.10.1,<3
+    pytest-xdist>=2.2.1,<3
 commands =
-    pytest --cov-report=xml  --cov-report=term --cov=hyperframe {posargs}
+    pytest --cov-report=xml --cov-report=term --cov=hyperframe {posargs}
 
 [testenv:pypy3]
 # temporarily disable coverage testing on PyPy due to performance problems
@@ -25,7 +26,7 @@ commands = pytest {posargs}
 [testenv:docs]
 basepython = python3.8
 deps =
-    sphinx==3.2.1
+    sphinx>=3.5.4,<4
 whitelist_externals = make
 changedir = {toxinidir}/docs
 commands =
@@ -35,15 +36,18 @@ commands =
 [testenv:lint]
 basepython = python3.8
 deps =
-    flake8==3.8.3
-commands = flake8 --max-complexity 10 src test
+    flake8==3.9.1
+    mypy==0.812
+commands =
+    flake8 --max-complexity 10 src test
+    mypy --strict src/
 
 [testenv:packaging]
 basepython = python3.8
 deps =
-    check-manifest==0.42
-    readme-renderer==26.0
-    twine==3.2.0
+    check-manifest==0.46
+    readme-renderer==29.0
+    twine==3.4.1
 whitelist_externals = rm
 commands =
     rm -rf dist/

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe-6.0.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe-6.0.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe-6.0.1.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe/py.typed

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe-6.0.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe-6.0.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/hyperframe-6.0.0.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details