Repository: attwad/python-osc

Issue:
Dispatcher replies expect List not Tuple on TCP
According to `dispatcher.py` lines [104-116](https://github.com/attwad/python-osc/blob/master/pythonosc/dispatcher.py#L104), one of the ways a callback function may pass a reply back to a client is by returning a message encapsulated thusly: `tuple(<Address>, <ArgValue>)`.

Actually attempting this with a TCP server results in an error, proclaiming that tuples lack an `encode` method.

The cause for this error appears to be because the code that handles the reply - found at `osc_tcp_server.py` lines 78 & 120:

```python
for r in resp:
    if not isinstance(r, list):
        r = [r]
```

checks if it is encapsulated within a *list*, not a *tuple* (and so we end up with `r` containing `[(<Address>, <ArgValue>)]`).

The same code in the UDP server - `osc_server.py` lines 38 & 164 - *does* check that the reply is encapsulated within a tuple, and thus is conformant to the documentation:

```python
for r in resp:
    if not isinstance(r, tuple):
        r = [r]
```

----

In addition, the tests `test_response_with_args()` and `test_async_response_with_args()` use List in `test_osc_tcp_server.py` and Tuple in `test_osc_server.py`.

Discussion:

Solution patch:
diff --git a/pythonosc/osc_tcp_server.py b/pythonosc/osc_tcp_server.py
index 56e0816..aec674f 100644
--- a/pythonosc/osc_tcp_server.py
+++ b/pythonosc/osc_tcp_server.py
@@ -75,7 +75,7 @@ def handle(self) -> None:
             )
             # resp = _call_handlers_for_packet(data, self.server.dispatcher)
             for r in resp:
-                if not isinstance(r, list):
+                if not isinstance(r, tuple):
                     r = [r]
                 msg = osc_message_builder.build_msg(r[0], r[1:])
                 b = struct.pack("!I", len(msg.dgram))
@@ -117,7 +117,7 @@ def handle(self) -> None:
                     p, self.client_address
                 )
                 for r in resp:
-                    if not isinstance(r, list):
+                    if not isinstance(r, tuple):
                         r = [r]
                     msg = osc_message_builder.build_msg(r[0], r[1:])
                     self.request.sendall(slip.encode(msg.dgram))
@@ -284,7 +284,7 @@ async def handle1_0(
                 buf, client_address
             )
             for r in result:
-                if not isinstance(r, list):
+                if not isinstance(r, tuple):
                     r = [r]
                 msg = osc_message_builder.build_msg(r[0], r[1:])
                 b = struct.pack("!I", len(msg.dgram))
@@ -319,7 +319,7 @@ async def handle_1_1(
                     p, client_address
                 )
                 for r in result:
-                    if not isinstance(r, list):
+                    if not isinstance(r, tuple):
                         r = [r]
                     msg = osc_message_builder.build_msg(r[0], r[1:])
                     writer.write(slip.encode(msg.dgram))


Test patch:
diff --git a/pythonosc/test/test_osc_tcp_server.py b/pythonosc/test/test_osc_tcp_server.py
index f1ed2d9..dd1d1d8 100644
--- a/pythonosc/test/test_osc_tcp_server.py
+++ b/pythonosc/test/test_osc_tcp_server.py
@@ -95,12 +95,12 @@ def respond(*args, **kwargs):
 
     def test_response_with_args(self):
         def respond(*args, **kwargs):
-            return [
+            return (
                 "/SYNC",
                 1,
                 "2",
                 3.0,
-            ]
+            )
 
         self.dispatcher.map("/SYNC", respond)
         mock_sock = mock.Mock()
@@ -208,12 +208,12 @@ def respond(*args, **kwargs):
 
     def test_response_with_args(self):
         def respond(*args, **kwargs):
-            return [
+            return (
                 "/SYNC",
                 1,
                 "2",
                 3.0,
-            ]
+            )
 
         self.dispatcher.map("/SYNC", respond)
         mock_sock = mock.Mock()
@@ -314,12 +314,12 @@ def respond(*args, **kwargs):
 
     async def test_response_with_args(self):
         def respond(*args, **kwargs):
-            return [
+            return (
                 "/SYNC",
                 1,
                 "2",
                 3.0,
-            ]
+            )
 
         self.dispatcher.map("/SYNC", respond)
         self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
@@ -332,12 +332,12 @@ def respond(*args, **kwargs):
 
     async def test_async_response_with_args(self):
         async def respond(*args, **kwargs):
-            return [
+            return (
                 "/SYNC",
                 1,
                 "2",
                 3.0,
-            ]
+            )
 
         self.dispatcher.map("/SYNC", respond)
         self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]