Skip to content

Commit ae1e5dd

Browse files
committed
add the ability to specify a timeout when constructing osc servers. Closes #170
1 parent b83ce43 commit ae1e5dd

File tree

6 files changed

+95
-31
lines changed

6 files changed

+95
-31
lines changed

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Simple client
7373
help="The port the OSC server is listening on")
7474
args = parser.parse_args()
7575
76-
client = udp_client.SimpleUDPClient(args.ip, args.port)
76+
client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
7777
7878
for x in range(10):
7979
client.send_message("/filter", random.random())
@@ -117,7 +117,7 @@ Simple server
117117
dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
118118
119119
server = osc_server.ThreadingOSCUDPServer(
120-
(args.ip, args.port), dispatcher)
120+
(args.ip, args.port), dispatcher, timeout=10)
121121
print("Serving on {}".format(server.server_address))
122122
server.serve_forever()
123123

pythonosc/osc_server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,19 @@ def __init__(
6363
server_address: Tuple[str, int],
6464
dispatcher: Dispatcher,
6565
bind_and_activate: bool = True,
66+
timeout: float | None = None,
6667
) -> None:
6768
"""Initialize
6869
6970
Args:
7071
server_address: IP and port of server
7172
dispatcher: Dispatcher this server will use
7273
(optional) bind_and_activate: default=True defines if the server has to start on call of constructor
74+
(optional) timeout: Default timeout in seconds for socket operations
7375
"""
7476
super().__init__(server_address, _UDPHandler, bind_and_activate)
7577
self._dispatcher = dispatcher
78+
self.timeout = timeout
7679

7780
def verify_request(
7881
self, request: _RequestType, client_address: _AddressType

pythonosc/tcp_client.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,23 @@ def __init__(
2222
port: int,
2323
family: socket.AddressFamily = socket.AF_INET,
2424
mode: str = MODE_1_1,
25+
timeout: float | None = 30.0,
2526
) -> None:
2627
"""Initialize client
2728
2829
Args:
2930
address: IP address of server
3031
port: Port of server
3132
family: address family parameter (passed to socket.getaddrinfo)
33+
timeout: Default timeout in seconds for socket operations
3234
"""
3335
self.address = address
3436
self.port = port
3537
self.family = family
3638
self.mode = mode
39+
self._timeout = timeout
3740
self.socket = socket.socket(self.family, socket.SOCK_STREAM)
38-
self.socket.settimeout(30)
41+
self.socket.settimeout(timeout)
3942
self.socket.connect((address, port))
4043

4144
def __enter__(self):
@@ -56,20 +59,21 @@ def send(self, content: Union[OscMessage, OscBundle]) -> None:
5659
b = struct.pack("!I", len(content.dgram))
5760
self.socket.sendall(b + content.dgram)
5861

59-
def receive(self, timeout: int = 30) -> List[bytes]:
60-
self.socket.settimeout(timeout)
62+
def receive(self, timeout: float | None = None) -> List[bytes]:
63+
effective_timeout = timeout if timeout is not None else self._timeout
64+
self.socket.settimeout(effective_timeout)
6165
if self.mode == MODE_1_1:
6266
try:
6367
buf = self.socket.recv(4096)
64-
except TimeoutError:
68+
except (TimeoutError, socket.timeout):
6569
return []
6670
if not buf:
6771
return []
6872
# If the last byte is not an END marker there could be more data coming
6973
while buf[-1] != 192:
7074
try:
7175
newbuf = self.socket.recv(4096)
72-
except TimeoutError:
76+
except (TimeoutError, socket.timeout):
7377
break
7478
if not newbuf:
7579
# Maybe should raise an exception here?
@@ -80,13 +84,13 @@ def receive(self, timeout: int = 30) -> List[bytes]:
8084
buf = b""
8185
try:
8286
lengthbuf = self.socket.recv(4)
83-
except TimeoutError:
87+
except (TimeoutError, socket.timeout):
8488
return []
8589
(length,) = struct.unpack("!I", lengthbuf)
8690
while length > 0:
8791
try:
8892
newbuf = self.socket.recv(length)
89-
except TimeoutError:
93+
except (TimeoutError, socket.timeout):
9094
return []
9195
if not newbuf:
9296
return []
@@ -116,7 +120,7 @@ def send_message(
116120
msg = build_msg(address, value)
117121
return self.send(msg)
118122

119-
def get_messages(self, timeout: int = 30) -> Generator:
123+
def get_messages(self, timeout: float | None = None) -> Generator:
120124
r = self.receive(timeout)
121125
while r:
122126
for m in r:
@@ -129,7 +133,7 @@ class TCPDispatchClient(SimpleTCPClient):
129133

130134
dispatcher = Dispatcher()
131135

132-
def handle_messages(self, timeout_sec: int = 30) -> None:
136+
def handle_messages(self, timeout_sec: float | None = None) -> None:
133137
"""Wait :int:`timeout` seconds for a message from the server and process each message with the registered
134138
handlers. Continue until a timeout occurs.
135139
@@ -152,18 +156,21 @@ def __init__(
152156
port: int,
153157
family: socket.AddressFamily = socket.AF_INET,
154158
mode: str = MODE_1_1,
159+
timeout: float | None = 30.0,
155160
) -> None:
156161
"""Initialize client
157162
158163
Args:
159164
address: IP address of server
160165
port: Port of server
161166
family: address family parameter (passed to socket.getaddrinfo)
167+
timeout: Default timeout in seconds for socket operations
162168
"""
163169
self.address: str = address
164170
self.port: int = port
165171
self.mode: str = mode
166172
self.family: socket.AddressFamily = family
173+
self._timeout = timeout
167174

168175
def __await__(self):
169176
async def closure():
@@ -197,19 +204,22 @@ async def send(self, content: Union[OscMessage, OscBundle]) -> None:
197204
self.writer.write(b + content.dgram)
198205
await self.writer.drain()
199206

200-
async def receive(self, timeout: int = 30) -> List[bytes]:
207+
async def receive(self, timeout: float | None = None) -> List[bytes]:
208+
effective_timeout = timeout if timeout is not None else self._timeout
201209
if self.mode == MODE_1_1:
202210
try:
203-
buf = await asyncio.wait_for(self.reader.read(4096), timeout)
204-
except TimeoutError:
211+
buf = await asyncio.wait_for(self.reader.read(4096), effective_timeout)
212+
except (TimeoutError, asyncio.TimeoutError):
205213
return []
206214
if not buf:
207215
return []
208216
# If the last byte is not an END marker there could be more data coming
209217
while buf[-1] != 192:
210218
try:
211-
newbuf = await asyncio.wait_for(self.reader.read(4096), timeout)
212-
except TimeoutError:
219+
newbuf = await asyncio.wait_for(
220+
self.reader.read(4096), effective_timeout
221+
)
222+
except (TimeoutError, asyncio.TimeoutError):
213223
break
214224
if not newbuf:
215225
# Maybe should raise an exception here?
@@ -219,15 +229,19 @@ async def receive(self, timeout: int = 30) -> List[bytes]:
219229
else:
220230
buf = b""
221231
try:
222-
lengthbuf = await asyncio.wait_for(self.reader.read(4), timeout)
223-
except TimeoutError:
232+
lengthbuf = await asyncio.wait_for(
233+
self.reader.read(4), effective_timeout
234+
)
235+
except (TimeoutError, asyncio.TimeoutError):
224236
return []
225237

226238
(length,) = struct.unpack("!I", lengthbuf)
227239
while length > 0:
228240
try:
229-
newbuf = await asyncio.wait_for(self.reader.read(length), timeout)
230-
except TimeoutError:
241+
newbuf = await asyncio.wait_for(
242+
self.reader.read(length), effective_timeout
243+
)
244+
except (TimeoutError, asyncio.TimeoutError):
231245
return []
232246
if not newbuf:
233247
return []
@@ -250,8 +264,9 @@ def __init__(
250264
port: int,
251265
family: socket.AddressFamily = socket.AF_INET,
252266
mode: str = MODE_1_1,
267+
timeout: float | None = 30.0,
253268
):
254-
super().__init__(address, port, family, mode)
269+
super().__init__(address, port, family, mode, timeout)
255270

256271
async def send_message(
257272
self, address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
@@ -265,7 +280,7 @@ async def send_message(
265280
msg = build_msg(address, value)
266281
return await self.send(msg)
267282

268-
async def get_messages(self, timeout: int = 30) -> AsyncGenerator:
283+
async def get_messages(self, timeout: float | None = None) -> AsyncGenerator:
269284
r = await self.receive(timeout)
270285
while r:
271286
for m in r:
@@ -278,7 +293,7 @@ class AsyncDispatchTCPClient(AsyncTCPClient):
278293

279294
dispatcher = Dispatcher()
280295

281-
async def handle_messages(self, timeout: int = 30) -> None:
296+
async def handle_messages(self, timeout: float | None = None) -> None:
282297
"""Wait :int:`timeout` seconds for a message from the server and process each message with the registered
283298
handlers. Continue until a timeout occurs.
284299

pythonosc/test/test_osc_server.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,13 @@ def respond(*args, **kwargs):
112112
)
113113

114114

115+
class TestOscUdpServer(unittest.TestCase):
116+
@unittest.mock.patch("socket.socket")
117+
def test_init_timeout(self, mock_socket_ctor):
118+
dispatcher = unittest.mock.Mock()
119+
server = osc_server.OSCUDPServer(("127.0.0.1", 0), dispatcher, timeout=10.0)
120+
self.assertEqual(server.timeout, 10.0)
121+
122+
115123
if __name__ == "__main__":
116124
unittest.main()

pythonosc/test/test_udp_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,30 @@ def test_context_manager(self, mock_socket_ctor):
6464
self.assertTrue(mock_socket.close.called)
6565

6666

67+
class TestUdpClientTimeout(unittest.TestCase):
68+
@mock.patch("socket.socket")
69+
def test_init_timeout(self, mock_socket_ctor):
70+
mock_socket = mock_socket_ctor.return_value
71+
client = udp_client.UDPClient("::1", 31337, timeout=10.0)
72+
self.assertEqual(client._timeout, 10.0)
73+
mock_socket.settimeout.assert_any_call(10.0)
74+
75+
@mock.patch("socket.socket")
76+
def test_receive_default_timeout(self, mock_socket_ctor):
77+
mock_socket = mock_socket_ctor.return_value
78+
client = udp_client.UDPClient("::1", 31337, timeout=10.0)
79+
mock_socket.recv.return_value = b"data"
80+
client.receive()
81+
mock_socket.settimeout.assert_called_with(10.0)
82+
83+
@mock.patch("socket.socket")
84+
def test_receive_override_timeout(self, mock_socket_ctor):
85+
mock_socket = mock_socket_ctor.return_value
86+
client = udp_client.UDPClient("::1", 31337, timeout=10.0)
87+
mock_socket.recv.return_value = b"data"
88+
client.receive(timeout=5.0)
89+
mock_socket.settimeout.assert_called_with(5.0)
90+
91+
6792
if __name__ == "__main__":
6893
unittest.main()

pythonosc/udp_client.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(
2525
port: int,
2626
allow_broadcast: bool = False,
2727
family: socket.AddressFamily = socket.AF_UNSPEC,
28+
timeout: float | None = None,
2829
) -> None:
2930
"""Initialize client
3031
@@ -36,6 +37,7 @@ def __init__(
3637
port: Port of server
3738
allow_broadcast: Allow for broadcast transmissions
3839
family: address family parameter (passed to socket.getaddrinfo)
40+
timeout: Default timeout in seconds for socket operations
3941
"""
4042

4143
for addr in socket.getaddrinfo(
@@ -50,6 +52,10 @@ def __init__(
5052
break
5153

5254
self._sock.setblocking(False)
55+
if timeout is not None:
56+
self._sock.settimeout(timeout)
57+
self._timeout = timeout
58+
5359
if allow_broadcast:
5460
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
5561
self._address = address
@@ -75,16 +81,21 @@ def send(self, content: Union[OscMessage, OscBundle]) -> None:
7581
"""
7682
self._sock.sendto(content.dgram, (self._address, self._port))
7783

78-
def receive(self, timeout: int = 30) -> bytes:
84+
def receive(self, timeout: float | None = None) -> bytes:
7985
"""Wait :int:`timeout` seconds for a message an return the raw bytes
8086
8187
Args:
82-
timeout: Number of seconds to wait for a message
88+
timeout: Number of seconds to wait for a message.
89+
If None, uses the default timeout set in __init__.
8390
"""
84-
self._sock.settimeout(timeout)
91+
if timeout is not None:
92+
self._sock.settimeout(timeout)
93+
elif self._timeout is not None:
94+
self._sock.settimeout(self._timeout)
95+
8596
try:
8697
return self._sock.recv(4096)
87-
except TimeoutError:
98+
except (TimeoutError, socket.timeout, BlockingIOError):
8899
return b""
89100

90101

@@ -111,11 +122,12 @@ def send_message(
111122
msg = builder.build()
112123
self.send(msg)
113124

114-
def get_messages(self, timeout: int = 30) -> Generator:
125+
def get_messages(self, timeout: float | None = None) -> Generator:
115126
"""Wait :int:`timeout` seconds for a message from the server and convert it to a :class:`OscMessage`
116127
117128
Args:
118-
timeout: Time in seconds to wait for a message
129+
timeout: Time in seconds to wait for a message.
130+
If None, uses the default timeout set in __init__.
119131
"""
120132
msg = self.receive(timeout)
121133
while msg:
@@ -128,12 +140,13 @@ class DispatchClient(SimpleUDPClient):
128140

129141
dispatcher = Dispatcher()
130142

131-
def handle_messages(self, timeout: int = 30) -> None:
143+
def handle_messages(self, timeout: float | None = None) -> None:
132144
"""Wait :int:`timeout` seconds for a message from the server and process each message with the registered
133145
handlers. Continue until a timeout occurs.
134146
135147
Args:
136-
timeout: Time in seconds to wait for a message
148+
timeout: Time in seconds to wait for a message.
149+
If None, uses the default timeout set in __init__.
137150
"""
138151
msg = self.receive(timeout)
139152
while msg:

0 commit comments

Comments
 (0)