Repository: aaugustin/websockets
Pull request #979 (2021-06-03 20:25:46)

Convert NamedTuple to dataclass


Patch:
diff --git a/src/websockets/client.py b/src/websockets/client.py
index 698228d..e9bc12c 100644
--- a/src/websockets/client.py
+++ b/src/websockets/client.py
@@ -302,7 +302,7 @@ class ClientConnection(Connection):
             try:
                 self.process_response(response)
             except InvalidHandshake as exc:
-                response = response._replace(exception=exc)
+                response.exception = exc
             else:
                 assert self.state == CONNECTING
                 self.set_state(OPEN)
diff --git a/src/websockets/connection.py b/src/websockets/connection.py
index 5052df3..dbdc15b 100644
--- a/src/websockets/connection.py
+++ b/src/websockets/connection.py
@@ -166,7 +166,7 @@ class Connection:
         if not self.expect_continuation_frame:
             raise ProtocolError("unexpected continuation frame")
         self.expect_continuation_frame = not fin
-        self.send_frame(Frame(fin, OP_CONT, data))
+        self.send_frame(Frame(OP_CONT, data, fin))
 
     def send_text(self, data: bytes, fin: bool = True) -> None:
         """
@@ -176,7 +176,7 @@ class Connection:
         if self.expect_continuation_frame:
             raise ProtocolError("expected a continuation frame")
         self.expect_continuation_frame = not fin
-        self.send_frame(Frame(fin, OP_TEXT, data))
+        self.send_frame(Frame(OP_TEXT, data, fin))
 
     def send_binary(self, data: bytes, fin: bool = True) -> None:
         """
@@ -186,7 +186,7 @@ class Connection:
         if self.expect_continuation_frame:
             raise ProtocolError("expected a continuation frame")
         self.expect_continuation_frame = not fin
-        self.send_frame(Frame(fin, OP_BINARY, data))
+        self.send_frame(Frame(OP_BINARY, data, fin))
 
     def send_close(self, code: Optional[int] = None, reason: str = "") -> None:
         """
@@ -201,7 +201,7 @@ class Connection:
             data = b""
         else:
             data = serialize_close(code, reason)
-        self.send_frame(Frame(True, OP_CLOSE, data))
+        self.send_frame(Frame(OP_CLOSE, data))
         # send_frame() guarantees that self.state is OPEN at this point.
         # 7.1.3. The WebSocket Closing Handshake is Started
         self.set_state(CLOSING)
@@ -213,14 +213,14 @@ class Connection:
         Send a ping frame.
 
         """
-        self.send_frame(Frame(True, OP_PING, data))
+        self.send_frame(Frame(OP_PING, data))
 
     def send_pong(self, data: bytes) -> None:
         """
         Send a pong frame.
 
         """
-        self.send_frame(Frame(True, OP_PONG, data))
+        self.send_frame(Frame(OP_PONG, data))
 
     # Public API for getting incoming events after receiving data.
 
@@ -257,7 +257,7 @@ class Connection:
         # sent if it's CLOSING), except when failing the connection because of
         # an error reading from or writing to the network.
         if code != 1006 and self.state is OPEN:
-            self.send_frame(Frame(True, OP_CLOSE, serialize_close(code, reason)))
+            self.send_frame(Frame(OP_CLOSE, serialize_close(code, reason)))
             self.set_state(CLOSING)
         if not self.eof_sent:
             self.send_eof()
@@ -365,7 +365,7 @@ class Connection:
                 # send a Pong frame in response, unless it already received a
                 # Close frame."
                 if not self.close_frame_received:
-                    pong_frame = Frame(True, OP_PONG, frame.data)
+                    pong_frame = Frame(OP_PONG, frame.data)
                     self.send_frame(pong_frame)
 
             elif frame.opcode is OP_PONG:
@@ -391,7 +391,7 @@ class Connection:
                     # serialize_close() because that fails when the close frame
                     # is empty and parse_close() synthetizes a 1005 close code.
                     # The rest is identical to send_close().
-                    self.send_frame(Frame(True, OP_CLOSE, frame.data))
+                    self.send_frame(Frame(OP_CLOSE, frame.data))
                     self.set_state(CLOSING)
                     if self.side is SERVER:
                         self.send_eof()
diff --git a/src/websockets/extensions/permessage_deflate.py b/src/websockets/extensions/permessage_deflate.py
index 34cc1f9..56ef03e 100644
--- a/src/websockets/extensions/permessage_deflate.py
+++ b/src/websockets/extensions/permessage_deflate.py
@@ -4,6 +4,7 @@ Extensions for WebSocket as specified in :rfc:`7692`.
 
 """
 
+import dataclasses
 import zlib
 from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
 
@@ -115,7 +116,7 @@ class PerMessageDeflate(Extension):
         else:
             if not frame.rsv1:
                 return frame
-            frame = frame._replace(rsv1=False)
+            frame = dataclasses.replace(frame, rsv1=False)
             if not frame.fin:
                 self.decode_cont_data = True
 
@@ -138,7 +139,7 @@ class PerMessageDeflate(Extension):
         if frame.fin and self.remote_no_context_takeover:
             del self.decoder
 
-        return frame._replace(data=data)
+        return dataclasses.replace(frame, data=data)
 
     def encode(self, frame: Frame) -> Frame:
         """
@@ -154,7 +155,7 @@ class PerMessageDeflate(Extension):
 
         if frame.opcode != OP_CONT:
             # Set the rsv1 flag on the first frame of a compressed message.
-            frame = frame._replace(rsv1=True)
+            frame = dataclasses.replace(frame, rsv1=True)
             # Re-initialize per-message decoder.
             if self.local_no_context_takeover:
                 self.encoder = zlib.compressobj(
@@ -170,7 +171,7 @@ class PerMessageDeflate(Extension):
         if frame.fin and self.local_no_context_takeover:
             del self.encoder
 
-        return frame._replace(data=data)
+        return dataclasses.replace(frame, data=data)
 
 
 def _build_parameters(
diff --git a/src/websockets/frames.py b/src/websockets/frames.py
index de7fdb9..440f5a9 100644
--- a/src/websockets/frames.py
+++ b/src/websockets/frames.py
@@ -3,11 +3,12 @@ Parse and serialize WebSocket frames.
 
 """
 
+import dataclasses
 import enum
 import io
 import secrets
 import struct
-from typing import Callable, Generator, NamedTuple, Optional, Sequence, Tuple
+from typing import Callable, Generator, Optional, Sequence, Tuple
 
 from .exceptions import PayloadTooBig, ProtocolError
 from .typing import Data
@@ -92,10 +93,8 @@ EXTERNAL_CLOSE_CODES = {
 }
 
 
-# Consider converting to a dataclass when dropping support for Python < 3.7.
-
-
-class Frame(NamedTuple):
+@dataclasses.dataclass
+class Frame:
     """
     WebSocket frame.
 
@@ -107,13 +106,13 @@ class Frame(NamedTuple):
     :param bytes data: payload data
 
     Only these fields are needed. The MASK bit, payload length and masking-key
-    are handled on the fly by :func:`parse_frame` and :meth:`serialize_frame`.
+    are handled on the fly by :meth:`parse` and :meth:`serialize`.
 
     """
 
-    fin: bool
     opcode: Opcode
     data: bytes
+    fin: bool = True
     rsv1: bool = False
     rsv2: bool = False
     rsv3: bool = False
@@ -225,7 +224,7 @@ class Frame(NamedTuple):
         if mask:
             data = apply_mask(data, mask_bytes)
 
-        frame = cls(fin, opcode, data, rsv1, rsv2, rsv3)
+        frame = cls(opcode, data, fin, rsv1, rsv2, rsv3)
 
         if extensions is None:
             extensions = []
diff --git a/src/websockets/http11.py b/src/websockets/http11.py
index 6f3cbcc..22488dc 100644
--- a/src/websockets/http11.py
+++ b/src/websockets/http11.py
@@ -1,5 +1,6 @@
+import dataclasses
 import re
-from typing import Callable, Generator, NamedTuple, Optional
+from typing import Callable, Generator, Optional
 
 from .datastructures import Headers
 from .exceptions import SecurityError
@@ -37,10 +38,8 @@ _token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
 _value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*")
 
 
-# Consider converting to dataclasses when dropping support for Python < 3.7.
-
-
-class Request(NamedTuple):
+@dataclasses.dataclass
+class Request:
     """
     WebSocket handshake request.
 
@@ -52,6 +51,9 @@ class Request(NamedTuple):
     headers: Headers
     # body isn't useful is the context of this library
 
+    # If processing the request triggers an exception, it's stored here.
+    exception: Optional[Exception] = None
+
     @classmethod
     def parse(
         cls, read_line: Callable[[], Generator[None, None, bytes]]
@@ -121,10 +123,8 @@ class Request(NamedTuple):
         return request
 
 
-# Consider converting to dataclasses when dropping support for Python < 3.7.
-
-
-class Response(NamedTuple):
+@dataclasses.dataclass
+class Response:
     """
     WebSocket handshake response.
 
diff --git a/src/websockets/legacy/framing.py b/src/websockets/legacy/framing.py
index e947c93..901b2e2 100644
--- a/src/websockets/legacy/framing.py
+++ b/src/websockets/legacy/framing.py
@@ -11,7 +11,7 @@ See `section 5 of RFC 6455`_.
 """
 
 import struct
-from typing import Any, Awaitable, Callable, Optional, Sequence
+from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence
 
 from ..exceptions import PayloadTooBig, ProtocolError
 from ..frames import Frame as NewFrame, Opcode
@@ -23,7 +23,32 @@ except ImportError:  # pragma: no cover
     from ..utils import apply_mask
 
 
-class Frame(NewFrame):
+class Frame(NamedTuple):
+
+    fin: bool
+    opcode: Opcode
+    data: bytes
+    rsv1: bool = False
+    rsv2: bool = False
+    rsv3: bool = False
+
+    @property
+    def new_frame(self) -> NewFrame:
+        return NewFrame(
+            self.opcode,
+            self.data,
+            self.fin,
+            self.rsv1,
+            self.rsv2,
+            self.rsv3,
+        )
+
+    def __str__(self) -> str:
+        return str(self.new_frame)
+
+    def check(self) -> None:
+        return self.new_frame.check()
+
     @classmethod
     async def read(
         cls,
@@ -86,16 +111,23 @@ class Frame(NewFrame):
         if mask:
             data = apply_mask(data, mask_bits)
 
-        frame = cls(fin, opcode, data, rsv1, rsv2, rsv3)
+        new_frame = NewFrame(opcode, data, fin, rsv1, rsv2, rsv3)
 
         if extensions is None:
             extensions = []
         for extension in reversed(extensions):
-            frame = cls(*extension.decode(frame, max_size=max_size))
+            new_frame = extension.decode(new_frame, max_size=max_size)
 
-        frame.check()
+        new_frame.check()
 
-        return frame
+        return cls(
+            new_frame.fin,
+            new_frame.opcode,
+            new_frame.data,
+            new_frame.rsv1,
+            new_frame.rsv2,
+            new_frame.rsv3,
+        )
 
     def write(
         self,
@@ -121,7 +153,7 @@ class Frame(NewFrame):
         # The frame is written in a single call to write in order to prevent
         # TCP fragmentation. See #68 for details. This also makes it safe to
         # send frames concurrently from multiple coroutines.
-        write(self.serialize(mask=mask, extensions=extensions))
+        write(self.new_frame.serialize(mask=mask, extensions=extensions))
 
 
 # at the bottom to allow circular import, because Extension depends on Frame
diff --git a/src/websockets/server.py b/src/websockets/server.py
index 1ce9438..483ddbe 100644
--- a/src/websockets/server.py
+++ b/src/websockets/server.py
@@ -79,19 +79,18 @@ class ServerConnection(Connection):
         connection, which may be unexpected.
 
         """
-        # TODO: when changing Request to a dataclass, set the exception
-        # attribute on the request rather than the Response, which will
-        # be semantically more correct.
         try:
             key, extensions_header, protocol_header = self.process_request(request)
         except InvalidOrigin as exc:
+            request.exception = exc
             if self.debug:
                 self.logger.debug("! invalid origin", exc_info=True)
             return self.reject(
                 http.HTTPStatus.FORBIDDEN,
                 f"Failed to open a WebSocket connection: {exc}.\n",
-            )._replace(exception=exc)
+            )
         except InvalidUpgrade as exc:
+            request.exception = exc
             if self.debug:
                 self.logger.debug("! invalid upgrade", exc_info=True)
             return self.reject(
@@ -103,15 +102,17 @@ class ServerConnection(Connection):
                     f"with a browser. You need a WebSocket client.\n"
                 ),
                 headers=Headers([("Upgrade", "websocket")]),
-            )._replace(exception=exc)
+            )
         except InvalidHandshake as exc:
+            request.exception = exc
             if self.debug:
                 self.logger.debug("! invalid handshake", exc_info=True)
             return self.reject(
                 http.HTTPStatus.BAD_REQUEST,
                 f"Failed to open a WebSocket connection: {exc}.\n",
-            )._replace(exception=exc)
+            )
         except Exception as exc:
+            request.exception = exc
             self.logger.error("opening handshake failed", exc_info=True)
             return self.reject(
                 http.HTTPStatus.INTERNAL_SERVER_ERROR,
@@ -119,7 +120,7 @@ class ServerConnection(Connection):
                     "Failed to open a WebSocket connection.\n"
                     "See server log for more information.\n"
                 ),
-            )._replace(exception=exc)
+            )
 
         headers = Headers()
 
diff --git a/src/websockets/uri.py b/src/websockets/uri.py
index ce21b44..958975b 100644
--- a/src/websockets/uri.py
+++ b/src/websockets/uri.py
@@ -7,8 +7,9 @@ See `section 3 of RFC 6455`_.
 
 """
 
+import dataclasses
 import urllib.parse
-from typing import NamedTuple, Optional, Tuple
+from typing import Optional, Tuple
 
 from .exceptions import InvalidURI
 
@@ -16,10 +17,8 @@ from .exceptions import InvalidURI
 __all__ = ["parse_uri", "WebSocketURI"]
 
 
-# Consider converting to a dataclass when dropping support for Python < 3.7.
-
-
-class WebSocketURI(NamedTuple):
+@dataclasses.dataclass
+class WebSocketURI:
     """
     WebSocket URI.
 
@@ -37,16 +36,7 @@ class WebSocketURI(NamedTuple):
     host: str
     port: int
     resource_name: str
-    user_info: Optional[Tuple[str, str]]
-
-
-# Work around https://bugs.python.org/issue19931
-
-WebSocketURI.secure.__doc__ = ""
-WebSocketURI.host.__doc__ = ""
-WebSocketURI.port.__doc__ = ""
-WebSocketURI.resource_name.__doc__ = ""
-WebSocketURI.user_info.__doc__ = ""
+    user_info: Optional[Tuple[str, str]] = None
 
 
 # All characters from the gen-delims and sub-delims sets in RFC 3987.


Test patch:
diff --git a/tests/extensions/test_permessage_deflate.py b/tests/extensions/test_permessage_deflate.py
index d3c6f0a..5ba4d8d 100644
--- a/tests/extensions/test_permessage_deflate.py
+++ b/tests/extensions/test_permessage_deflate.py
@@ -1,3 +1,4 @@
+import dataclasses
 import unittest
 import zlib
 
@@ -55,21 +56,21 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
     # Control frames aren't encoded or decoded.
 
     def test_no_encode_decode_ping_frame(self):
-        frame = Frame(True, OP_PING, b"")
+        frame = Frame(OP_PING, b"")
 
         self.assertEqual(self.extension.encode(frame), frame)
 
         self.assertEqual(self.extension.decode(frame), frame)
 
     def test_no_encode_decode_pong_frame(self):
-        frame = Frame(True, OP_PONG, b"")
+        frame = Frame(OP_PONG, b"")
 
         self.assertEqual(self.extension.encode(frame), frame)
 
         self.assertEqual(self.extension.decode(frame), frame)
 
     def test_no_encode_decode_close_frame(self):
-        frame = Frame(True, OP_CLOSE, serialize_close(1000, ""))
+        frame = Frame(OP_CLOSE, serialize_close(1000, ""))
 
         self.assertEqual(self.extension.encode(frame), frame)
 
@@ -78,31 +79,35 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
     # Data frames are encoded and decoded.
 
     def test_encode_decode_text_frame(self):
-        frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+        frame = Frame(OP_TEXT, "café".encode("utf-8"))
 
         enc_frame = self.extension.encode(frame)
 
-        self.assertEqual(enc_frame, frame._replace(rsv1=True, data=b"JNL;\xbc\x12\x00"))
+        self.assertEqual(
+            enc_frame, dataclasses.replace(frame, rsv1=True, data=b"JNL;\xbc\x12\x00")
+        )
 
         dec_frame = self.extension.decode(enc_frame)
 
         self.assertEqual(dec_frame, frame)
 
     def test_encode_decode_binary_frame(self):
-        frame = Frame(True, OP_BINARY, b"tea")
+        frame = Frame(OP_BINARY, b"tea")
 
         enc_frame = self.extension.encode(frame)
 
-        self.assertEqual(enc_frame, frame._replace(rsv1=True, data=b"*IM\x04\x00"))
+        self.assertEqual(
+            enc_frame, dataclasses.replace(frame, rsv1=True, data=b"*IM\x04\x00")
+        )
 
         dec_frame = self.extension.decode(enc_frame)
 
         self.assertEqual(dec_frame, frame)
 
     def test_encode_decode_fragmented_text_frame(self):
-        frame1 = Frame(False, OP_TEXT, "café".encode("utf-8"))
-        frame2 = Frame(False, OP_CONT, " & ".encode("utf-8"))
-        frame3 = Frame(True, OP_CONT, "croissants".encode("utf-8"))
+        frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False)
+        frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False)
+        frame3 = Frame(OP_CONT, "croissants".encode("utf-8"))
 
         enc_frame1 = self.extension.encode(frame1)
         enc_frame2 = self.extension.encode(frame2)
@@ -110,13 +115,15 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
 
         self.assertEqual(
             enc_frame1,
-            frame1._replace(rsv1=True, data=b"JNL;\xbc\x12\x00\x00\x00\xff\xff"),
+            dataclasses.replace(
+                frame1, rsv1=True, data=b"JNL;\xbc\x12\x00\x00\x00\xff\xff"
+            ),
         )
         self.assertEqual(
-            enc_frame2, frame2._replace(data=b"RPS\x00\x00\x00\x00\xff\xff")
+            enc_frame2, dataclasses.replace(frame2, data=b"RPS\x00\x00\x00\x00\xff\xff")
         )
         self.assertEqual(
-            enc_frame3, frame3._replace(data=b"J.\xca\xcf,.N\xcc+)\x06\x00")
+            enc_frame3, dataclasses.replace(frame3, data=b"J.\xca\xcf,.N\xcc+)\x06\x00")
         )
 
         dec_frame1 = self.extension.decode(enc_frame1)
@@ -128,19 +135,21 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         self.assertEqual(dec_frame3, frame3)
 
     def test_encode_decode_fragmented_binary_frame(self):
-        frame1 = Frame(False, OP_TEXT, b"tea ")
-        frame2 = Frame(True, OP_CONT, b"time")
+        frame1 = Frame(OP_TEXT, b"tea ", fin=False)
+        frame2 = Frame(OP_CONT, b"time")
 
         enc_frame1 = self.extension.encode(frame1)
         enc_frame2 = self.extension.encode(frame2)
 
         self.assertEqual(
             enc_frame1,
-            frame1._replace(rsv1=True, data=b"*IMT\x00\x00\x00\x00\xff\xff"),
+            dataclasses.replace(
+                frame1, rsv1=True, data=b"*IMT\x00\x00\x00\x00\xff\xff"
+            ),
         )
         self.assertEqual(
             enc_frame2,
-            frame2._replace(data=b"*\xc9\xccM\x05\x00"),
+            dataclasses.replace(frame2, data=b"*\xc9\xccM\x05\x00"),
         )
 
         dec_frame1 = self.extension.decode(enc_frame1)
@@ -150,21 +159,21 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         self.assertEqual(dec_frame2, frame2)
 
     def test_no_decode_text_frame(self):
-        frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+        frame = Frame(OP_TEXT, "café".encode("utf-8"))
 
         # Try decoding a frame that wasn't encoded.
         self.assertEqual(self.extension.decode(frame), frame)
 
     def test_no_decode_binary_frame(self):
-        frame = Frame(True, OP_TEXT, b"tea")
+        frame = Frame(OP_TEXT, b"tea")
 
         # Try decoding a frame that wasn't encoded.
         self.assertEqual(self.extension.decode(frame), frame)
 
     def test_no_decode_fragmented_text_frame(self):
-        frame1 = Frame(False, OP_TEXT, "café".encode("utf-8"))
-        frame2 = Frame(False, OP_CONT, " & ".encode("utf-8"))
-        frame3 = Frame(True, OP_CONT, "croissants".encode("utf-8"))
+        frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False)
+        frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False)
+        frame3 = Frame(OP_CONT, "croissants".encode("utf-8"))
 
         dec_frame1 = self.extension.decode(frame1)
         dec_frame2 = self.extension.decode(frame2)
@@ -175,8 +184,8 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         self.assertEqual(dec_frame3, frame3)
 
     def test_no_decode_fragmented_binary_frame(self):
-        frame1 = Frame(False, OP_TEXT, b"tea ")
-        frame2 = Frame(True, OP_CONT, b"time")
+        frame1 = Frame(OP_TEXT, b"tea ", fin=False)
+        frame2 = Frame(OP_CONT, b"time")
 
         dec_frame1 = self.extension.decode(frame1)
         dec_frame2 = self.extension.decode(frame2)
@@ -185,7 +194,7 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         self.assertEqual(dec_frame2, frame2)
 
     def test_context_takeover(self):
-        frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+        frame = Frame(OP_TEXT, "café".encode("utf-8"))
 
         enc_frame1 = self.extension.encode(frame)
         enc_frame2 = self.extension.encode(frame)
@@ -197,7 +206,7 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         # No context takeover when decoding messages.
         self.extension = PerMessageDeflate(True, False, 15, 15)
 
-        frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+        frame = Frame(OP_TEXT, "café".encode("utf-8"))
 
         enc_frame1 = self.extension.encode(frame)
         enc_frame2 = self.extension.encode(frame)
@@ -216,7 +225,7 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         # No context takeover when encoding and decoding messages.
         self.extension = PerMessageDeflate(True, True, 15, 15)
 
-        frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+        frame = Frame(OP_TEXT, "café".encode("utf-8"))
 
         enc_frame1 = self.extension.encode(frame)
         enc_frame2 = self.extension.encode(frame)
@@ -236,21 +245,23 @@ class PerMessageDeflateTests(unittest.TestCase, ExtensionTestsMixin):
         # Configure an extension so that no compression actually occurs.
         extension = PerMessageDeflate(False, False, 15, 15, {"level": 0})
 
-        frame = Frame(True, OP_TEXT, "café".encode("utf-8"))
+        frame = Frame(OP_TEXT, "café".encode("utf-8"))
 
         enc_frame = extension.encode(frame)
 
         self.assertEqual(
             enc_frame,
-            frame._replace(
-                rsv1=True, data=b"\x00\x05\x00\xfa\xffcaf\xc3\xa9\x00"  # not compressed
+            dataclasses.replace(
+                frame,
+                rsv1=True,
+                data=b"\x00\x05\x00\xfa\xffcaf\xc3\xa9\x00",  # not compressed
             ),
         )
 
     # Frames aren't decoded beyond max_size.
 
     def test_decompress_max_size(self):
-        frame = Frame(True, OP_TEXT, ("a" * 20).encode("utf-8"))
+        frame = Frame(OP_TEXT, ("a" * 20).encode("utf-8"))
 
         enc_frame = self.extension.encode(frame)
 
diff --git a/tests/extensions/utils.py b/tests/extensions/utils.py
index 81990bb..1eabc16 100644
--- a/tests/extensions/utils.py
+++ b/tests/extensions/utils.py
@@ -1,3 +1,5 @@
+import dataclasses
+
 from websockets.exceptions import NegotiationError
 
 
@@ -49,11 +51,11 @@ class Rsv2Extension:
 
     def decode(self, frame, *, max_size=None):
         assert frame.rsv2
-        return frame._replace(rsv2=False)
+        return dataclasses.replace(frame, rsv2=False)
 
     def encode(self, frame):
         assert not frame.rsv2
-        return frame._replace(rsv2=True)
+        return dataclasses.replace(frame, rsv2=True)
 
     def __eq__(self, other):
         return isinstance(other, Rsv2Extension)
diff --git a/tests/legacy/test_framing.py b/tests/legacy/test_framing.py
index ac870c7..2baa827 100644
--- a/tests/legacy/test_framing.py
+++ b/tests/legacy/test_framing.py
@@ -1,5 +1,6 @@
 import asyncio
 import codecs
+import dataclasses
 import unittest
 import unittest.mock
 import warnings
@@ -43,6 +44,7 @@ class FramingTests(AsyncioTestCase):
 
     def round_trip(self, message, expected, mask=False, extensions=None):
         decoded = self.decode(message, mask, extensions=extensions)
+        decoded.check()
         self.assertEqual(decoded, expected)
         encoded = self.encode(decoded, mask, extensions=extensions)
         if mask:  # non-deterministic encoding
@@ -159,7 +161,7 @@ class FramingTests(AsyncioTestCase):
                 assert frame.opcode == OP_TEXT
                 text = frame.data.decode()
                 data = codecs.encode(text, "rot13").encode()
-                return frame._replace(data=data)
+                return dataclasses.replace(frame, data=data)
 
             # This extensions is symmetrical.
             @staticmethod
diff --git a/tests/test_client.py b/tests/test_client.py
index 840b714..2ef1f6a 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -577,7 +577,7 @@ class MiscTests(unittest.TestCase):
         client = ClientConnection("ws://example.com/test", state=OPEN)
         client.receive_data(b"\x81\x06Hello!")
         [frame] = client.events_received()
-        self.assertEqual(frame, Frame(True, OP_TEXT, b"Hello!"))
+        self.assertEqual(frame, Frame(OP_TEXT, b"Hello!"))
 
     def test_custom_logger(self):
         logger = logging.getLogger("test")
diff --git a/tests/test_connection.py b/tests/test_connection.py
index 6203a14..6778812 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -59,7 +59,6 @@ class ConnectionTestCase(FramesTestCase):
 
         """
         close_frame = Frame(
-            True,
             OP_CLOSE,
             b"" if code is None else serialize_close(code, reason),
         )
@@ -76,7 +75,6 @@ class ConnectionTestCase(FramesTestCase):
 
         """
         close_frame = Frame(
-            True,
             OP_CLOSE,
             b"" if code is None else serialize_close(code, reason),
         )
@@ -113,7 +111,7 @@ class MaskingTests(ConnectionTestCase):
         client.receive_data(self.unmasked_text_frame_date)
         self.assertFrameReceived(
             client,
-            Frame(True, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam"),
         )
 
     def test_server_receives_masked_frame(self):
@@ -121,7 +119,7 @@ class MaskingTests(ConnectionTestCase):
         server.receive_data(self.masked_text_frame_data)
         self.assertFrameReceived(
             server,
-            Frame(True, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam"),
         )
 
     def test_client_receives_masked_frame(self):
@@ -235,7 +233,7 @@ class TextTests(ConnectionTestCase):
         client.receive_data(b"\x81\x04\xf0\x9f\x98\x80")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_TEXT, "😀".encode()),
+            Frame(OP_TEXT, "😀".encode()),
         )
 
     def test_server_receives_text(self):
@@ -243,7 +241,7 @@ class TextTests(ConnectionTestCase):
         server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_TEXT, "😀".encode()),
+            Frame(OP_TEXT, "😀".encode()),
         )
 
     def test_client_receives_text_over_size_limit(self):
@@ -265,7 +263,7 @@ class TextTests(ConnectionTestCase):
         client.receive_data(b"\x81\x04\xf0\x9f\x98\x80")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_TEXT, "😀".encode()),
+            Frame(OP_TEXT, "😀".encode()),
         )
 
     def test_server_receives_text_without_size_limit(self):
@@ -273,7 +271,7 @@ class TextTests(ConnectionTestCase):
         server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_TEXT, "😀".encode()),
+            Frame(OP_TEXT, "😀".encode()),
         )
 
     def test_client_sends_fragmented_text(self):
@@ -304,17 +302,17 @@ class TextTests(ConnectionTestCase):
         client.receive_data(b"\x01\x02\xf0\x9f")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_TEXT, "😀".encode()[:2]),
+            Frame(OP_TEXT, "😀".encode()[:2], fin=False),
         )
         client.receive_data(b"\x00\x04\x98\x80\xf0\x9f")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_CONT, "😀😀".encode()[2:6]),
+            Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
         )
         client.receive_data(b"\x80\x02\x98\x80")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_CONT, "😀".encode()[2:]),
+            Frame(OP_CONT, "😀".encode()[2:]),
         )
 
     def test_server_receives_fragmented_text(self):
@@ -322,17 +320,17 @@ class TextTests(ConnectionTestCase):
         server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_TEXT, "😀".encode()[:2]),
+            Frame(OP_TEXT, "😀".encode()[:2], fin=False),
         )
         server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_CONT, "😀😀".encode()[2:6]),
+            Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
         )
         server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_CONT, "😀".encode()[2:]),
+            Frame(OP_CONT, "😀".encode()[2:]),
         )
 
     def test_client_receives_fragmented_text_over_size_limit(self):
@@ -340,7 +338,7 @@ class TextTests(ConnectionTestCase):
         client.receive_data(b"\x01\x02\xf0\x9f")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_TEXT, "😀".encode()[:2]),
+            Frame(OP_TEXT, "😀".encode()[:2], fin=False),
         )
         with self.assertRaises(PayloadTooBig) as raised:
             client.receive_data(b"\x80\x02\x98\x80")
@@ -352,7 +350,7 @@ class TextTests(ConnectionTestCase):
         server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_TEXT, "😀".encode()[:2]),
+            Frame(OP_TEXT, "😀".encode()[:2], fin=False),
         )
         with self.assertRaises(PayloadTooBig) as raised:
             server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
@@ -364,17 +362,17 @@ class TextTests(ConnectionTestCase):
         client.receive_data(b"\x01\x02\xf0\x9f")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_TEXT, "😀".encode()[:2]),
+            Frame(OP_TEXT, "😀".encode()[:2], fin=False),
         )
         client.receive_data(b"\x00\x04\x98\x80\xf0\x9f")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_CONT, "😀😀".encode()[2:6]),
+            Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
         )
         client.receive_data(b"\x80\x02\x98\x80")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_CONT, "😀".encode()[2:]),
+            Frame(OP_CONT, "😀".encode()[2:]),
         )
 
     def test_server_receives_fragmented_text_without_size_limit(self):
@@ -382,17 +380,17 @@ class TextTests(ConnectionTestCase):
         server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_TEXT, "😀".encode()[:2]),
+            Frame(OP_TEXT, "😀".encode()[:2], fin=False),
         )
         server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_CONT, "😀😀".encode()[2:6]),
+            Frame(OP_CONT, "😀😀".encode()[2:6], fin=False),
         )
         server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_CONT, "😀".encode()[2:]),
+            Frame(OP_CONT, "😀".encode()[2:]),
         )
 
     def test_client_sends_unexpected_text(self):
@@ -414,7 +412,7 @@ class TextTests(ConnectionTestCase):
         client.receive_data(b"\x01\x00")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_TEXT, b""),
+            Frame(OP_TEXT, b"", fin=False),
         )
         with self.assertRaises(ProtocolError) as raised:
             client.receive_data(b"\x01\x00")
@@ -426,7 +424,7 @@ class TextTests(ConnectionTestCase):
         server.receive_data(b"\x01\x80\x00\x00\x00\x00")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_TEXT, b""),
+            Frame(OP_TEXT, b"", fin=False),
         )
         with self.assertRaises(ProtocolError) as raised:
             server.receive_data(b"\x01\x80\x00\x00\x00\x00")
@@ -489,7 +487,7 @@ class BinaryTests(ConnectionTestCase):
         client.receive_data(b"\x82\x04\x01\x02\xfe\xff")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_BINARY, b"\x01\x02\xfe\xff"),
+            Frame(OP_BINARY, b"\x01\x02\xfe\xff"),
         )
 
     def test_server_receives_binary(self):
@@ -497,7 +495,7 @@ class BinaryTests(ConnectionTestCase):
         server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_BINARY, b"\x01\x02\xfe\xff"),
+            Frame(OP_BINARY, b"\x01\x02\xfe\xff"),
         )
 
     def test_client_receives_binary_over_size_limit(self):
@@ -542,17 +540,17 @@ class BinaryTests(ConnectionTestCase):
         client.receive_data(b"\x02\x02\x01\x02")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_BINARY, b"\x01\x02"),
+            Frame(OP_BINARY, b"\x01\x02", fin=False),
         )
         client.receive_data(b"\x00\x04\xfe\xff\x01\x02")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_CONT, b"\xfe\xff\x01\x02"),
+            Frame(OP_CONT, b"\xfe\xff\x01\x02", fin=False),
         )
         client.receive_data(b"\x80\x02\xfe\xff")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_CONT, b"\xfe\xff"),
+            Frame(OP_CONT, b"\xfe\xff"),
         )
 
     def test_server_receives_fragmented_binary(self):
@@ -560,17 +558,17 @@ class BinaryTests(ConnectionTestCase):
         server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_BINARY, b"\x01\x02"),
+            Frame(OP_BINARY, b"\x01\x02", fin=False),
         )
         server.receive_data(b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_CONT, b"\xee\xff\x01\x02"),
+            Frame(OP_CONT, b"\xee\xff\x01\x02", fin=False),
         )
         server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_CONT, b"\xfe\xff"),
+            Frame(OP_CONT, b"\xfe\xff"),
         )
 
     def test_client_receives_fragmented_binary_over_size_limit(self):
@@ -578,7 +576,7 @@ class BinaryTests(ConnectionTestCase):
         client.receive_data(b"\x02\x02\x01\x02")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_BINARY, b"\x01\x02"),
+            Frame(OP_BINARY, b"\x01\x02", fin=False),
         )
         with self.assertRaises(PayloadTooBig) as raised:
             client.receive_data(b"\x80\x02\xfe\xff")
@@ -590,7 +588,7 @@ class BinaryTests(ConnectionTestCase):
         server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_BINARY, b"\x01\x02"),
+            Frame(OP_BINARY, b"\x01\x02", fin=False),
         )
         with self.assertRaises(PayloadTooBig) as raised:
             server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff")
@@ -616,7 +614,7 @@ class BinaryTests(ConnectionTestCase):
         client.receive_data(b"\x02\x00")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_BINARY, b""),
+            Frame(OP_BINARY, b"", fin=False),
         )
         with self.assertRaises(ProtocolError) as raised:
             client.receive_data(b"\x02\x00")
@@ -628,7 +626,7 @@ class BinaryTests(ConnectionTestCase):
         server.receive_data(b"\x02\x80\x00\x00\x00\x00")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_BINARY, b""),
+            Frame(OP_BINARY, b"", fin=False),
         )
         with self.assertRaises(ProtocolError) as raised:
             server.receive_data(b"\x02\x80\x00\x00\x00\x00")
@@ -690,14 +688,14 @@ class CloseTests(ConnectionTestCase):
         client = Connection(Side.CLIENT)
         with self.enforce_mask(b"\x3c\x3c\x3c\x3c"):
             client.receive_data(b"\x88\x00")
-        self.assertEqual(client.events_received(), [Frame(True, OP_CLOSE, b"")])
+        self.assertEqual(client.events_received(), [Frame(OP_CLOSE, b"")])
         self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"])
         self.assertIs(client.state, State.CLOSING)
 
     def test_server_receives_close(self):
         server = Connection(Side.SERVER)
         server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
-        self.assertEqual(server.events_received(), [Frame(True, OP_CLOSE, b"")])
+        self.assertEqual(server.events_received(), [Frame(OP_CLOSE, b"")])
         self.assertEqual(server.data_to_send(), [b"\x88\x00", b""])
         self.assertIs(server.state, State.CLOSING)
 
@@ -707,10 +705,10 @@ class CloseTests(ConnectionTestCase):
 
         client.send_close()
         self.assertFrameReceived(client, None)
-        self.assertFrameSent(client, Frame(True, OP_CLOSE, b""))
+        self.assertFrameSent(client, Frame(OP_CLOSE, b""))
 
         client.receive_data(b"\x88\x00")
-        self.assertFrameReceived(client, Frame(True, OP_CLOSE, b""))
+        self.assertFrameReceived(client, Frame(OP_CLOSE, b""))
         self.assertFrameSent(client, None)
 
         client.receive_eof()
@@ -723,10 +721,10 @@ class CloseTests(ConnectionTestCase):
 
         server.send_close()
         self.assertFrameReceived(server, None)
-        self.assertFrameSent(server, Frame(True, OP_CLOSE, b""), eof=True)
+        self.assertFrameSent(server, Frame(OP_CLOSE, b""), eof=True)
 
         server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
-        self.assertFrameReceived(server, Frame(True, OP_CLOSE, b""))
+        self.assertFrameReceived(server, Frame(OP_CLOSE, b""))
         self.assertFrameSent(server, None)
 
         server.receive_eof()
@@ -738,8 +736,8 @@ class CloseTests(ConnectionTestCase):
         client = Connection(Side.CLIENT)
 
         client.receive_data(b"\x88\x00")
-        self.assertFrameReceived(client, Frame(True, OP_CLOSE, b""))
-        self.assertFrameSent(client, Frame(True, OP_CLOSE, b""))
+        self.assertFrameReceived(client, Frame(OP_CLOSE, b""))
+        self.assertFrameSent(client, Frame(OP_CLOSE, b""))
 
         client.receive_eof()
         self.assertFrameReceived(client, None)
@@ -750,8 +748,8 @@ class CloseTests(ConnectionTestCase):
         server = Connection(Side.SERVER)
 
         server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c")
-        self.assertFrameReceived(server, Frame(True, OP_CLOSE, b""))
-        self.assertFrameSent(server, Frame(True, OP_CLOSE, b""), eof=True)
+        self.assertFrameReceived(server, Frame(OP_CLOSE, b""))
+        self.assertFrameSent(server, Frame(OP_CLOSE, b""), eof=True)
 
         server.receive_eof()
         self.assertFrameReceived(server, None)
@@ -882,11 +880,11 @@ class PingTests(ConnectionTestCase):
         client.receive_data(b"\x89\x00")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PING, b""),
+            Frame(OP_PING, b""),
         )
         self.assertFrameSent(
             client,
-            Frame(True, OP_PONG, b""),
+            Frame(OP_PONG, b""),
         )
 
     def test_server_receives_ping(self):
@@ -894,11 +892,11 @@ class PingTests(ConnectionTestCase):
         server.receive_data(b"\x89\x80\x00\x44\x88\xcc")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PING, b""),
+            Frame(OP_PING, b""),
         )
         self.assertFrameSent(
             server,
-            Frame(True, OP_PONG, b""),
+            Frame(OP_PONG, b""),
         )
 
     def test_client_sends_ping_with_data(self):
@@ -919,11 +917,11 @@ class PingTests(ConnectionTestCase):
         client.receive_data(b"\x89\x04\x22\x66\xaa\xee")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PING, b"\x22\x66\xaa\xee"),
+            Frame(OP_PING, b"\x22\x66\xaa\xee"),
         )
         self.assertFrameSent(
             client,
-            Frame(True, OP_PONG, b"\x22\x66\xaa\xee"),
+            Frame(OP_PONG, b"\x22\x66\xaa\xee"),
         )
 
     def test_server_receives_ping_with_data(self):
@@ -931,25 +929,25 @@ class PingTests(ConnectionTestCase):
         server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PING, b"\x22\x66\xaa\xee"),
+            Frame(OP_PING, b"\x22\x66\xaa\xee"),
         )
         self.assertFrameSent(
             server,
-            Frame(True, OP_PONG, b"\x22\x66\xaa\xee"),
+            Frame(OP_PONG, b"\x22\x66\xaa\xee"),
         )
 
     def test_client_sends_fragmented_ping_frame(self):
         client = Connection(Side.CLIENT)
         # This is only possible through a private API.
         with self.assertRaises(ProtocolError) as raised:
-            client.send_frame(Frame(False, OP_PING, b""))
+            client.send_frame(Frame(OP_PING, b"", fin=False))
         self.assertEqual(str(raised.exception), "fragmented control frame")
 
     def test_server_sends_fragmented_ping_frame(self):
         server = Connection(Side.SERVER)
         # This is only possible through a private API.
         with self.assertRaises(ProtocolError) as raised:
-            server.send_frame(Frame(False, OP_PING, b""))
+            server.send_frame(Frame(OP_PING, b"", fin=False))
         self.assertEqual(str(raised.exception), "fragmented control frame")
 
     def test_client_receives_fragmented_ping_frame(self):
@@ -1000,7 +998,7 @@ class PingTests(ConnectionTestCase):
         client.receive_data(b"\x89\x04\x22\x66\xaa\xee")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PING, b"\x22\x66\xaa\xee"),
+            Frame(OP_PING, b"\x22\x66\xaa\xee"),
         )
         self.assertFrameSent(client, None)
 
@@ -1011,7 +1009,7 @@ class PingTests(ConnectionTestCase):
         server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PING, b"\x22\x66\xaa\xee"),
+            Frame(OP_PING, b"\x22\x66\xaa\xee"),
         )
         self.assertFrameSent(server, None)
 
@@ -1038,7 +1036,7 @@ class PongTests(ConnectionTestCase):
         client.receive_data(b"\x8a\x00")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PONG, b""),
+            Frame(OP_PONG, b""),
         )
 
     def test_server_receives_pong(self):
@@ -1046,7 +1044,7 @@ class PongTests(ConnectionTestCase):
         server.receive_data(b"\x8a\x80\x00\x44\x88\xcc")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PONG, b""),
+            Frame(OP_PONG, b""),
         )
 
     def test_client_sends_pong_with_data(self):
@@ -1067,7 +1065,7 @@ class PongTests(ConnectionTestCase):
         client.receive_data(b"\x8a\x04\x22\x66\xaa\xee")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PONG, b"\x22\x66\xaa\xee"),
+            Frame(OP_PONG, b"\x22\x66\xaa\xee"),
         )
 
     def test_server_receives_pong_with_data(self):
@@ -1075,21 +1073,21 @@ class PongTests(ConnectionTestCase):
         server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PONG, b"\x22\x66\xaa\xee"),
+            Frame(OP_PONG, b"\x22\x66\xaa\xee"),
         )
 
     def test_client_sends_fragmented_pong_frame(self):
         client = Connection(Side.CLIENT)
         # This is only possible through a private API.
         with self.assertRaises(ProtocolError) as raised:
-            client.send_frame(Frame(False, OP_PONG, b""))
+            client.send_frame(Frame(OP_PONG, b"", fin=False))
         self.assertEqual(str(raised.exception), "fragmented control frame")
 
     def test_server_sends_fragmented_pong_frame(self):
         server = Connection(Side.SERVER)
         # This is only possible through a private API.
         with self.assertRaises(ProtocolError) as raised:
-            server.send_frame(Frame(False, OP_PONG, b""))
+            server.send_frame(Frame(OP_PONG, b"", fin=False))
         self.assertEqual(str(raised.exception), "fragmented control frame")
 
     def test_client_receives_fragmented_pong_frame(self):
@@ -1130,7 +1128,7 @@ class PongTests(ConnectionTestCase):
         client.receive_data(b"\x8a\x04\x22\x66\xaa\xee")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PONG, b"\x22\x66\xaa\xee"),
+            Frame(OP_PONG, b"\x22\x66\xaa\xee"),
         )
 
     def test_server_receives_pong_after_receiving_close(self):
@@ -1140,7 +1138,7 @@ class PongTests(ConnectionTestCase):
         server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PONG, b"\x22\x66\xaa\xee"),
+            Frame(OP_PONG, b"\x22\x66\xaa\xee"),
         )
 
 
@@ -1155,59 +1153,59 @@ class FragmentationTests(ConnectionTestCase):
     def test_client_send_ping_pong_in_fragmented_message(self):
         client = Connection(Side.CLIENT)
         client.send_text(b"Spam", fin=False)
-        self.assertFrameSent(client, Frame(False, OP_TEXT, b"Spam"))
+        self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False))
         client.send_ping(b"Ping")
-        self.assertFrameSent(client, Frame(True, OP_PING, b"Ping"))
+        self.assertFrameSent(client, Frame(OP_PING, b"Ping"))
         client.send_continuation(b"Ham", fin=False)
-        self.assertFrameSent(client, Frame(False, OP_CONT, b"Ham"))
+        self.assertFrameSent(client, Frame(OP_CONT, b"Ham", fin=False))
         client.send_pong(b"Pong")
-        self.assertFrameSent(client, Frame(True, OP_PONG, b"Pong"))
+        self.assertFrameSent(client, Frame(OP_PONG, b"Pong"))
         client.send_continuation(b"Eggs", fin=True)
-        self.assertFrameSent(client, Frame(True, OP_CONT, b"Eggs"))
+        self.assertFrameSent(client, Frame(OP_CONT, b"Eggs"))
 
     def test_server_send_ping_pong_in_fragmented_message(self):
         server = Connection(Side.SERVER)
         server.send_text(b"Spam", fin=False)
-        self.assertFrameSent(server, Frame(False, OP_TEXT, b"Spam"))
+        self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False))
         server.send_ping(b"Ping")
-        self.assertFrameSent(server, Frame(True, OP_PING, b"Ping"))
+        self.assertFrameSent(server, Frame(OP_PING, b"Ping"))
         server.send_continuation(b"Ham", fin=False)
-        self.assertFrameSent(server, Frame(False, OP_CONT, b"Ham"))
+        self.assertFrameSent(server, Frame(OP_CONT, b"Ham", fin=False))
         server.send_pong(b"Pong")
-        self.assertFrameSent(server, Frame(True, OP_PONG, b"Pong"))
+        self.assertFrameSent(server, Frame(OP_PONG, b"Pong"))
         server.send_continuation(b"Eggs", fin=True)
-        self.assertFrameSent(server, Frame(True, OP_CONT, b"Eggs"))
+        self.assertFrameSent(server, Frame(OP_CONT, b"Eggs"))
 
     def test_client_receive_ping_pong_in_fragmented_message(self):
         client = Connection(Side.CLIENT)
         client.receive_data(b"\x01\x04Spam")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam", fin=False),
         )
         client.receive_data(b"\x89\x04Ping")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PING, b"Ping"),
+            Frame(OP_PING, b"Ping"),
         )
         self.assertFrameSent(
             client,
-            Frame(True, OP_PONG, b"Ping"),
+            Frame(OP_PONG, b"Ping"),
         )
         client.receive_data(b"\x00\x03Ham")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_CONT, b"Ham"),
+            Frame(OP_CONT, b"Ham", fin=False),
         )
         client.receive_data(b"\x8a\x04Pong")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_PONG, b"Pong"),
+            Frame(OP_PONG, b"Pong"),
         )
         client.receive_data(b"\x80\x04Eggs")
         self.assertFrameReceived(
             client,
-            Frame(True, OP_CONT, b"Eggs"),
+            Frame(OP_CONT, b"Eggs"),
         )
 
     def test_server_receive_ping_pong_in_fragmented_message(self):
@@ -1215,37 +1213,37 @@ class FragmentationTests(ConnectionTestCase):
         server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam", fin=False),
         )
         server.receive_data(b"\x89\x84\x00\x00\x00\x00Ping")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PING, b"Ping"),
+            Frame(OP_PING, b"Ping"),
         )
         self.assertFrameSent(
             server,
-            Frame(True, OP_PONG, b"Ping"),
+            Frame(OP_PONG, b"Ping"),
         )
         server.receive_data(b"\x00\x83\x00\x00\x00\x00Ham")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_CONT, b"Ham"),
+            Frame(OP_CONT, b"Ham", fin=False),
         )
         server.receive_data(b"\x8a\x84\x00\x00\x00\x00Pong")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_PONG, b"Pong"),
+            Frame(OP_PONG, b"Pong"),
         )
         server.receive_data(b"\x80\x84\x00\x00\x00\x00Eggs")
         self.assertFrameReceived(
             server,
-            Frame(True, OP_CONT, b"Eggs"),
+            Frame(OP_CONT, b"Eggs"),
         )
 
     def test_client_send_close_in_fragmented_message(self):
         client = Connection(Side.CLIENT)
         client.send_text(b"Spam", fin=False)
-        self.assertFrameSent(client, Frame(False, OP_TEXT, b"Spam"))
+        self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False))
         # The spec says: "An endpoint MUST be capable of handling control
         # frames in the middle of a fragmented message." However, since the
         # endpoint must not send a data frame after a close frame, a close
@@ -1258,7 +1256,7 @@ class FragmentationTests(ConnectionTestCase):
     def test_server_send_close_in_fragmented_message(self):
         server = Connection(Side.CLIENT)
         server.send_text(b"Spam", fin=False)
-        self.assertFrameSent(server, Frame(False, OP_TEXT, b"Spam"))
+        self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False))
         # The spec says: "An endpoint MUST be capable of handling control
         # frames in the middle of a fragmented message." However, since the
         # endpoint must not send a data frame after a close frame, a close
@@ -1272,7 +1270,7 @@ class FragmentationTests(ConnectionTestCase):
         client.receive_data(b"\x01\x04Spam")
         self.assertFrameReceived(
             client,
-            Frame(False, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam", fin=False),
         )
         # The spec says: "An endpoint MUST be capable of handling control
         # frames in the middle of a fragmented message." However, since the
@@ -1288,7 +1286,7 @@ class FragmentationTests(ConnectionTestCase):
         server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam")
         self.assertFrameReceived(
             server,
-            Frame(False, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam", fin=False),
         )
         # The spec says: "An endpoint MUST be capable of handling control
         # frames in the middle of a fragmented message." However, since the
@@ -1477,10 +1475,10 @@ class ExtensionsTests(ConnectionTestCase):
         client = Connection(Side.CLIENT)
         client.extensions = [Rsv2Extension()]
         client.receive_data(b"\xaa\x00")
-        self.assertEqual(client.events_received(), [Frame(True, OP_PONG, b"")])
+        self.assertEqual(client.events_received(), [Frame(OP_PONG, b"")])
 
     def test_server_extension_decodes_frame(self):
         server = Connection(Side.SERVER)
         server.extensions = [Rsv2Extension()]
         server.receive_data(b"\xaa\x80\x00\x44\x88\xcc")
-        self.assertEqual(server.events_received(), [Frame(True, OP_PONG, b"")])
+        self.assertEqual(server.events_received(), [Frame(OP_PONG, b"")])
diff --git a/tests/test_frames.py b/tests/test_frames.py
index 4913865..d363446 100644
--- a/tests/test_frames.py
+++ b/tests/test_frames.py
@@ -1,4 +1,5 @@
 import codecs
+import dataclasses
 import unittest
 import unittest.mock
 
@@ -47,77 +48,77 @@ class FramesTestCase(GeneratorTestCase):
 class FrameTests(FramesTestCase):
     def test_text_unmasked(self):
         self.assertFrameData(
-            Frame(True, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam"),
             b"\x81\x04Spam",
             mask=False,
         )
 
     def test_text_masked(self):
         self.assertFrameData(
-            Frame(True, OP_TEXT, b"Spam"),
+            Frame(OP_TEXT, b"Spam"),
             b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5",
             mask=True,
         )
 
     def test_binary_unmasked(self):
         self.assertFrameData(
-            Frame(True, OP_BINARY, b"Eggs"),
+            Frame(OP_BINARY, b"Eggs"),
             b"\x82\x04Eggs",
             mask=False,
         )
 
     def test_binary_masked(self):
         self.assertFrameData(
-            Frame(True, OP_BINARY, b"Eggs"),
+            Frame(OP_BINARY, b"Eggs"),
             b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa",
             mask=True,
         )
 
     def test_non_ascii_text_unmasked(self):
         self.assertFrameData(
-            Frame(True, OP_TEXT, "café".encode("utf-8")),
+            Frame(OP_TEXT, "café".encode("utf-8")),
             b"\x81\x05caf\xc3\xa9",
             mask=False,
         )
 
     def test_non_ascii_text_masked(self):
         self.assertFrameData(
-            Frame(True, OP_TEXT, "café".encode("utf-8")),
+            Frame(OP_TEXT, "café".encode("utf-8")),
             b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd",
             mask=True,
         )
 
     def test_close(self):
         self.assertFrameData(
-            Frame(True, OP_CLOSE, b""),
+            Frame(OP_CLOSE, b""),
             b"\x88\x00",
             mask=False,
         )
 
     def test_ping(self):
         self.assertFrameData(
-            Frame(True, OP_PING, b"ping"),
+            Frame(OP_PING, b"ping"),
             b"\x89\x04ping",
             mask=False,
         )
 
     def test_pong(self):
         self.assertFrameData(
-            Frame(True, OP_PONG, b"pong"),
+            Frame(OP_PONG, b"pong"),
             b"\x8a\x04pong",
             mask=False,
         )
 
     def test_long(self):
         self.assertFrameData(
-            Frame(True, OP_BINARY, 126 * b"a"),
+            Frame(OP_BINARY, 126 * b"a"),
             b"\x82\x7e\x00\x7e" + 126 * b"a",
             mask=False,
         )
 
     def test_very_long(self):
         self.assertFrameData(
-            Frame(True, OP_BINARY, 65536 * b"a"),
+            Frame(OP_BINARY, 65536 * b"a"),
             b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a",
             mask=False,
         )
@@ -178,7 +179,7 @@ class FrameTests(FramesTestCase):
                 assert frame.opcode == OP_TEXT
                 text = frame.data.decode()
                 data = codecs.encode(text, "rot13").encode()
-                return frame._replace(data=data)
+                return dataclasses.replace(frame, data=data)
 
             # This extensions is symmetrical.
             @staticmethod
@@ -186,7 +187,7 @@ class FrameTests(FramesTestCase):
                 return Rot13.encode(frame)
 
         self.assertFrameData(
-            Frame(True, OP_TEXT, b"hello"),
+            Frame(OP_TEXT, b"hello"),
             b"\x81\x05uryyb",
             mask=False,
             extensions=[Rot13()],
@@ -196,125 +197,125 @@ class FrameTests(FramesTestCase):
 class StrTests(unittest.TestCase):
     def test_cont_text(self):
         self.assertEqual(
-            str(Frame(False, OP_CONT, b" cr\xc3\xa8me")),
+            str(Frame(OP_CONT, b" cr\xc3\xa8me", fin=False)),
             "CONT  crème [text, 7 bytes, continued]",
         )
 
     def test_cont_binary(self):
         self.assertEqual(
-            str(Frame(False, OP_CONT, b"\xfc\xfd\xfe\xff")),
+            str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff", fin=False)),
             "CONT fc fd fe ff [binary, 4 bytes, continued]",
         )
 
     def test_cont_final_text(self):
         self.assertEqual(
-            str(Frame(True, OP_CONT, b" cr\xc3\xa8me")),
+            str(Frame(OP_CONT, b" cr\xc3\xa8me")),
             "CONT  crème [text, 7 bytes]",
         )
 
     def test_cont_final_binary(self):
         self.assertEqual(
-            str(Frame(True, OP_CONT, b"\xfc\xfd\xfe\xff")),
+            str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff")),
             "CONT fc fd fe ff [binary, 4 bytes]",
         )
 
     def test_cont_text_truncated(self):
         self.assertEqual(
-            str(Frame(False, OP_CONT, b"caf\xc3\xa9 " * 16)),
+            str(Frame(OP_CONT, b"caf\xc3\xa9 " * 16, fin=False)),
             "CONT café café café café café café café café café caf..."
             "afé café café café café  [text, 96 bytes, continued]",
         )
 
     def test_cont_binary_truncated(self):
         self.assertEqual(
-            str(Frame(False, OP_CONT, bytes(range(256)))),
+            str(Frame(OP_CONT, bytes(range(256)), fin=False)),
             "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..."
             " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]",
         )
 
     def test_text(self):
         self.assertEqual(
-            str(Frame(True, OP_TEXT, b"caf\xc3\xa9")),
+            str(Frame(OP_TEXT, b"caf\xc3\xa9")),
             "TEXT café [5 bytes]",
         )
 
     def test_text_non_final(self):
         self.assertEqual(
-            str(Frame(False, OP_TEXT, b"caf\xc3\xa9")),
+            str(Frame(OP_TEXT, b"caf\xc3\xa9", fin=False)),
             "TEXT café [5 bytes, continued]",
         )
 
     def test_text_truncated(self):
         self.assertEqual(
-            str(Frame(True, OP_TEXT, b"caf\xc3\xa9 " * 16)),
+            str(Frame(OP_TEXT, b"caf\xc3\xa9 " * 16)),
             "TEXT café café café café café café café café café caf..."
             "afé café café café café  [96 bytes]",
         )
 
     def test_binary(self):
         self.assertEqual(
-            str(Frame(True, OP_BINARY, b"\x00\x01\x02\x03")),
+            str(Frame(OP_BINARY, b"\x00\x01\x02\x03")),
             "BINARY 00 01 02 03 [4 bytes]",
         )
 
     def test_binary_non_final(self):
         self.assertEqual(
-            str(Frame(False, OP_BINARY, b"\x00\x01\x02\x03")),
+            str(Frame(OP_BINARY, b"\x00\x01\x02\x03", fin=False)),
             "BINARY 00 01 02 03 [4 bytes, continued]",
         )
 
     def test_binary_truncated(self):
         self.assertEqual(
-            str(Frame(True, OP_BINARY, bytes(range(256)))),
+            str(Frame(OP_BINARY, bytes(range(256)))),
             "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..."
             " f8 f9 fa fb fc fd fe ff [256 bytes]",
         )
 
     def test_close(self):
         self.assertEqual(
-            str(Frame(True, OP_CLOSE, b"\x03\xe8")),
+            str(Frame(OP_CLOSE, b"\x03\xe8")),
             "CLOSE code = 1000 (OK), no reason [2 bytes]",
         )
 
     def test_close_reason(self):
         self.assertEqual(
-            str(Frame(True, OP_CLOSE, b"\x03\xe9Bye!")),
+            str(Frame(OP_CLOSE, b"\x03\xe9Bye!")),
             "CLOSE code = 1001 (going away), reason = Bye! [6 bytes]",
         )
 
     def test_ping(self):
         self.assertEqual(
-            str(Frame(True, OP_PING, b"")),
+            str(Frame(OP_PING, b"")),
             "PING  [0 bytes]",
         )
 
     def test_ping_text(self):
         self.assertEqual(
-            str(Frame(True, OP_PING, b"ping")),
+            str(Frame(OP_PING, b"ping")),
             "PING ping [text, 4 bytes]",
         )
 
     def test_ping_binary(self):
         self.assertEqual(
-            str(Frame(True, OP_PING, b"\xff\x00\xff\x00")),
+            str(Frame(OP_PING, b"\xff\x00\xff\x00")),
             "PING ff 00 ff 00 [binary, 4 bytes]",
         )
 
     def test_pong(self):
         self.assertEqual(
-            str(Frame(True, OP_PONG, b"")),
+            str(Frame(OP_PONG, b"")),
             "PONG  [0 bytes]",
         )
 
     def test_pong_text(self):
         self.assertEqual(
-            str(Frame(True, OP_PONG, b"pong")),
+            str(Frame(OP_PONG, b"pong")),
             "PONG pong [text, 4 bytes]",
         )
 
     def test_pong_binary(self):
         self.assertEqual(
-            str(Frame(True, OP_PONG, b"\xff\x00\xff\x00")),
+            str(Frame(OP_PONG, b"\xff\x00\xff\x00")),
             "PONG ff 00 ff 00 [binary, 4 bytes]",
         )
 
diff --git a/tests/test_server.py b/tests/test_server.py
index db34e6b..d2c4159 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -187,7 +187,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 500)
         with self.assertRaises(Exception) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "BOOM")
 
     def test_missing_connection(self):
@@ -199,7 +199,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(response.status_code, 426)
         self.assertEqual(response.headers["Upgrade"], "websocket")
         with self.assertRaises(InvalidUpgrade) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "missing Connection header")
 
     def test_invalid_connection(self):
@@ -212,7 +212,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(response.status_code, 426)
         self.assertEqual(response.headers["Upgrade"], "websocket")
         with self.assertRaises(InvalidUpgrade) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "invalid Connection header: close")
 
     def test_missing_upgrade(self):
@@ -224,7 +224,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(response.status_code, 426)
         self.assertEqual(response.headers["Upgrade"], "websocket")
         with self.assertRaises(InvalidUpgrade) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "missing Upgrade header")
 
     def test_invalid_upgrade(self):
@@ -237,7 +237,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(response.status_code, 426)
         self.assertEqual(response.headers["Upgrade"], "websocket")
         with self.assertRaises(InvalidUpgrade) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c")
 
     def test_missing_key(self):
@@ -248,7 +248,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Key header")
 
     def test_multiple_key(self):
@@ -259,7 +259,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception),
             "invalid Sec-WebSocket-Key header: "
@@ -275,7 +275,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception), "invalid Sec-WebSocket-Key header: not Base64 data!"
         )
@@ -291,7 +291,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception), f"invalid Sec-WebSocket-Key header: {KEY[:16]}"
         )
@@ -304,7 +304,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Version header")
 
     def test_multiple_version(self):
@@ -315,7 +315,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception),
             "invalid Sec-WebSocket-Version header: "
@@ -331,7 +331,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception), "invalid Sec-WebSocket-Version header: 11"
         )
@@ -343,7 +343,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 403)
         with self.assertRaises(InvalidOrigin) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(str(raised.exception), "missing Origin header")
 
     def test_origin(self):
@@ -363,7 +363,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 403)
         with self.assertRaises(InvalidOrigin) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception), "invalid Origin header: https://other.example.com"
         )
@@ -381,7 +381,7 @@ class AcceptRejectTests(unittest.TestCase):
         # 400 Bad Request rather than 403 Forbidden.
         self.assertEqual(response.status_code, 400)
         with self.assertRaises(InvalidHeader) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception),
             "invalid Origin header: more than one Origin header found",
@@ -408,7 +408,7 @@ class AcceptRejectTests(unittest.TestCase):
 
         self.assertEqual(response.status_code, 403)
         with self.assertRaises(InvalidOrigin) as raised:
-            raise response.exception
+            raise request.exception
         self.assertEqual(
             str(raised.exception), "invalid Origin header: https://original.example.com"
         )
@@ -634,7 +634,7 @@ class MiscTests(unittest.TestCase):
         server = ServerConnection(state=OPEN)
         server.receive_data(b"\x81\x86\x00\x00\x00\x00Hello!")
         [frame] = server.events_received()
-        self.assertEqual(frame, Frame(True, OP_TEXT, b"Hello!"))
+        self.assertEqual(frame, Frame(OP_TEXT, b"Hello!"))
 
     def test_custom_logger(self):
         logger = logging.getLogger("test")
diff --git a/tests/test_uri.py b/tests/test_uri.py
index 9eeb843..a91bcb0 100644
--- a/tests/test_uri.py
+++ b/tests/test_uri.py
@@ -5,15 +5,32 @@ from websockets.uri import *
 
 
 VALID_URIS = [
-    ("ws://localhost/", (False, "localhost", 80, "/", None)),
-    ("wss://localhost/", (True, "localhost", 443, "/", None)),
-    ("ws://localhost/path?query", (False, "localhost", 80, "/path?query", None)),
-    ("WS://LOCALHOST/PATH?QUERY", (False, "localhost", 80, "/PATH?QUERY", None)),
-    ("ws://user:pass@localhost/", (False, "localhost", 80, "/", ("user", "pass"))),
-    ("ws://høst/", (False, "xn--hst-0na", 80, "/", None)),
+    (
+        "ws://localhost/",
+        WebSocketURI(False, "localhost", 80, "/", None),
+    ),
+    (
+        "wss://localhost/",
+        WebSocketURI(True, "localhost", 443, "/", None),
+    ),
+    (
+        "ws://localhost/path?query",
+        WebSocketURI(False, "localhost", 80, "/path?query", None),
+    ),
+    (
+        "WS://LOCALHOST/PATH?QUERY",
+        WebSocketURI(False, "localhost", 80, "/PATH?QUERY", None),
+    ),
+    (
+        "ws://user:pass@localhost/",
+        WebSocketURI(False, "localhost", 80, "/", ("user", "pass")),
+    ),
+    ("ws://høst/", WebSocketURI(False, "xn--hst-0na", 80, "/", None)),
     (
         "ws://üser:påss@høst/πass",
-        (False, "xn--hst-0na", 80, "/%CF%80ass", ("%C3%BCser", "p%C3%A5ss")),
+        WebSocketURI(
+            False, "xn--hst-0na", 80, "/%CF%80ass", ("%C3%BCser", "p%C3%A5ss")
+        ),
     ),
 ]