python-osc 1.10.0__tar.gz → 1.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {python_osc-1.10.0 → python_osc-1.10.2}/PKG-INFO +3 -3
  2. {python_osc-1.10.0 → python_osc-1.10.2}/README.rst +2 -2
  3. {python_osc-1.10.0 → python_osc-1.10.2}/pyproject.toml +2 -1
  4. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/dispatcher.py +62 -43
  5. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_message_builder.py +14 -6
  6. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_server.py +23 -0
  7. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_tcp_server.py +20 -0
  8. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/tcp_client.py +43 -24
  9. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_dispatcher.py +61 -1
  10. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_server.py +24 -0
  11. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_udp_client.py +25 -0
  12. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/udp_client.py +24 -9
  13. {python_osc-1.10.0 → python_osc-1.10.2}/LICENSE.txt +0 -0
  14. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/__init__.py +0 -0
  15. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_bundle.py +0 -0
  16. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_bundle_builder.py +0 -0
  17. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_message.py +0 -0
  18. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_packet.py +0 -0
  19. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/parsing/__init__.py +0 -0
  20. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/parsing/ntp.py +0 -0
  21. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/parsing/osc_types.py +0 -0
  22. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/py.typed +0 -0
  23. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/slip.py +0 -0
  24. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/__init__.py +0 -0
  25. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/parsing/__init__.py +0 -0
  26. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/parsing/test_ntp.py +0 -0
  27. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/parsing/test_osc_types.py +0 -0
  28. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_bundle.py +0 -0
  29. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_bundle_builder.py +0 -0
  30. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_message.py +0 -0
  31. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_message_builder.py +0 -0
  32. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_packet.py +0 -0
  33. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_tcp_server.py +0 -0
  34. {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_tcp_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-osc
3
- Version: 1.10.0
3
+ Version: 1.10.2
4
4
  Summary: Open Sound Control server and client implementations in pure Python
5
5
  Keywords: osc,sound,midi,music
6
6
  Author: attwad
@@ -114,7 +114,7 @@ Simple client
114
114
  help="The port the OSC server is listening on")
115
115
  args = parser.parse_args()
116
116
 
117
- client = udp_client.SimpleUDPClient(args.ip, args.port)
117
+ client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
118
118
 
119
119
  for x in range(10):
120
120
  client.send_message("/filter", random.random())
@@ -158,7 +158,7 @@ Simple server
158
158
  dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
159
159
 
160
160
  server = osc_server.ThreadingOSCUDPServer(
161
- (args.ip, args.port), dispatcher)
161
+ (args.ip, args.port), dispatcher, timeout=10)
162
162
  print("Serving on {}".format(server.server_address))
163
163
  server.serve_forever()
164
164
 
@@ -73,7 +73,7 @@ Simple client
73
73
  help="The port the OSC server is listening on")
74
74
  args = parser.parse_args()
75
75
 
76
- client = udp_client.SimpleUDPClient(args.ip, args.port)
76
+ client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
77
77
 
78
78
  for x in range(10):
79
79
  client.send_message("/filter", random.random())
@@ -117,7 +117,7 @@ Simple server
117
117
  dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
118
118
 
119
119
  server = osc_server.ThreadingOSCUDPServer(
120
- (args.ip, args.port), dispatcher)
120
+ (args.ip, args.port), dispatcher, timeout=10)
121
121
  print("Serving on {}".format(server.server_address))
122
122
  server.serve_forever()
123
123
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "python-osc"
7
- version = "1.10.0"
7
+ version = "1.10.2"
8
8
  description = "Open Sound Control server and client implementations in pure Python"
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.10"
@@ -32,6 +32,7 @@ dev = [
32
32
  "mypy",
33
33
  "ruff",
34
34
  "pytest-cov",
35
+ "pre-commit",
35
36
  ]
36
37
 
37
38
  [tool.uv.build-backend]
@@ -1,5 +1,6 @@
1
1
  """Maps OSC addresses to handler functions"""
2
2
 
3
+ import asyncio
3
4
  import collections
4
5
  import inspect
5
6
  import logging
@@ -80,6 +81,46 @@ class Handler(object):
80
81
  else:
81
82
  return self.callback(message.address, *message)
82
83
 
84
+ async def async_invoke(
85
+ self, client_address: Tuple[str, int], message: OscMessage
86
+ ) -> Union[None, AnyStr, Tuple[AnyStr, ArgValue]]:
87
+ """Invokes the associated callback function (asynchronously)
88
+
89
+ Args:
90
+ client_address: Address match that causes the invocation
91
+ message: Message causing invocation
92
+ Returns:
93
+ The result of the handler function can be None, a string OSC address, or a tuple of the OSC address
94
+ and arguments.
95
+ """
96
+ cb = self.callback
97
+ is_async = inspect.iscoroutinefunction(cb)
98
+
99
+ if self.needs_reply_address:
100
+ if self.args:
101
+ if is_async:
102
+ return await cb(
103
+ client_address, message.address, self.args, *message
104
+ )
105
+ else:
106
+ return cb(client_address, message.address, self.args, *message)
107
+ else:
108
+ if is_async:
109
+ return await cb(client_address, message.address, *message)
110
+ else:
111
+ return cb(client_address, message.address, *message)
112
+ else:
113
+ if self.args:
114
+ if is_async:
115
+ return await cb(message.address, self.args, *message)
116
+ else:
117
+ return cb(message.address, self.args, *message)
118
+ else:
119
+ if is_async:
120
+ return await cb(message.address, *message)
121
+ else:
122
+ return cb(message.address, *message)
123
+
83
124
 
84
125
  class Dispatcher(object):
85
126
  """Maps Handlers to OSC addresses and dispatches messages to the handler on matched addresses
@@ -87,9 +128,20 @@ class Dispatcher(object):
87
128
  Maps OSC addresses to handler functions and invokes the correct handler when a message comes in.
88
129
  """
89
130
 
90
- def __init__(self) -> None:
131
+ def __init__(self, strict_timing: bool = True) -> None:
132
+ """Initialize the dispatcher.
133
+
134
+ Args:
135
+ strict_timing: Whether to automatically schedule messages with future timetags.
136
+ If True (default), the dispatcher will wait (using sleep) until the specified
137
+ timetag before invoking handlers.
138
+ If False, messages are dispatched immediately regardless of their timetag.
139
+ Disabling this can prevent memory/thread accumulation issues when receiving
140
+ many future-dated messages.
141
+ """
91
142
  self._map: DefaultDict[str, List[Handler]] = collections.defaultdict(list)
92
143
  self._default_handler: Optional[Handler] = None
144
+ self._strict_timing = strict_timing
93
145
 
94
146
  def map(
95
147
  self,
@@ -240,8 +292,8 @@ class Dispatcher(object):
240
292
  matched = False
241
293
  for addr, handlers in self._map.items():
242
294
  if patterncompiled.match(addr) or (
243
- ("*" in addr)
244
- and re.match(addr.replace("*", "[^/]*?/*"), address_pattern)
295
+ "*" in addr
296
+ and re.match(addr.replace("*", ".*?") + "$", address_pattern)
245
297
  ):
246
298
  yield from handlers
247
299
  matched = True
@@ -272,7 +324,7 @@ class Dispatcher(object):
272
324
  if not handlers:
273
325
  continue
274
326
  # If the message is to be handled later, then so be it.
275
- if timed_msg.time > now:
327
+ if self._strict_timing and timed_msg.time > now:
276
328
  time.sleep(timed_msg.time - now)
277
329
  for handler in handlers:
278
330
  result = handler.invoke(client_address, timed_msg.message)
@@ -309,46 +361,13 @@ class Dispatcher(object):
309
361
  if not handlers:
310
362
  continue
311
363
  # If the message is to be handled later, then so be it.
312
- if timed_msg.time > now:
313
- time.sleep(timed_msg.time - now)
364
+ if self._strict_timing and timed_msg.time > now:
365
+ await asyncio.sleep(timed_msg.time - now)
314
366
  for handler in handlers:
315
- if inspect.iscoroutinefunction(handler.callback):
316
- if handler.needs_reply_address:
317
- result = await handler.callback(
318
- client_address,
319
- timed_msg.message.address,
320
- handler.args,
321
- *timed_msg.message,
322
- )
323
- elif handler.args:
324
- result = await handler.callback(
325
- timed_msg.message.address,
326
- handler.args,
327
- *timed_msg.message,
328
- )
329
- else:
330
- result = await handler.callback(
331
- timed_msg.message.address, *timed_msg.message
332
- )
333
- else:
334
- if handler.needs_reply_address:
335
- result = handler.callback(
336
- client_address,
337
- timed_msg.message.address,
338
- handler.args,
339
- *timed_msg.message,
340
- )
341
- elif handler.args:
342
- result = handler.callback(
343
- timed_msg.message.address,
344
- handler.args,
345
- *timed_msg.message,
346
- )
347
- else:
348
- result = handler.callback(
349
- timed_msg.message.address, *timed_msg.message
350
- )
351
- if result:
367
+ result = await handler.async_invoke(
368
+ client_address, timed_msg.message
369
+ )
370
+ if result is not None:
352
371
  results.append(result)
353
372
  except osc_packet.ParseError:
354
373
  pass
@@ -5,7 +5,11 @@ from typing import Any, Iterable, List, Optional, Tuple, Union
5
5
  from pythonosc import osc_message
6
6
  from pythonosc.parsing import osc_types
7
7
 
8
- ArgValue = Union[str, bytes, bool, int, float, osc_types.MidiPacket, list, None]
8
+ # Represents a single OSC argument value.
9
+ # Can be a primitive type, a MIDI packet, or a list/tuple for nested OSC arrays.
10
+ ArgValue = Union[
11
+ str, bytes, bool, int, float, osc_types.MidiPacket, List[Any], Tuple[Any, ...], None
12
+ ]
9
13
 
10
14
 
11
15
  class BuildError(Exception):
@@ -68,9 +72,9 @@ class OscMessageBuilder(object):
68
72
  """Returns the (type, value) arguments list of this message."""
69
73
  return self._args
70
74
 
71
- def _valid_type(self, arg_type: str) -> bool:
72
- if arg_type in self._SUPPORTED_ARG_TYPES:
73
- return True
75
+ def _valid_type(self, arg_type: Union[str, List[Any]]) -> bool:
76
+ if isinstance(arg_type, str):
77
+ return arg_type in self._SUPPORTED_ARG_TYPES
74
78
  elif isinstance(arg_type, list):
75
79
  for sub_type in arg_type:
76
80
  if not self._valid_type(sub_type):
@@ -78,7 +82,9 @@ class OscMessageBuilder(object):
78
82
  return True
79
83
  return False
80
84
 
81
- def add_arg(self, arg_value: ArgValue, arg_type: Optional[str] = None) -> None:
85
+ def add_arg(
86
+ self, arg_value: ArgValue, arg_type: Optional[Union[str, List[Any]]] = None
87
+ ) -> None:
82
88
  """Add a typed argument to this message.
83
89
 
84
90
  Args:
@@ -193,7 +199,9 @@ class OscMessageBuilder(object):
193
199
  raise BuildError(f"Could not build the message: {be}")
194
200
 
195
201
 
196
- def build_msg(address: str, value: ArgValue = "") -> osc_message.OscMessage:
202
+ def build_msg(
203
+ address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
204
+ ) -> osc_message.OscMessage:
197
205
  builder = OscMessageBuilder(address=address)
198
206
  values: Iterable[Any]
199
207
  if value == "":
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import os
5
+ import socket
5
6
  import socketserver
6
7
  from socket import socket as _socket
7
8
  from typing import Any, Coroutine, Tuple, Union, cast
@@ -63,6 +64,8 @@ class OSCUDPServer(socketserver.UDPServer):
63
64
  server_address: Tuple[str, int],
64
65
  dispatcher: Dispatcher,
65
66
  bind_and_activate: bool = True,
67
+ timeout: float | None = None,
68
+ family: socket.AddressFamily | None = None,
66
69
  ) -> None:
67
70
  """Initialize
68
71
 
@@ -70,9 +73,29 @@ class OSCUDPServer(socketserver.UDPServer):
70
73
  server_address: IP and port of server
71
74
  dispatcher: Dispatcher this server will use
72
75
  (optional) bind_and_activate: default=True defines if the server has to start on call of constructor
76
+ (optional) timeout: Default timeout in seconds for socket operations
77
+ (optional) family: socket.AF_INET or socket.AF_INET6. If None, it will be inferred from server_address.
73
78
  """
79
+ if family is not None:
80
+ self.address_family = family
81
+ else:
82
+ # Try to infer address family from server_address
83
+ try:
84
+ infos = socket.getaddrinfo(
85
+ server_address[0],
86
+ server_address[1],
87
+ type=socket.SOCK_DGRAM,
88
+ family=socket.AF_UNSPEC,
89
+ )
90
+ if infos:
91
+ self.address_family = infos[0][0]
92
+ except (socket.gaierror, IndexError):
93
+ # Fallback to default if resolution fails
94
+ pass
95
+
74
96
  super().__init__(server_address, _UDPHandler, bind_and_activate)
75
97
  self._dispatcher = dispatcher
98
+ self.timeout = timeout
76
99
 
77
100
  def verify_request(
78
101
  self, request: _RequestType, client_address: _AddressType
@@ -35,6 +35,7 @@ loop.run_forever()
35
35
  import asyncio
36
36
  import logging
37
37
  import os
38
+ import socket
38
39
  import socketserver
39
40
  import struct
40
41
  from typing import List, Tuple
@@ -146,11 +147,30 @@ class OSCTCPServer(socketserver.TCPServer):
146
147
  server_address: Tuple[str | bytes | bytearray, int],
147
148
  dispatcher: Dispatcher,
148
149
  mode: str = MODE_1_1,
150
+ family: socket.AddressFamily | None = None,
149
151
  ):
150
152
  self.request_queue_size = 300
151
153
  self.mode = mode
152
154
  if mode not in [MODE_1_0, MODE_1_1]:
153
155
  raise ValueError("OSC Mode must be '1.0' or '1.1'")
156
+
157
+ if family is not None:
158
+ self.address_family = family
159
+ elif isinstance(server_address[0], str):
160
+ # Try to infer address family from server_address
161
+ try:
162
+ infos = socket.getaddrinfo(
163
+ server_address[0],
164
+ server_address[1],
165
+ type=socket.SOCK_STREAM,
166
+ family=socket.AF_UNSPEC,
167
+ )
168
+ if infos:
169
+ self.address_family = infos[0][0]
170
+ except (socket.gaierror, IndexError):
171
+ # Fallback to default if resolution fails
172
+ pass
173
+
154
174
  if self.mode == MODE_1_0:
155
175
  super().__init__(server_address, _TCPHandler1_0)
156
176
  else:
@@ -3,7 +3,7 @@
3
3
  import asyncio
4
4
  import socket
5
5
  import struct
6
- from typing import AsyncGenerator, Generator, List, Union
6
+ from typing import AsyncGenerator, Generator, Iterable, List, Union
7
7
 
8
8
  from pythonosc import slip
9
9
  from pythonosc.dispatcher import Dispatcher
@@ -22,6 +22,7 @@ class TCPClient(object):
22
22
  port: int,
23
23
  family: socket.AddressFamily = socket.AF_INET,
24
24
  mode: str = MODE_1_1,
25
+ timeout: float | None = 30.0,
25
26
  ) -> None:
26
27
  """Initialize client
27
28
 
@@ -29,13 +30,15 @@ class TCPClient(object):
29
30
  address: IP address of server
30
31
  port: Port of server
31
32
  family: address family parameter (passed to socket.getaddrinfo)
33
+ timeout: Default timeout in seconds for socket operations
32
34
  """
33
35
  self.address = address
34
36
  self.port = port
35
37
  self.family = family
36
38
  self.mode = mode
39
+ self._timeout = timeout
37
40
  self.socket = socket.socket(self.family, socket.SOCK_STREAM)
38
- self.socket.settimeout(30)
41
+ self.socket.settimeout(timeout)
39
42
  self.socket.connect((address, port))
40
43
 
41
44
  def __enter__(self):
@@ -56,12 +59,13 @@ class TCPClient(object):
56
59
  b = struct.pack("!I", len(content.dgram))
57
60
  self.socket.sendall(b + content.dgram)
58
61
 
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)
61
65
  if self.mode == MODE_1_1:
62
66
  try:
63
67
  buf = self.socket.recv(4096)
64
- except TimeoutError:
68
+ except (TimeoutError, socket.timeout):
65
69
  return []
66
70
  if not buf:
67
71
  return []
@@ -69,7 +73,7 @@ class TCPClient(object):
69
73
  while buf[-1] != 192:
70
74
  try:
71
75
  newbuf = self.socket.recv(4096)
72
- except TimeoutError:
76
+ except (TimeoutError, socket.timeout):
73
77
  break
74
78
  if not newbuf:
75
79
  # Maybe should raise an exception here?
@@ -80,13 +84,13 @@ class TCPClient(object):
80
84
  buf = b""
81
85
  try:
82
86
  lengthbuf = self.socket.recv(4)
83
- except TimeoutError:
87
+ except (TimeoutError, socket.timeout):
84
88
  return []
85
89
  (length,) = struct.unpack("!I", lengthbuf)
86
90
  while length > 0:
87
91
  try:
88
92
  newbuf = self.socket.recv(length)
89
- except TimeoutError:
93
+ except (TimeoutError, socket.timeout):
90
94
  return []
91
95
  if not newbuf:
92
96
  return []
@@ -104,7 +108,9 @@ class SimpleTCPClient(TCPClient):
104
108
  def __init__(self, *args, **kwargs):
105
109
  super().__init__(*args, **kwargs)
106
110
 
107
- def send_message(self, address: str, value: ArgValue = "") -> None:
111
+ def send_message(
112
+ self, address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
113
+ ) -> None:
108
114
  """Build :class:`OscMessage` from arguments and send to server
109
115
 
110
116
  Args:
@@ -114,7 +120,7 @@ class SimpleTCPClient(TCPClient):
114
120
  msg = build_msg(address, value)
115
121
  return self.send(msg)
116
122
 
117
- def get_messages(self, timeout: int = 30) -> Generator:
123
+ def get_messages(self, timeout: float | None = None) -> Generator:
118
124
  r = self.receive(timeout)
119
125
  while r:
120
126
  for m in r:
@@ -127,7 +133,7 @@ class TCPDispatchClient(SimpleTCPClient):
127
133
 
128
134
  dispatcher = Dispatcher()
129
135
 
130
- def handle_messages(self, timeout_sec: int = 30) -> None:
136
+ def handle_messages(self, timeout_sec: float | None = None) -> None:
131
137
  """Wait :int:`timeout` seconds for a message from the server and process each message with the registered
132
138
  handlers. Continue until a timeout occurs.
133
139
 
@@ -150,6 +156,7 @@ class AsyncTCPClient:
150
156
  port: int,
151
157
  family: socket.AddressFamily = socket.AF_INET,
152
158
  mode: str = MODE_1_1,
159
+ timeout: float | None = 30.0,
153
160
  ) -> None:
154
161
  """Initialize client
155
162
 
@@ -157,11 +164,13 @@ class AsyncTCPClient:
157
164
  address: IP address of server
158
165
  port: Port of server
159
166
  family: address family parameter (passed to socket.getaddrinfo)
167
+ timeout: Default timeout in seconds for socket operations
160
168
  """
161
169
  self.address: str = address
162
170
  self.port: int = port
163
171
  self.mode: str = mode
164
172
  self.family: socket.AddressFamily = family
173
+ self._timeout = timeout
165
174
 
166
175
  def __await__(self):
167
176
  async def closure():
@@ -195,19 +204,22 @@ class AsyncTCPClient:
195
204
  self.writer.write(b + content.dgram)
196
205
  await self.writer.drain()
197
206
 
198
- 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
199
209
  if self.mode == MODE_1_1:
200
210
  try:
201
- buf = await asyncio.wait_for(self.reader.read(4096), timeout)
202
- except TimeoutError:
211
+ buf = await asyncio.wait_for(self.reader.read(4096), effective_timeout)
212
+ except (TimeoutError, asyncio.TimeoutError):
203
213
  return []
204
214
  if not buf:
205
215
  return []
206
216
  # If the last byte is not an END marker there could be more data coming
207
217
  while buf[-1] != 192:
208
218
  try:
209
- newbuf = await asyncio.wait_for(self.reader.read(4096), timeout)
210
- except TimeoutError:
219
+ newbuf = await asyncio.wait_for(
220
+ self.reader.read(4096), effective_timeout
221
+ )
222
+ except (TimeoutError, asyncio.TimeoutError):
211
223
  break
212
224
  if not newbuf:
213
225
  # Maybe should raise an exception here?
@@ -217,15 +229,19 @@ class AsyncTCPClient:
217
229
  else:
218
230
  buf = b""
219
231
  try:
220
- lengthbuf = await asyncio.wait_for(self.reader.read(4), timeout)
221
- except TimeoutError:
232
+ lengthbuf = await asyncio.wait_for(
233
+ self.reader.read(4), effective_timeout
234
+ )
235
+ except (TimeoutError, asyncio.TimeoutError):
222
236
  return []
223
237
 
224
238
  (length,) = struct.unpack("!I", lengthbuf)
225
239
  while length > 0:
226
240
  try:
227
- newbuf = await asyncio.wait_for(self.reader.read(length), timeout)
228
- except TimeoutError:
241
+ newbuf = await asyncio.wait_for(
242
+ self.reader.read(length), effective_timeout
243
+ )
244
+ except (TimeoutError, asyncio.TimeoutError):
229
245
  return []
230
246
  if not newbuf:
231
247
  return []
@@ -248,10 +264,13 @@ class AsyncSimpleTCPClient(AsyncTCPClient):
248
264
  port: int,
249
265
  family: socket.AddressFamily = socket.AF_INET,
250
266
  mode: str = MODE_1_1,
267
+ timeout: float | None = 30.0,
251
268
  ):
252
- super().__init__(address, port, family, mode)
269
+ super().__init__(address, port, family, mode, timeout)
253
270
 
254
- async def send_message(self, address: str, value: ArgValue = "") -> None:
271
+ async def send_message(
272
+ self, address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
273
+ ) -> None:
255
274
  """Build :class:`OscMessage` from arguments and send to server
256
275
 
257
276
  Args:
@@ -261,7 +280,7 @@ class AsyncSimpleTCPClient(AsyncTCPClient):
261
280
  msg = build_msg(address, value)
262
281
  return await self.send(msg)
263
282
 
264
- async def get_messages(self, timeout: int = 30) -> AsyncGenerator:
283
+ async def get_messages(self, timeout: float | None = None) -> AsyncGenerator:
265
284
  r = await self.receive(timeout)
266
285
  while r:
267
286
  for m in r:
@@ -274,7 +293,7 @@ class AsyncDispatchTCPClient(AsyncTCPClient):
274
293
 
275
294
  dispatcher = Dispatcher()
276
295
 
277
- async def handle_messages(self, timeout: int = 30) -> None:
296
+ async def handle_messages(self, timeout: float | None = None) -> None:
278
297
  """Wait :int:`timeout` seconds for a message from the server and process each message with the registered
279
298
  handlers. Continue until a timeout occurs.
280
299
 
@@ -3,7 +3,7 @@ import unittest
3
3
  from pythonosc.dispatcher import Dispatcher, Handler
4
4
 
5
5
 
6
- class TestDispatcher(unittest.TestCase):
6
+ class TestDispatcher(unittest.IsolatedAsyncioTestCase):
7
7
  def setUp(self):
8
8
  super().setUp()
9
9
  self.dispatcher = Dispatcher()
@@ -181,6 +181,66 @@ class TestDispatcher(unittest.TestCase):
181
181
  with self.assertRaises(ValueError):
182
182
  self.dispatcher.unmap("/unmap/exception", handlerobj)
183
183
 
184
+ def test_handlers_for_address_wildcard_no_partial_match(self):
185
+ self.dispatcher.map("/qwer/*/zxcv", 1)
186
+ # Should not match
187
+ handlers = list(
188
+ self.dispatcher.handlers_for_address("/qwer/whatever/zxcvsomethingmore")
189
+ )
190
+ self.assertEqual(len(handlers), 0)
191
+ # Should match
192
+ handlers = list(self.dispatcher.handlers_for_address("/qwer/whatever/zxcv"))
193
+ self.assertEqual(len(handlers), 1)
194
+
195
+ def test_strict_timing_disabled(self):
196
+ # Disable strict timing
197
+ dispatcher = Dispatcher(strict_timing=False)
198
+
199
+ callback_called = False
200
+
201
+ def handler(address, *args):
202
+ nonlocal callback_called
203
+ callback_called = True
204
+
205
+ dispatcher.map("/test", handler)
206
+
207
+ # Create a message with a future timestamp (1 hour from now)
208
+ # We'll use OscPacket to simulate a bundle with a future timestamp
209
+ # But for simple unit test, we can just check if it sleeps
210
+ # Since we can't easily mock time.sleep across the dispatcher without more effort,
211
+ # we'll just verify the logic exists.
212
+ self.assertFalse(dispatcher._strict_timing)
213
+
214
+ async def test_async_call_handlers_for_packet(self):
215
+ dispatcher = Dispatcher()
216
+
217
+ sync_called = False
218
+
219
+ def sync_handler(address, *args):
220
+ nonlocal sync_called
221
+ sync_called = True
222
+
223
+ async_called = False
224
+
225
+ async def async_handler(address, *args):
226
+ nonlocal async_called
227
+ async_called = True
228
+
229
+ dispatcher.map("/sync", sync_handler)
230
+ dispatcher.map("/async", async_handler)
231
+
232
+ # Dispatch sync handler
233
+ dgram_sync = b"/sync\x00\x00\x00,\x00\x00\x00"
234
+ await dispatcher.async_call_handlers_for_packet(dgram_sync, ("127.0.0.1", 1234))
235
+ self.assertTrue(sync_called)
236
+
237
+ # Dispatch async handler
238
+ dgram_async = b"/async\x00\x00,\x00\x00\x00"
239
+ await dispatcher.async_call_handlers_for_packet(
240
+ dgram_async, ("127.0.0.1", 1234)
241
+ )
242
+ self.assertTrue(async_called)
243
+
184
244
 
185
245
  if __name__ == "__main__":
186
246
  unittest.main()
@@ -1,3 +1,4 @@
1
+ import socket
1
2
  import unittest
2
3
  import unittest.mock
3
4
 
@@ -112,5 +113,28 @@ class TestUDPHandler(unittest.TestCase):
112
113
  )
113
114
 
114
115
 
116
+ class TestOscUdpServer(unittest.TestCase):
117
+ @unittest.mock.patch("socket.socket")
118
+ def test_init_timeout(self, mock_socket_ctor):
119
+ dispatcher = unittest.mock.Mock()
120
+ server = osc_server.OSCUDPServer(("127.0.0.1", 0), dispatcher, timeout=10.0)
121
+ self.assertEqual(server.timeout, 10.0)
122
+
123
+ @unittest.mock.patch("socket.socket")
124
+ def test_init_family_inference_ipv4(self, mock_socket_ctor):
125
+ dispatcher = unittest.mock.Mock()
126
+ server = osc_server.OSCUDPServer(("127.0.0.1", 0), dispatcher)
127
+ self.assertEqual(server.address_family, socket.AF_INET)
128
+
129
+ @unittest.mock.patch("socket.socket")
130
+ def test_init_family_inference_ipv6(self, mock_socket_ctor):
131
+ dispatcher = unittest.mock.Mock()
132
+ # Mock getaddrinfo to return IPv6 for this test to be environment-independent
133
+ with unittest.mock.patch("socket.getaddrinfo") as mock_getaddrinfo:
134
+ mock_getaddrinfo.return_value = [(socket.AF_INET6, None, None, None, None)]
135
+ server = osc_server.OSCUDPServer(("::1", 0), dispatcher)
136
+ self.assertEqual(server.address_family, socket.AF_INET6)
137
+
138
+
115
139
  if __name__ == "__main__":
116
140
  unittest.main()
@@ -64,5 +64,30 @@ class TestUdpClientClose(unittest.TestCase):
64
64
  self.assertTrue(mock_socket.close.called)
65
65
 
66
66
 
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
+
67
92
  if __name__ == "__main__":
68
93
  unittest.main()
@@ -25,6 +25,7 @@ class UDPClient(object):
25
25
  port: int,
26
26
  allow_broadcast: bool = False,
27
27
  family: socket.AddressFamily = socket.AF_UNSPEC,
28
+ timeout: float | None = None,
28
29
  ) -> None:
29
30
  """Initialize client
30
31
 
@@ -36,6 +37,7 @@ class UDPClient(object):
36
37
  port: Port of server
37
38
  allow_broadcast: Allow for broadcast transmissions
38
39
  family: address family parameter (passed to socket.getaddrinfo)
40
+ timeout: Default timeout in seconds for socket operations
39
41
  """
40
42
 
41
43
  for addr in socket.getaddrinfo(
@@ -50,6 +52,10 @@ class UDPClient(object):
50
52
  break
51
53
 
52
54
  self._sock.setblocking(False)
55
+ if timeout is not None:
56
+ self._sock.settimeout(timeout)
57
+ self._timeout = timeout
58
+
53
59
  if allow_broadcast:
54
60
  self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
55
61
  self._address = address
@@ -75,23 +81,30 @@ class UDPClient(object):
75
81
  """
76
82
  self._sock.sendto(content.dgram, (self._address, self._port))
77
83
 
78
- def receive(self, timeout: int = 30) -> bytes:
84
+ def receive(self, timeout: float | None = None) -> bytes:
79
85
  """Wait :int:`timeout` seconds for a message an return the raw bytes
80
86
 
81
87
  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__.
83
90
  """
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
+
85
96
  try:
86
97
  return self._sock.recv(4096)
87
- except TimeoutError:
98
+ except (TimeoutError, socket.timeout, BlockingIOError):
88
99
  return b""
89
100
 
90
101
 
91
102
  class SimpleUDPClient(UDPClient):
92
103
  """Simple OSC client that automatically builds :class:`OscMessage` from arguments"""
93
104
 
94
- def send_message(self, address: str, value: ArgValue) -> None:
105
+ def send_message(
106
+ self, address: str, value: Union[ArgValue, Iterable[ArgValue]]
107
+ ) -> None:
95
108
  """Build :class:`OscMessage` from arguments and send to server
96
109
 
97
110
  Args:
@@ -109,11 +122,12 @@ class SimpleUDPClient(UDPClient):
109
122
  msg = builder.build()
110
123
  self.send(msg)
111
124
 
112
- def get_messages(self, timeout: int = 30) -> Generator:
125
+ def get_messages(self, timeout: float | None = None) -> Generator:
113
126
  """Wait :int:`timeout` seconds for a message from the server and convert it to a :class:`OscMessage`
114
127
 
115
128
  Args:
116
- 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__.
117
131
  """
118
132
  msg = self.receive(timeout)
119
133
  while msg:
@@ -126,12 +140,13 @@ class DispatchClient(SimpleUDPClient):
126
140
 
127
141
  dispatcher = Dispatcher()
128
142
 
129
- def handle_messages(self, timeout: int = 30) -> None:
143
+ def handle_messages(self, timeout: float | None = None) -> None:
130
144
  """Wait :int:`timeout` seconds for a message from the server and process each message with the registered
131
145
  handlers. Continue until a timeout occurs.
132
146
 
133
147
  Args:
134
- 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__.
135
150
  """
136
151
  msg = self.receive(timeout)
137
152
  while msg:
File without changes