diff mbox series

[meta-python,7/8] python3-simple-websocket: Add recipe

Message ID 20261026035814.55254-7-khem.raj@oss.qualcomm.com
State New
Headers show
Series [meta-python,1/8] python3-ipy: upgrade 1.01 -> 1.2 | expand

Commit Message

Khem Raj Oct. 26, 2026, 3:58 a.m. UTC
Simple WebSocket server and client for Python, built on wsproto.

Backport the upstream conversion of the async tests to pytest-asyncio;
the 1.1.0 tests call asyncio.get_event_loop(), which fails on Python
3.14 when there is no current event loop. Add it to the fast ptest list;
all 47 tests pass on qemuriscv64.

AI-Generated: Uses Claude Code
Signed-off-by: Khem Raj <khem.raj@oss.qualcomm.com>
---
 .../ptest-packagelists-meta-python.inc        |   1 +
 ...test-improvements-for-Python-3.14-48.patch | 422 ++++++++++++++++++
 .../python/python3-simple-websocket_1.1.0.bb  |  19 +
 3 files changed, 442 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-simple-websocket/0001-Async-test-improvements-for-Python-3.14-48.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-simple-websocket_1.1.0.bb
diff mbox series

Patch

diff --git a/meta-python/conf/include/ptest-packagelists-meta-python.inc b/meta-python/conf/include/ptest-packagelists-meta-python.inc
index 92183897c7..6e4af06aaa 100644
--- a/meta-python/conf/include/ptest-packagelists-meta-python.inc
+++ b/meta-python/conf/include/ptest-packagelists-meta-python.inc
@@ -103,6 +103,7 @@  PTESTS_FAST_META_PYTHON = "\
     python3-serpent \
     python3-service-identity \
     python3-setproctitle \
+    python3-simple-websocket \
     python3-simpleeval \
     python3-six \
     python3-smpplib \
diff --git a/meta-python/recipes-devtools/python/python3-simple-websocket/0001-Async-test-improvements-for-Python-3.14-48.patch b/meta-python/recipes-devtools/python/python3-simple-websocket/0001-Async-test-improvements-for-Python-3.14-48.patch
new file mode 100644
index 0000000000..2be661c29e
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-simple-websocket/0001-Async-test-improvements-for-Python-3.14-48.patch
@@ -0,0 +1,422 @@ 
+From 03832db418f11a818051eefcd774495798b203af Mon Sep 17 00:00:00 2001
+From: Miguel Grinberg <miguel.grinberg@gmail.com>
+Date: Sun, 12 Apr 2026 10:38:44 +0100
+Subject: [PATCH] Async test improvements for Python 3.14 (#48)
+
+Python 3.14 no longer creates an event loop in asyncio.get_event_loop(),
+so the make_sync() test helper fails with "There is no current event
+loop in thread 'MainThread'". Run the async tests with pytest-asyncio.
+
+Upstream-Status: Backport [https://github.com/miguelgrinberg/simple-websocket/commit/2005dbb96b5b42a4ebf309d10ac1d33daaddb99f]
+Signed-off-by: Khem Raj <raj.khem@gmail.com>
+---
+ tests/helpers.py        | 25 -------------------------
+ tests/test_aioclient.py | 39 ++++++++++++++-------------------------
+ tests/test_aioserver.py | 39 +++++++++++++--------------------------
+ tests/test_server.py    |  2 +-
+ 4 files changed, 28 insertions(+), 77 deletions(-)
+ delete mode 100644 tests/helpers.py
+
+diff --git a/tests/helpers.py b/tests/helpers.py
+deleted file mode 100644
+index a7c1a46..0000000
+--- a/tests/helpers.py
++++ /dev/null
+@@ -1,25 +0,0 @@
+-import asyncio
+-from unittest import mock
+-
+-
+-def AsyncMock(*args, **kwargs):
+-    """Return a mock asynchronous function."""
+-    m = mock.MagicMock(*args, **kwargs)
+-
+-    async def mock_coro(*args, **kwargs):
+-        return m(*args, **kwargs)
+-
+-    mock_coro.mock = m
+-    return mock_coro
+-
+-
+-def _run(coro):
+-    """Run the given coroutine."""
+-    return asyncio.get_event_loop().run_until_complete(coro)
+-
+-
+-def make_sync(coro):
+-    """Wrap a coroutine so that it can be executed by pytest."""
+-    def wrapper(*args, **kwargs):
+-        return _run(coro(*args, **kwargs))
+-    return wrapper
+diff --git a/tests/test_aioclient.py b/tests/test_aioclient.py
+index 01a772a..5ec12ff 100644
+--- a/tests/test_aioclient.py
++++ b/tests/test_aioclient.py
+@@ -1,15 +1,14 @@
+ import asyncio
+-import unittest
+ from unittest import mock
+ import pytest  # noqa: F401
+
+ from wsproto.events import AcceptConnection, CloseConnection, TextMessage, \
+     BytesMessage, Ping
+ import simple_websocket
+-from .helpers import make_sync, AsyncMock
+
+
+-class AioSimpleWebSocketClientTestCase(unittest.TestCase):
++@pytest.mark.asyncio
++class TestAioSimpleWebSocketClient:
+     async def get_client(self, mock_wsconn, url, events=[], subprotocols=None,
+                          headers=None):
+         mock_wsconn().events.side_effect = \
+@@ -19,11 +18,10 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+         return await simple_websocket.AioClient.connect(
+             url, subprotocols=subprotocols, headers=headers)
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_make_client(self, mock_wsconn, mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(mock_wsconn, 'ws://example.com/ws?a=1')
+@@ -40,12 +38,11 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+         assert client.port == 80
+         assert client.path == '/ws?a=1'
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_make_client_subprotocol(self, mock_wsconn,
+                                            mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(mock_wsconn, 'ws://example.com/ws?a=1',
+@@ -55,12 +52,11 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+             b"Request(host='example.com', target='/ws?a=1', extensions=[], "
+             b"extra_headers=[], subprotocols=['foo'])")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_make_client_subprotocols(self, mock_wsconn,
+                                             mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(mock_wsconn, 'ws://example.com/ws?a=1',
+@@ -70,12 +66,11 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+             b"Request(host='example.com', target='/ws?a=1', extensions=[], "
+             b"extra_headers=[], subprotocols=['foo', 'bar'])")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_make_client_headers(self, mock_wsconn,
+                                        mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(mock_wsconn, 'ws://example.com/ws?a=1',
+@@ -84,12 +79,11 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+             b"Request(host='example.com', target='/ws?a=1', extensions=[], "
+             b"extra_headers=[('Foo', 'Bar')], subprotocols=[])")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_make_client_headers2(self, mock_wsconn,
+                                         mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(
+@@ -100,11 +94,10 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+             b"extra_headers=[('Foo', 'Bar'), ('Foo', 'Baz')], "
+             b"subprotocols=[])")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_send(self, mock_wsconn, mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(mock_wsconn, 'ws://example.com/ws')
+@@ -123,11 +116,10 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+             b"Message(data=b'hello', frame_finished=True, "
+             b"message_finished=True)")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive(self, mock_wsconn, mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(
+@@ -142,12 +134,11 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+         assert await client.receive() == b'hello'
+         assert await client.receive(timeout=0) is None
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_after_close(self, mock_wsconn,
+                                        mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(
+@@ -160,11 +151,10 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+         with pytest.raises(simple_websocket.ConnectionClosed):
+             await client.receive()
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_ping(self, mock_wsconn, mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+@@ -176,11 +166,11 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+             await asyncio.sleep(0.01)
+         wsock.write.assert_any_call(b"Pong(payload=b'hello')")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_empty(self, mock_wsconn, mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(side_effect=[b'x', b'x', b'']))
++        rsock = mock.MagicMock(read=mock.AsyncMock(
++            side_effect=[b'x', b'x', b'']))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(
+@@ -193,11 +183,10 @@ class AioSimpleWebSocketClientTestCase(unittest.TestCase):
+         assert await client.receive() == 'hello'
+         assert await client.receive(timeout=0) is None
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_close(self, mock_wsconn, mock_open_connection):
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         client = await self.get_client(
+diff --git a/tests/test_aioserver.py b/tests/test_aioserver.py
+index a274019..071e035 100644
+--- a/tests/test_aioserver.py
++++ b/tests/test_aioserver.py
+@@ -1,15 +1,14 @@
+ import asyncio
+-import unittest
+ from unittest import mock
+ import pytest  # noqa: F401
+
+ from wsproto.events import Request, CloseConnection, TextMessage, \
+     BytesMessage, Ping, Pong
+ import simple_websocket
+-from .helpers import make_sync, AsyncMock
+
+
+-class AioSimpleWebSocketServerTestCase(unittest.TestCase):
++@pytest.mark.asyncio
++class TestAioSimpleWebSocketServer:
+     async def get_server(self, mock_wsconn, request, events=[],
+                          client_subprotocols=None, server_subprotocols=None,
+                          **kwargs):
+@@ -29,12 +28,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         return await simple_websocket.AioServer.accept(
+             aiohttp=request, subprotocols=server_subprotocols, **kwargs)
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_aiohttp(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request)
+@@ -53,19 +51,17 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+             b'Sec-Websocket-Version: 13\r\n\r\n')
+         assert server.is_server
+
+-    @make_sync
+     async def test_invalid_request(self):
+         with pytest.raises(ValueError):
+             await simple_websocket.AioServer.accept(aiohttp='foo', asgi='bar')
+         with pytest.raises(ValueError):
+             await simple_websocket.AioServer.accept(asgi='bar', sock='baz')
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_send(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request)
+@@ -84,12 +80,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+             b"Message(data=b'hello', frame_finished=True, "
+             b"message_finished=True)")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -103,13 +98,12 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         assert await server.receive() == b'hello'
+         assert await server.receive(timeout=0) is None
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_after_close(self, mock_wsconn,
+                                        mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -121,13 +115,12 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         with pytest.raises(simple_websocket.ConnectionClosed):
+             await server.receive()
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_split_messages(self, mock_wsconn,
+                                           mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -151,12 +144,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         assert await server.receive() == b'hello'
+         assert await server.receive(timeout=0) is None
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_ping(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -166,12 +158,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+             await asyncio.sleep(0.01)
+         wsock.write.assert_any_call(b"Pong(payload=b'hello')")
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_empty(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -183,12 +174,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         assert await server.receive() == 'hello'
+         assert await server.receive(timeout=0) is None
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_receive_large(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -201,12 +191,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         assert await server.receive() == 'hello'
+         assert await server.receive(timeout=0) is None
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_close(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request)
+@@ -222,7 +211,6 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+             b'CloseConnection(code=<CloseReason.NORMAL_CLOSURE: 1000>, '
+             b'reason=None)')
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     @mock.patch('simple_websocket.aiows.time')
+@@ -230,7 +218,7 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+     async def test_ping_pong(self, mock_wait_for, mock_time, mock_wsconn,
+                              mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock())
++        rsock = mock.MagicMock(read=mock.AsyncMock())
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+         server = await self.get_server(mock_wsconn, mock_request, events=[
+@@ -246,12 +234,11 @@ class AioSimpleWebSocketServerTestCase(unittest.TestCase):
+         assert wsock.write.call_args_list[2][0][0].startswith(b'Ping')
+         assert wsock.write.call_args_list[3][0][0].startswith(b'Close')
+
+-    @make_sync
+     @mock.patch('simple_websocket.aiows.asyncio.open_connection')
+     @mock.patch('simple_websocket.aiows.WSConnection')
+     async def test_subprotocols(self, mock_wsconn, mock_open_connection):
+         mock_request = mock.MagicMock(headers={})
+-        rsock = mock.MagicMock(read=AsyncMock(return_value=b'x'))
++        rsock = mock.MagicMock(read=mock.AsyncMock(return_value=b'x'))
+         wsock = mock.MagicMock()
+         mock_open_connection.return_value = (rsock, wsock)
+
+diff --git a/tests/test_server.py b/tests/test_server.py
+index 7be5d15..ca5fe99 100644
+--- a/tests/test_server.py
++++ b/tests/test_server.py
+@@ -92,7 +92,7 @@ class SimpleWebSocketServerTestCase(unittest.TestCase):
+             b"message_finished=True)")
+         server.connected = True
+         server.send(b'hello')
+-        mock_socket.send.assert_called_with(
++        mock_socket.send.assert_any_call(
+             b"Message(data=b'hello', frame_finished=True, "
+             b"message_finished=True)")
+
diff --git a/meta-python/recipes-devtools/python/python3-simple-websocket_1.1.0.bb b/meta-python/recipes-devtools/python/python3-simple-websocket_1.1.0.bb
new file mode 100644
index 0000000000..35c568cf7c
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-simple-websocket_1.1.0.bb
@@ -0,0 +1,19 @@ 
+SUMMARY = "Simple WebSocket server and client for Python"
+HOMEPAGE = "https://github.com/miguelgrinberg/simple-websocket"
+LICENSE = "MIT"
+LIC_FILES_CHKSUM = "file://LICENSE;md5=9d272c9fe2437531b5bbecf4fcc82e24"
+
+inherit pypi python_setuptools_build_meta ptest-python-pytest
+
+SRC_URI += "file://0001-Async-test-improvements-for-Python-3.14-48.patch"
+SRC_URI[sha256sum] = "7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"
+
+RDEPENDS:${PN} += " \
+    python3-asyncio \
+    python3-io \
+    python3-wsproto \
+"
+
+RDEPENDS:${PN}-ptest += " \
+    python3-pytest-asyncio \
+"