From 7343dce39fcc3c3f6886e7e609d787eced55a215 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 14 Dec 2025 18:37:59 +0000 Subject: [PATCH 1/3] Support multiple arguments via pubsub emits --- src/socketio/pubsub_manager.py | 9 ++ tests/common/test_pubsub_manager.py | 180 ++++++++++++++++++++++++++-- 2 files changed, 179 insertions(+), 10 deletions(-) diff --git a/src/socketio/pubsub_manager.py b/src/socketio/pubsub_manager.py index 3528d228..4254d4af 100644 --- a/src/socketio/pubsub_manager.py +++ b/src/socketio/pubsub_manager.py @@ -63,6 +63,10 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None + if isinstance(data, tuple): + data = list(data) + else: + data = [data] binary = Packet.data_is_binary(data) if binary: data, attachments = Packet.deconstruct_binary(data) @@ -151,6 +155,11 @@ def _handle_emit(self, message): if message.get('binary'): attachments = [base64.b64decode(a) for a in data[1:]] data = Packet.reconstruct_binary(data[0], attachments) + if isinstance(data, list): + if len(data) == 1: + data = data[0] + else: + data = tuple(data) super().emit(message['event'], data, namespace=message.get('namespace'), room=message.get('room'), diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index abef2bf2..227a2eed 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -70,7 +70,7 @@ def test_emit(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -86,7 +86,7 @@ def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -100,7 +100,7 @@ def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -116,7 +116,7 @@ def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -130,7 +130,87 @@ def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + def test_emit_list(self): + self.pm.emit('foo', [1, 'two']) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [[1, 'two']], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + self.pm.emit('foo', [1, b'two', 'three']) + self.pm._publish.assert_called_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv', + ], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + def test_emit_no_arguments(self): + self.pm.emit('foo', ()) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + def test_emit_multiple_arguments(self): + self.pm.emit('foo', (1, 'two')) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [1, 'two'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + self.pm.emit('foo', (1, b'two', 'three')) + self.pm._publish.assert_called_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv', + ], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -147,7 +227,7 @@ def test_emit_with_to(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': sid, 'skip_sid': None, @@ -163,7 +243,7 @@ def test_emit_with_namespace(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/baz', 'room': None, 'skip_sid': None, @@ -179,7 +259,7 @@ def test_emit_with_room(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -195,7 +275,7 @@ def test_emit_with_skip_sid(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': 'baz', @@ -214,7 +294,7 @@ def test_emit_with_callback(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -305,6 +385,18 @@ def test_close_room_with_namespace(self): ) def test_handle_emit(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': ['bar']}) + super_emit.assert_called_once_with( + 'foo', + 'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_legacy_emit(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit({'event': 'foo', 'data': 'bar'}) super_emit.assert_called_once_with( @@ -317,6 +409,35 @@ def test_handle_emit(self): ) def test_handle_emit_binary(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], + }) + super_emit.assert_called_once_with( + 'foo', + b'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + }) + super_emit.assert_called_with( + 'foo', + {'foo': b'bar'}, + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_legacy_emit_binary(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit({ 'event': 'foo', @@ -345,6 +466,45 @@ def test_handle_emit_binary(self): callback=None, ) + def test_handle_emit_no_arguments(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': []}) + super_emit.assert_called_once_with( + 'foo', + (), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_emit_multiple_arguments(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': [1, 'two']}) + super_emit.assert_called_once_with( + 'foo', + (1, 'two'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv' + ], + }) + super_emit.assert_called_with( + 'foo', + (1, b'two', 'three'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + def test_handle_emit_with_namespace(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( From c49f05050deb34150d885e658f89fcb4f4c06bac Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 14 Dec 2025 18:50:22 +0000 Subject: [PATCH 2/3] async side --- src/socketio/async_pubsub_manager.py | 9 +++ tests/async/test_pubsub_manager.py | 100 ++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/socketio/async_pubsub_manager.py b/src/socketio/async_pubsub_manager.py index 1bfcf9dc..6d631640 100644 --- a/src/socketio/async_pubsub_manager.py +++ b/src/socketio/async_pubsub_manager.py @@ -66,6 +66,10 @@ async def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None + if isinstance(data, tuple): + data = list(data) + else: + data = [data] binary = Packet.data_is_binary(data) if binary: data, attachments = Packet.deconstruct_binary(data) @@ -155,6 +159,11 @@ async def _handle_emit(self, message): if message.get('binary'): attachments = [base64.b64decode(a) for a in data[1:]] data = Packet.reconstruct_binary(data[0], attachments) + if isinstance(data, list): + if len(data) == 1: + data = data[0] + else: + data = tuple(data) await super().emit(message['event'], data, namespace=message.get('namespace'), room=message.get('room'), diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index f9cfb7eb..0da08d22 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -58,7 +58,7 @@ async def test_emit(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -74,7 +74,7 @@ async def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -88,7 +88,7 @@ async def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -104,7 +104,7 @@ async def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -118,7 +118,87 @@ async def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + async def test_emit_list(self): + await self.pm.emit('foo', [1, 'two']) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [[1, 'two']], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + await self.pm.emit('foo', [1, b'two', 'three']) + self.pm._publish.assert_awaited_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv', + ], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + async def test_emit_no_arguments(self): + await self.pm.emit('foo', ()) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + async def test_emit_multiple_arguments(self): + await self.pm.emit('foo', (1, 'two')) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [1, 'two'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + await self.pm.emit('foo', (1, b'two', 'three')) + self.pm._publish.assert_awaited_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv', + ], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -135,7 +215,7 @@ async def test_emit_with_to(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': sid, 'skip_sid': None, @@ -151,7 +231,7 @@ async def test_emit_with_namespace(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/baz', 'room': None, 'skip_sid': None, @@ -167,7 +247,7 @@ async def test_emit_with_room(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -183,7 +263,7 @@ async def test_emit_with_skip_sid(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': 'baz', @@ -202,7 +282,7 @@ async def test_emit_with_callback(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, From 70f93900574a4dced15550f138fa9e7384d6b8af Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 14 Dec 2025 19:05:21 +0000 Subject: [PATCH 3/3] more unit tests --- tests/async/test_pubsub_manager.py | 117 ++++++++++++++++++++++++++++ tests/common/test_pubsub_manager.py | 27 +++++++ 2 files changed, 144 insertions(+) diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index 0da08d22..abf41a24 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -374,6 +374,20 @@ async def test_close_room_with_namespace(self): ) async def test_handle_emit(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': ['bar']}) + super_emit.assert_awaited_once_with( + 'foo', + 'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_legacy_emit(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: @@ -388,6 +402,37 @@ async def test_handle_emit(self): ) async def test_handle_emit_binary(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], + }) + super_emit.assert_awaited_once_with( + 'foo', + b'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + }) + super_emit.assert_awaited_with( + 'foo', + {'foo': b'bar'}, + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_legacy_emit_binary(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: @@ -418,6 +463,78 @@ async def test_handle_emit_binary(self): callback=None, ) + async def test_handle_emit_list(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': [[1, 'two']]}) + super_emit.assert_awaited_once_with( + 'foo', + [1, 'two'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv' + ] + }) + super_emit.assert_awaited_with( + 'foo', + [1, b'two', 'three'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_emit_no_arguments(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': []}) + super_emit.assert_awaited_once_with( + 'foo', + (), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_emit_multiple_arguments(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': [1, 'two']}) + super_emit.assert_awaited_once_with( + 'foo', + (1, 'two'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv' + ] + }) + super_emit.assert_awaited_with( + 'foo', + (1, b'two', 'three'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + async def test_handle_emit_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'emit' diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 227a2eed..91b77854 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -466,6 +466,33 @@ def test_handle_legacy_emit_binary(self): callback=None, ) + def test_handle_emit_list(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': [[1, 'two']]}) + super_emit.assert_called_once_with( + 'foo', + [1, 'two'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv' + ], + }) + super_emit.assert_called_with( + 'foo', + [1, b'two', 'three'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + def test_handle_emit_no_arguments(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit({'event': 'foo', 'data': []})