Repository: attwad/python-osc

Issue:
Add support for 64 bits double type
`unhandled type: d` warnings are all that gets returned, no handlers even end up running.

Discussion:

Patch:
diff --git a/pythonosc/osc_message.py b/pythonosc/osc_message.py
index 911dbcf..b89a0d6 100644
--- a/pythonosc/osc_message.py
+++ b/pythonosc/osc_message.py
@@ -41,6 +41,8 @@ class OscMessage(object):
           val, index = osc_types.get_int(self._dgram, index)
         elif param == "f":  # Float.
           val, index = osc_types.get_float(self._dgram, index)
+        elif param == "d":  # Double.
+          val, index = osc_types.get_double(self._dgram, index)
         elif param == "s":  # String.
           val, index = osc_types.get_string(self._dgram, index)
         elif param == "b":  # Blob.
diff --git a/pythonosc/osc_message_builder.py b/pythonosc/osc_message_builder.py
index 0f9bfba..28128fb 100644
--- a/pythonosc/osc_message_builder.py
+++ b/pythonosc/osc_message_builder.py
@@ -12,6 +12,7 @@ class OscMessageBuilder(object):
   """Builds arbitrary OscMessage instances."""
 
   ARG_TYPE_FLOAT = "f"
+  ARG_TYPE_DOUBLE = "d"
   ARG_TYPE_INT = "i"
   ARG_TYPE_STRING = "s"
   ARG_TYPE_BLOB = "b"
@@ -24,8 +25,8 @@ class OscMessageBuilder(object):
   ARG_TYPE_ARRAY_STOP = "]"
 
   _SUPPORTED_ARG_TYPES = (
-      ARG_TYPE_FLOAT, ARG_TYPE_INT, ARG_TYPE_BLOB, ARG_TYPE_STRING, ARG_TYPE_RGBA,
-      ARG_TYPE_MIDI, ARG_TYPE_TRUE, ARG_TYPE_FALSE)
+      ARG_TYPE_FLOAT, ARG_TYPE_DOUBLE, ARG_TYPE_INT, ARG_TYPE_BLOB, ARG_TYPE_STRING,
+      ARG_TYPE_RGBA, ARG_TYPE_MIDI, ARG_TYPE_TRUE, ARG_TYPE_FALSE)
 
   def __init__(self, address=None):
     """Initialize a new builder for a message.
@@ -143,6 +144,8 @@ class OscMessageBuilder(object):
           dgram += osc_types.write_int(value)
         elif arg_type == self.ARG_TYPE_FLOAT:
           dgram += osc_types.write_float(value)
+        elif arg_type == self.ARG_TYPE_DOUBLE:
+          dgram += osc_types.write_double(value)
         elif arg_type == self.ARG_TYPE_BLOB:
           dgram += osc_types.write_blob(value)
         elif arg_type == self.ARG_TYPE_RGBA:
diff --git a/pythonosc/parsing/osc_types.py b/pythonosc/parsing/osc_types.py
index a91003b..5558399 100644
--- a/pythonosc/parsing/osc_types.py
+++ b/pythonosc/parsing/osc_types.py
@@ -21,6 +21,7 @@ IMMEDIATELY = 0
 # Datagram length in bytes for types that have a fixed size.
 _INT_DGRAM_LEN = 4
 _FLOAT_DGRAM_LEN = 4
+_DOUBLE_DGRAM_LEN = 8
 _DATE_DGRAM_LEN = _INT_DGRAM_LEN * 2
 # Strings and blob dgram length is always a multiple of 4 bytes.
 _STRING_DGRAM_PAD = 4
@@ -199,6 +200,42 @@ def get_float(dgram, start_index):
     raise ParseError('Could not parse datagram %s' % e)
 
 
+def write_double(val):
+  """Returns the datagram for the given double parameter value
+
+  Raises:
+    - BuildError if the double could not be converted.
+  """
+  try:
+    return struct.pack('>d', val)
+  except struct.error as e:
+    raise BuildError('Wrong argument value passed: {}'.format(e))
+
+
+def get_double(dgram, start_index):
+  """Get a 64-bit big-endian IEEE 754 floating point number from the datagram.
+
+  Args:
+    dgram: A datagram packet.
+    start_index: An index where the double starts in the datagram.
+
+  Returns:
+    A tuple containing the double and the new end index.
+
+  Raises:
+    ParseError if the datagram could not be parsed.
+  """
+  try:
+    if len(dgram[start_index:]) < _DOUBLE_DGRAM_LEN:
+      raise ParseError('Datagram is too short')
+    return (
+        struct.unpack('>d',
+                      dgram[start_index:start_index + _DOUBLE_DGRAM_LEN])[0],
+        start_index + _DOUBLE_DGRAM_LEN)
+  except (struct.error, TypeError) as e:
+    raise ParseError('Could not parse datagram {}'.format(e))
+
+
 def get_blob(dgram, start_index):
   """ Get a blob from the datagram.
 


Test patch:
diff --git a/pythonosc/test/parsing/test_osc_types.py b/pythonosc/test/parsing/test_osc_types.py
index 0863fd5..8734ad1 100644
--- a/pythonosc/test/parsing/test_osc_types.py
+++ b/pythonosc/test/parsing/test_osc_types.py
@@ -232,6 +232,39 @@ class TestFloat(unittest.TestCase):
     self.assertEqual((0, 4), osc_types.get_float(dgram, 0))
 
 
+class TestDouble(unittest.TestCase):
+
+  def test_get_double(self):
+    cases = {
+        b'\x00\x00\x00\x00\x00\x00\x00\x00': (0.0, 8),
+        b'?\xf0\x00\x00\x00\x00\x00\x00': (1.0, 8),
+        b'@\x00\x00\x00\x00\x00\x00\x00': (2.0, 8),
+        b'\xbf\xf0\x00\x00\x00\x00\x00\x00': (-1.0, 8),
+        b'\xc0\x00\x00\x00\x00\x00\x00\x00': (-2.0, 8),
+
+        b"\x00\x00\x00\x00\x00\x00\x00\x00GARBAGE": (0.0, 8),
+    }
+
+    for dgram, expected in cases.items():
+      self.assertAlmostEqual(expected, osc_types.get_double(dgram, 0))
+
+  def test_get_double_raises_on_wrong_dgram(self):
+    cases = [True]
+
+    for case in cases:
+      self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)
+
+  def test_get_double_raises_on_type_error(self):
+    cases = [None]
+
+    for case in cases:
+      self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)
+
+  def test_datagram_too_short_pads(self):
+    dgram = b'\x00' * 2
+    self.assertRaises(osc_types.ParseError, osc_types.get_double, dgram, 0)
+
+
 class TestBlob(unittest.TestCase):
 
   def test_get_blob(self):
diff --git a/pythonosc/test/test_osc_message_builder.py b/pythonosc/test/test_osc_message_builder.py
index c9720b4..d5bbe25 100644
--- a/pythonosc/test/test_osc_message_builder.py
+++ b/pythonosc/test/test_osc_message_builder.py
@@ -43,14 +43,15 @@ class TestOscMessageBuilder(unittest.TestCase):
     builder.add_arg([1, ["abc"]], [builder.ARG_TYPE_INT, [builder.ARG_TYPE_STRING]])
     builder.add_arg(4278255360, builder.ARG_TYPE_RGBA)
     builder.add_arg((1, 145, 36, 125), builder.ARG_TYPE_MIDI)
-    self.assertEqual(len("fisTFb[i[s]]")*2+2, len(builder.args))
+    builder.add_arg(1e-9, builder.ARG_TYPE_DOUBLE)
+    self.assertEqual(len("fisTFb[i[s]]")*2+3, len(builder.args))
     self.assertEqual("/SYNC", builder.address)
     builder.address = '/SEEK'
     msg = builder.build()
     self.assertEqual("/SEEK", msg.address)
     self.assertSequenceEqual(
         [4.0, 2, "value", True, False, b"\x01\x02\x03", [1, ["abc"]]] * 2 +
-        [4278255360, (1, 145, 36, 125)],
+        [4278255360, (1, 145, 36, 125), 1e-9],
         msg.params)
 
   def test_long_list(self):