Skip to content

Commit

Permalink
Move prepare_data/ctrl back to the legacy framing module.
Browse files Browse the repository at this point in the history
They are deprecated and they must go away with the legacy implementation.

They were only documented in the framing module, not in the new frames
module, so this doesn't require more backwards-compatibility shims.
  • Loading branch information
aaugustin committed Aug 21, 2024
1 parent 3944595 commit 6b1cc94
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 123 deletions.
17 changes: 13 additions & 4 deletions src/websockets/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)

from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError
from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl
from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode
from ..http11 import Request, Response
from ..protocol import CLOSED, OPEN, Event, Protocol, State
from ..typing import Data, LoggerLike, Subprotocol
Expand Down Expand Up @@ -597,8 +597,12 @@ async def ping(self, data: Data | None = None) -> Awaitable[float]:
the corresponding pong wasn't received yet.
"""
if data is not None:
data = prepare_ctrl(data)
if isinstance(data, BytesLike):
data = bytes(data)
elif isinstance(data, str):
data = data.encode()
elif data is not None:
raise TypeError("data must be str or bytes-like")

async with self.send_context():
# Protect against duplicates if a payload is explicitly set.
Expand Down Expand Up @@ -632,7 +636,12 @@ async def pong(self, data: Data = b"") -> None:
ConnectionClosed: When the connection is closed.
"""
data = prepare_ctrl(data)
if isinstance(data, BytesLike):
data = bytes(data)
elif isinstance(data, str):
data = data.encode()
else:
raise TypeError("data must be str or bytes-like")

async with self.send_context():
self.protocol.send_pong(data)
Expand Down
50 changes: 0 additions & 50 deletions src/websockets/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Callable, Generator, Sequence

from . import exceptions, extensions
from .typing import Data


try:
Expand All @@ -29,8 +28,6 @@
"DATA_OPCODES",
"CTRL_OPCODES",
"Frame",
"prepare_data",
"prepare_ctrl",
"Close",
]

Expand Down Expand Up @@ -354,53 +351,6 @@ def check(self) -> None:
raise exceptions.ProtocolError("fragmented control frame")


def prepare_data(data: Data) -> tuple[int, bytes]:
"""
Convert a string or byte-like object to an opcode and a bytes-like object.
This function is designed for data frames.
If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes`
object encoding ``data`` in UTF-8.
If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like
object.
Raises:
TypeError: If ``data`` doesn't have a supported type.
"""
if isinstance(data, str):
return OP_TEXT, data.encode()
elif isinstance(data, BytesLike):
return OP_BINARY, data
else:
raise TypeError("data must be str or bytes-like")


def prepare_ctrl(data: Data) -> bytes:
"""
Convert a string or byte-like object to bytes.
This function is designed for ping and pong frames.
If ``data`` is a :class:`str`, return a :class:`bytes` object encoding
``data`` in UTF-8.
If ``data`` is a bytes-like object, return a :class:`bytes` object.
Raises:
TypeError: If ``data`` doesn't have a supported type.
"""
if isinstance(data, str):
return data.encode()
elif isinstance(data, BytesLike):
return bytes(data)
else:
raise TypeError("data must be str or bytes-like")


@dataclasses.dataclass
class Close:
"""
Expand Down
58 changes: 53 additions & 5 deletions src/websockets/legacy/framing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from .. import extensions, frames
from ..exceptions import PayloadTooBig, ProtocolError
from ..frames import BytesLike
from ..typing import Data


try:
Expand Down Expand Up @@ -144,12 +146,58 @@ def write(
write(self.new_frame.serialize(mask=mask, extensions=extensions))


def prepare_data(data: Data) -> tuple[int, bytes]:
"""
Convert a string or byte-like object to an opcode and a bytes-like object.
This function is designed for data frames.
If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes`
object encoding ``data`` in UTF-8.
If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like
object.
Raises:
TypeError: If ``data`` doesn't have a supported type.
"""
if isinstance(data, str):
return frames.Opcode.TEXT, data.encode()
elif isinstance(data, BytesLike):
return frames.Opcode.BINARY, data
else:
raise TypeError("data must be str or bytes-like")


def prepare_ctrl(data: Data) -> bytes:
"""
Convert a string or byte-like object to bytes.
This function is designed for ping and pong frames.
If ``data`` is a :class:`str`, return a :class:`bytes` object encoding
``data`` in UTF-8.
If ``data`` is a bytes-like object, return a :class:`bytes` object.
Raises:
TypeError: If ``data`` doesn't have a supported type.
"""
if isinstance(data, str):
return data.encode()
elif isinstance(data, BytesLike):
return bytes(data)
else:
raise TypeError("data must be str or bytes-like")


# Backwards compatibility with previously documented public APIs
encode_data = prepare_ctrl

# Backwards compatibility with previously documented public APIs
from ..frames import ( # noqa: E402, F401, I001
Close,
prepare_ctrl as encode_data,
prepare_data,
)
from ..frames import Close # noqa: E402 F401, I001


def parse_close(data: bytes) -> tuple[int, str]:
Expand Down
4 changes: 1 addition & 3 deletions src/websockets/legacy/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,10 @@
Close,
CloseCode,
Opcode,
prepare_ctrl,
prepare_data,
)
from ..protocol import State
from ..typing import Data, LoggerLike, Subprotocol
from .framing import Frame
from .framing import Frame, prepare_ctrl, prepare_data


__all__ = ["WebSocketCommonProtocol"]
Expand Down
1 change: 0 additions & 1 deletion src/websockets/speedups.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ _PyBytesLike_AsStringAndSize(PyObject *obj, PyObject **tmp, char **buffer, Py_ss
{
// This supports bytes, bytearrays, and memoryview objects,
// which are common data structures for handling byte streams.
// websockets.framing.prepare_data() returns only these types.
// If *tmp isn't NULL, the caller gets a new reference.
if (PyBytes_Check(obj))
{
Expand Down
17 changes: 13 additions & 4 deletions src/websockets/sync/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Any, Iterable, Iterator, Mapping

from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError
from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl
from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode
from ..http11 import Request, Response
from ..protocol import CLOSED, OPEN, Event, Protocol, State
from ..typing import Data, LoggerLike, Subprotocol
Expand Down Expand Up @@ -449,8 +449,12 @@ def ping(self, data: Data | None = None) -> threading.Event:
the corresponding pong wasn't received yet.
"""
if data is not None:
data = prepare_ctrl(data)
if isinstance(data, BytesLike):
data = bytes(data)
elif isinstance(data, str):
data = data.encode()
elif data is not None:
raise TypeError("data must be str or bytes-like")

with self.send_context():
# Protect against duplicates if a payload is explicitly set.
Expand Down Expand Up @@ -481,7 +485,12 @@ def pong(self, data: Data = b"") -> None:
ConnectionClosed: When the connection is closed.
"""
data = prepare_ctrl(data)
if isinstance(data, BytesLike):
data = bytes(data)
elif isinstance(data, str):
data = data.encode()
else:
raise TypeError("data must be str or bytes-like")

with self.send_context():
self.protocol.send_pong(data)
Expand Down
10 changes: 10 additions & 0 deletions tests/asyncio/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,11 @@ async def test_ping_duplicate_payload(self):

await self.connection.ping("idem") # doesn't raise an exception

async def test_ping_unsupported_type(self):
"""ping raises TypeError when called with an unsupported type."""
with self.assertRaises(TypeError):
await self.connection.ping([])

# Test pong.

async def test_pong(self):
Expand All @@ -886,6 +891,11 @@ async def test_pong_explicit_binary(self):
await self.connection.pong(b"pong")
await self.assertFrameSent(Frame(Opcode.PONG, b"pong"))

async def test_pong_unsupported_type(self):
"""pong raises TypeError when called with an unsupported type."""
with self.assertRaises(TypeError):
await self.connection.pong([])

# Test keepalive.

@patch("random.getrandbits")
Expand Down
56 changes: 56 additions & 0 deletions tests/legacy/test_framing.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,62 @@ def decode(frame, *, max_size=None):
)


class PrepareDataTests(unittest.TestCase):
def test_prepare_data_str(self):
self.assertEqual(
prepare_data("café"),
(OP_TEXT, b"caf\xc3\xa9"),
)

def test_prepare_data_bytes(self):
self.assertEqual(
prepare_data(b"tea"),
(OP_BINARY, b"tea"),
)

def test_prepare_data_bytearray(self):
self.assertEqual(
prepare_data(bytearray(b"tea")),
(OP_BINARY, bytearray(b"tea")),
)

def test_prepare_data_memoryview(self):
self.assertEqual(
prepare_data(memoryview(b"tea")),
(OP_BINARY, memoryview(b"tea")),
)

def test_prepare_data_list(self):
with self.assertRaises(TypeError):
prepare_data([])

def test_prepare_data_none(self):
with self.assertRaises(TypeError):
prepare_data(None)


class PrepareCtrlTests(unittest.TestCase):
def test_prepare_ctrl_str(self):
self.assertEqual(prepare_ctrl("café"), b"caf\xc3\xa9")

def test_prepare_ctrl_bytes(self):
self.assertEqual(prepare_ctrl(b"tea"), b"tea")

def test_prepare_ctrl_bytearray(self):
self.assertEqual(prepare_ctrl(bytearray(b"tea")), b"tea")

def test_prepare_ctrl_memoryview(self):
self.assertEqual(prepare_ctrl(memoryview(b"tea")), b"tea")

def test_prepare_ctrl_list(self):
with self.assertRaises(TypeError):
prepare_ctrl([])

def test_prepare_ctrl_none(self):
with self.assertRaises(TypeError):
prepare_ctrl(None)


class ParseAndSerializeCloseTests(unittest.TestCase):
def assertCloseData(self, code, reason, data):
"""
Expand Down
10 changes: 10 additions & 0 deletions tests/sync/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,11 @@ def test_ping_duplicate_payload(self):

self.connection.ping("idem") # doesn't raise an exception

def test_ping_unsupported_type(self):
"""ping raises TypeError when called with an unsupported type."""
with self.assertRaises(TypeError):
self.connection.ping([])

# Test pong.

def test_pong(self):
Expand All @@ -682,6 +687,11 @@ def test_pong_explicit_binary(self):
self.connection.pong(b"pong")
self.assertFrameSent(Frame(Opcode.PONG, b"pong"))

def test_pong_unsupported_type(self):
"""pong raises TypeError when called with an unsupported type."""
with self.assertRaises(TypeError):
self.connection.pong([])

# Test attributes.

def test_id(self):
Expand Down
Loading

0 comments on commit 6b1cc94

Please sign in to comment.