Skip to content

Commit b2f0a76

Browse files
committed
Add option to force sending text or binary frames.
This adds the same functionality to the threading implemetation as bc4b8f2 did to the asyncio implementation. Refs #1515.
1 parent c5985d5 commit b2f0a76

File tree

3 files changed

+90
-42
lines changed

3 files changed

+90
-42
lines changed

src/websockets/asyncio/connection.py

+24-19
Original file line numberDiff line numberDiff line change
@@ -409,19 +409,17 @@ async def send(
409409
# strings and bytes-like objects are iterable.
410410

411411
if isinstance(message, str):
412-
if text is False:
413-
async with self.send_context():
412+
async with self.send_context():
413+
if text is False:
414414
self.protocol.send_binary(message.encode())
415-
else:
416-
async with self.send_context():
415+
else:
417416
self.protocol.send_text(message.encode())
418417

419418
elif isinstance(message, BytesLike):
420-
if text is True:
421-
async with self.send_context():
419+
async with self.send_context():
420+
if text is True:
422421
self.protocol.send_text(message)
423-
else:
424-
async with self.send_context():
422+
else:
425423
self.protocol.send_binary(message)
426424

427425
# Catch a common mistake -- passing a dict to send().
@@ -443,19 +441,17 @@ async def send(
443441
try:
444442
# First fragment.
445443
if isinstance(chunk, str):
446-
if text is False:
447-
async with self.send_context():
444+
async with self.send_context():
445+
if text is False:
448446
self.protocol.send_binary(chunk.encode(), fin=False)
449-
else:
450-
async with self.send_context():
447+
else:
451448
self.protocol.send_text(chunk.encode(), fin=False)
452449
encode = True
453450
elif isinstance(chunk, BytesLike):
454-
if text is True:
455-
async with self.send_context():
451+
async with self.send_context():
452+
if text is True:
456453
self.protocol.send_text(chunk, fin=False)
457-
else:
458-
async with self.send_context():
454+
else:
459455
self.protocol.send_binary(chunk, fin=False)
460456
encode = False
461457
else:
@@ -480,7 +476,10 @@ async def send(
480476
# We're half-way through a fragmented message and we can't
481477
# complete it. This makes the connection unusable.
482478
async with self.send_context():
483-
self.protocol.fail(1011, "error in fragmented message")
479+
self.protocol.fail(
480+
CloseCode.INTERNAL_ERROR,
481+
"error in fragmented message",
482+
)
484483
raise
485484

486485
finally:
@@ -538,7 +537,10 @@ async def send(
538537
# We're half-way through a fragmented message and we can't
539538
# complete it. This makes the connection unusable.
540539
async with self.send_context():
541-
self.protocol.fail(1011, "error in fragmented message")
540+
self.protocol.fail(
541+
CloseCode.INTERNAL_ERROR,
542+
"error in fragmented message",
543+
)
542544
raise
543545

544546
finally:
@@ -568,7 +570,10 @@ async def close(self, code: int = 1000, reason: str = "") -> None:
568570
# to terminate after calling a method that sends a close frame.
569571
async with self.send_context():
570572
if self.fragmented_send_waiter is not None:
571-
self.protocol.fail(1011, "close during fragmented message")
573+
self.protocol.fail(
574+
CloseCode.INTERNAL_ERROR,
575+
"close during fragmented message",
576+
)
572577
else:
573578
self.protocol.send_close(code, reason)
574579
except ConnectionClosed:

src/websockets/sync/connection.py

+38-23
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,11 @@ def recv_streaming(self) -> Iterator[Data]:
251251
"is already running recv or recv_streaming"
252252
) from None
253253

254-
def send(self, message: Data | Iterable[Data]) -> None:
254+
def send(
255+
self,
256+
message: Data | Iterable[Data],
257+
text: bool | None = None,
258+
) -> None:
255259
"""
256260
Send a message.
257261
@@ -262,6 +266,17 @@ def send(self, message: Data | Iterable[Data]) -> None:
262266
.. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
263267
.. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
264268
269+
You may override this behavior with the ``text`` argument:
270+
271+
* Set ``text=True`` to send a bytestring or bytes-like object
272+
(:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a
273+
Text_ frame. This improves performance when the message is already
274+
UTF-8 encoded, for example if the message contains JSON and you're
275+
using a JSON library that produces a bytestring.
276+
* Set ``text=False`` to send a string (:class:`str`) in a Binary_
277+
frame. This may be useful for servers that expect binary frames
278+
instead of text frames.
279+
265280
:meth:`send` also accepts an iterable of strings, bytestrings, or
266281
bytes-like objects to enable fragmentation_. Each item is treated as a
267282
message fragment and sent in its own frame. All items must be of the
@@ -300,7 +315,10 @@ def send(self, message: Data | Iterable[Data]) -> None:
300315
"cannot call send while another thread "
301316
"is already running send"
302317
)
303-
self.protocol.send_text(message.encode())
318+
if text is False:
319+
self.protocol.send_binary(message.encode())
320+
else:
321+
self.protocol.send_text(message.encode())
304322

305323
elif isinstance(message, BytesLike):
306324
with self.send_context():
@@ -309,7 +327,10 @@ def send(self, message: Data | Iterable[Data]) -> None:
309327
"cannot call send while another thread "
310328
"is already running send"
311329
)
312-
self.protocol.send_binary(message)
330+
if text is True:
331+
self.protocol.send_text(message)
332+
else:
333+
self.protocol.send_binary(message)
313334

314335
# Catch a common mistake -- passing a dict to send().
315336

@@ -328,50 +349,44 @@ def send(self, message: Data | Iterable[Data]) -> None:
328349
try:
329350
# First fragment.
330351
if isinstance(chunk, str):
331-
text = True
332352
with self.send_context():
333353
if self.send_in_progress:
334354
raise ConcurrencyError(
335355
"cannot call send while another thread "
336356
"is already running send"
337357
)
338358
self.send_in_progress = True
339-
self.protocol.send_text(
340-
chunk.encode(),
341-
fin=False,
342-
)
359+
if text is False:
360+
self.protocol.send_binary(chunk.encode(), fin=False)
361+
else:
362+
self.protocol.send_text(chunk.encode(), fin=False)
363+
encode = True
343364
elif isinstance(chunk, BytesLike):
344-
text = False
345365
with self.send_context():
346366
if self.send_in_progress:
347367
raise ConcurrencyError(
348368
"cannot call send while another thread "
349369
"is already running send"
350370
)
351371
self.send_in_progress = True
352-
self.protocol.send_binary(
353-
chunk,
354-
fin=False,
355-
)
372+
if text is True:
373+
self.protocol.send_text(chunk, fin=False)
374+
else:
375+
self.protocol.send_binary(chunk, fin=False)
376+
encode = False
356377
else:
357378
raise TypeError("data iterable must contain bytes or str")
358379

359380
# Other fragments
360381
for chunk in chunks:
361-
if isinstance(chunk, str) and text:
382+
if isinstance(chunk, str) and encode:
362383
with self.send_context():
363384
assert self.send_in_progress
364-
self.protocol.send_continuation(
365-
chunk.encode(),
366-
fin=False,
367-
)
368-
elif isinstance(chunk, BytesLike) and not text:
385+
self.protocol.send_continuation(chunk.encode(), fin=False)
386+
elif isinstance(chunk, BytesLike) and not encode:
369387
with self.send_context():
370388
assert self.send_in_progress
371-
self.protocol.send_continuation(
372-
chunk,
373-
fin=False,
374-
)
389+
self.protocol.send_continuation(chunk, fin=False)
375390
else:
376391
raise TypeError("data iterable must contain uniform types")
377392

tests/sync/test_connection.py

+28
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,16 @@ def test_send_binary(self):
308308
self.connection.send(b"\x01\x02\xfe\xff")
309309
self.assertEqual(self.remote_connection.recv(), b"\x01\x02\xfe\xff")
310310

311+
def test_send_binary_from_str(self):
312+
"""send sends a binary message from a str."""
313+
self.connection.send("😀", text=False)
314+
self.assertEqual(self.remote_connection.recv(), "😀".encode())
315+
316+
def test_send_text_from_bytes(self):
317+
"""send sends a text message from bytes."""
318+
self.connection.send("😀".encode(), text=True)
319+
self.assertEqual(self.remote_connection.recv(), "😀")
320+
311321
def test_send_fragmented_text(self):
312322
"""send sends a fragmented text message."""
313323
self.connection.send(["😀", "😀"])
@@ -326,6 +336,24 @@ def test_send_fragmented_binary(self):
326336
[b"\x01\x02", b"\xfe\xff", b""],
327337
)
328338

339+
def test_send_fragmented_binary_from_str(self):
340+
"""send sends a fragmented binary message from a str."""
341+
self.connection.send(["😀", "😀"], text=False)
342+
# websockets sends an trailing empty fragment. That's an implementation detail.
343+
self.assertEqual(
344+
list(self.remote_connection.recv_streaming()),
345+
["😀".encode(), "😀".encode(), b""],
346+
)
347+
348+
def test_send_fragmented_text_from_bytes(self):
349+
"""send sends a fragmented text message from bytes."""
350+
self.connection.send(["😀".encode(), "😀".encode()], text=True)
351+
# websockets sends an trailing empty fragment. That's an implementation detail.
352+
self.assertEqual(
353+
list(self.remote_connection.recv_streaming()),
354+
["😀", "😀", ""],
355+
)
356+
329357
def test_send_connection_closed_ok(self):
330358
"""send raises ConnectionClosedOK after a normal closure."""
331359
self.remote_connection.close()

0 commit comments

Comments
 (0)