Repository: pydap/pydap
Pull request #399 (2024-10-18 03:38:01)

Allow dds and DMR parser of remote datasets with Flatten groups (slashes in name)
<!-- Thank you very much for your hard work and contribution! -->
Draft PR
The following Pull Request:

- [x] Closes issue #397 by allowing the dds and dmr parser to create dataset with '/' in name of variables.
- [x] Closes issue #398 .
- [x] Closes issue #400 .
- [x] Tests are added to demonstrate the new or fixed behavior, and all tests pass
on a local environment.


This PR allows pydap to open remote datasets with flattened group representation as follows:
### DAP2
Sample dds is `src/pydap/tests/data/flatgroup.dds`
```python
.sst.mnmean.nc
├──/A/B/sst
│  ├──/A/B/sst
│  ├──/time
│  ├──/lat
│  └──/lon
├──/A/B/lat
├──/A/B/lon
├──/A/B/time
├──/A/B/time_bnds
└──/A/B/Vertical_binsize
```

### DAP4
a) Flattened Group representation ( `src/pydap/tests/data/dmrs/SimpleGroupFlat.dmr`)
```python
dsf = dmr_to_dataset(dmrf)
dsf.tree()
.example dataset
├──SimpleGroup/Temperature
├──SimpleGroup/Salinity
├──SimpleGroup/Y
├──SimpleGroup/X
├──time
└──time_bnds
```

b) with Group representation (dmr already part of testing) 

```python
.example dataset
├──SimpleGroup
│  ├──Temperature
│  ├──Salinity
│  ├──Y
│  └──X
├──time
└──time_bnds
```

Mikejmnez: @jgallagher59701 this is ready for review. 

I may try to add an additional test

Patch:
diff --git a/src/pydap/lib.py b/src/pydap/lib.py
index 01280c0..891a587 100644
--- a/src/pydap/lib.py
+++ b/src/pydap/lib.py
@@ -136,6 +136,7 @@ DAP4_TO_NUMPY_PARSER_TYPEMAP = {
     "Byte": "B",
     "String": STRING,
     "Url": STRING,
+    "Char": ">u1",
 }
 
 
diff --git a/src/pydap/model.py b/src/pydap/model.py
index b572285..4bb868f 100644
--- a/src/pydap/model.py
+++ b/src/pydap/model.py
@@ -541,9 +541,9 @@ class StructureType(DapType, Mapping):
 
     def sequences(self):
         out = {}
-        Sqns = [key for key in self.children() if isinstance(key, SequenceType)]
-        for var in Sqns:
-            out.update({var.name: [key.name for key in var.children()]})
+        for var in walk(self, SequenceType):
+            if var.type == "Sequence":
+                out.update({var.name: [key.name for key in var.children()]})
         return out
 
     def variables(self):
@@ -565,18 +565,25 @@ class DatasetType(StructureType):
         'B'
     """
 
-    def __setitem__(self, key, item, **kwargs):
+    def __setitem__(self, key, item):
         # key a path-like only in DAP4
-        parts = re.split(r"[/.]", key)[1:]
+        split_by = " "
+        if len(self.groups()) > 0:
+            if key[0] == "/":
+                key = key[1:]
+            split_by += "/"
+        if len(self.sequences()) > 0 or len(self.structures()) > 0:
+            split_by += "."
+        parts = re.split("[" + split_by + "]", key)
         N = len(parts)
-        if key[0] == "/" and N > 1:
+        if N > 1:
             # add parent container type if not there
             if parts[0] not in self._dict:
                 self._visible_keys.append(parts[0])
             #  iterate over all groups to reach DAP object
             current = self._dict
             for j in range(N - 1):
-                if parts[j] not in current:
+                if parts[j] not in current:  # and Flat is not None
                     #     # This current approach works when parsing a DMR
                     #     # with only Groups and arrays. Need to enable
                     #     # Sequences and Structures. This works with all
@@ -585,7 +592,7 @@ class DatasetType(StructureType):
                 current = current[parts[j]]
             current[parts[-1]] = item
         else:
-            if key[0] == "/":
+            if key[0] == "/" and len(self.groups()) > 0:
                 key = key[1:]
             key = _quote(key)
             if key != item.name:
@@ -599,7 +606,6 @@ class DatasetType(StructureType):
             self._dict[key] = item
             # By default added keys are visible:
             self._visible_keys.append(key)
-
         key = key.replace("%2E", ".")
         if len(key.split(".")) == 1:
             # The parent name does not go into the children ids.
@@ -683,26 +689,16 @@ class DatasetType(StructureType):
         # since these are safe to escape. This catches all cases
         # much more cleanly. User specifies FQN but DAPtype is defined
         # within the method so OK.
-        if name[0] != "/":
-            warnings.warn(
-                """name must start with `/` to have a Fully Qualifing Name.
-                Adding a `/` to the begining of the name."""
-            )
-            # makes sure DAP4 fqn name (even when DAP2)
-            name = "/" + name
-        parts = re.split(r"[/.]", name)[1:]
+        split_by = " "
+        if len(self.groups()) > 0:  # true
+            split_by += "/"
+        else:
+            if name[0] == "/":
+                name = name[1:]
+        if len(self.sequences()) > 0 or len(self.structures()) > 0:
+            split_by += "."
+        parts = re.split("[" + split_by + "]", name)
         item = daptype(name=parts[-1], **attrs)
-        try:
-            for i in range(1, len(parts)):
-                self[("/").join(parts[:i])]
-        except KeyError:
-            warnings.warn(
-                """Failed to create `{}` because parent `{}` does not exist!
-                """.format(
-                    parts[-1], parts[-2]
-                )
-            )
-            return None
         DatasetType.__setitem__(self, name, item)
 
     def createGroup(self, name, **attrs):
@@ -717,7 +713,20 @@ class DatasetType(StructureType):
             name = "/" + name
         if len(name.split("/")) > 2:
             path = ("/").join(name.split("/")[:-1]) + "/"
-        return self.createDapType(GroupType, name, **attrs, path=path)
+        parts = re.split(r"[/]", name)
+        try:
+            for i in range(1, len(parts)):
+                self[("/").join(parts[:i])]
+        except KeyError:
+            warnings.warn(
+                """Failed to create `{}` because parent `{}` does not exist!
+                """.format(
+                    parts[-1], parts[-2]
+                )
+            )
+            return None
+
+        return self.createDapType(GroupType, name, path=path, **attrs)
 
     def createVariable(self, name, **attrs):
         """
@@ -997,6 +1006,10 @@ class GridType(StructureType):
         """Return the name of the axes."""
         return tuple(list(self.keys())[1:])
 
+    @property
+    def type(self):
+        return "Grid"
+
 
 class GroupType(StructureType):
     """A Group container.
diff --git a/src/pydap/parsers/dmr.py b/src/pydap/parsers/dmr.py
index e67f358..6ff63d5 100644
--- a/src/pydap/parsers/dmr.py
+++ b/src/pydap/parsers/dmr.py
@@ -84,12 +84,16 @@ def get_attributes(element, attributes={}):
     attribute_elements = element.findall("Attribute")
     Float_types = ["Float32", "float", "Float64"]
     Int_types = ["Int16", "Int32", "Int64", "int", "Int8"]
-    uInt_types = ["uInt16", "uInt32", "uInt64", "uint", "uInt8"]
+    uInt_types = ["uInt16", "uInt32", "uInt64", "uint", "uInt8", "Char"]
     for attribute_element in attribute_elements:
         name = attribute_element.get("name")
         value = attribute_element.find("Value").text
+        if value is None:
+            # This could be because server is TDS.
+            # If value is None still, then data is missing
+            value = attribute_element.find("Value").get("value")
         _type = attribute_element.get("type")
-        if _type in dmr_atomic_types:
+        if _type in dmr_atomic_types and value is not None:
             if _type in Float_types:
                 # keep float-type of value
                 value = float(value)
@@ -182,6 +186,11 @@ def dmr_to_dataset(dmr):
     # Parse the DMR. First dropping the namespace
     dom_et = DMRParser(dmr).node
     # emtpy dataset
+    if DMRParser(dmr).Groups:
+        split_by = "/"
+    else:
+        split_by = None
+
     dataset = DMRParser(dmr).init_dataset()
 
     variables = get_variables(dom_et)
@@ -190,7 +199,7 @@ def dmr_to_dataset(dmr):
     # get Global dimensions at root level
     global_dimensions = []
     for name, size in named_dimensions.items():
-        if len(name.split("/")) == 1:
+        if len(name.split(split_by)) == 1:
             global_dimensions.append([name, size])
 
     dataset.dimensions = {k: v for k, v in global_dimensions}
@@ -220,9 +229,9 @@ def dmr_to_dataset(dmr):
     for name, variable in variables.items():
         var_name = variable["name"]
         path = None
-        if len(var_name.split("/")) > 1:
+        if len(var_name.split(split_by)) > 1:
             # path-like name - Groups!
-            parts = var_name.split("/")
+            parts = var_name.split(split_by)
             var_name = parts[-1]
             path = ("/").join(parts[:-1])
             variable["attributes"]["path"] = path
@@ -231,33 +240,39 @@ def dmr_to_dataset(dmr):
         # make sure all dimensions have qualifying name
         Dims = []
         for dim in variable["dims"]:
-            if len(dim.split("/")) == 1:
+            if len(dim.split(split_by)) == 1:
                 Dims.append("/" + dim)
             else:
                 Dims.append(dim)
-
-        array = pydap.model.BaseType(name=var_name, data=data, dimensions=Dims)
         # pass along maps
+        var_kwargs = {
+            "name": name,
+            "data": data,
+            "dimensions": Dims,
+            "attributes": variable["attributes"],
+        }
         if "maps" in variable.keys():
-            array.Maps = variable["maps"]
-        var = array
-        var.attributes = variable["attributes"]
+            var_kwargs.update({"Maps": variable["maps"]})
         if "parent" in variable.keys() and variable["parent"] in [
             "Sequence",
             "Structure",
         ]:
-            parts = name.split("/")
+            parts = name.split(split_by)
             parent_name = parts[-2]
             path = ("/").join(parts[:-2])
-            if variable["parent"] == "Sequence":
-                dapType = pydap.model.SequenceType
-            else:
-                dapType = pydap.model.StructureType
-            if parent_name not in dataset[path].keys():
-                dataset[path + parent_name] = dapType(parent_name, path=path)
-            dataset[("/").join(parts)] = var
+            if (
+                variable["parent"] == "Sequence"
+                and parent_name not in dataset[path].keys()
+            ):
+                dataset.createSequence(("/").join(parts), path=path)
+            elif (
+                variable["parent"] == "Structure"
+                and parent_name not in dataset[path].keys()
+            ):
+                dataset.createStructure(("/").join(parts), path=path)
         else:
-            dataset[name] = var
+            dataset.createVariable(**var_kwargs)
+
     return dataset
 
 
@@ -265,11 +280,14 @@ class DMRParser(object):
     """A parser for the DMR."""
 
     def __init__(self, dmr):
-        # super(DMRParser, self).__init__(dmr, re.IGNORECASE)
         self.dmr = dmr
+        self.Groups = None
 
         _dmr = re.sub(' xmlns="[^"]+"', "", self.dmr, count=1)
         self.node = ET.fromstring(_dmr)
+        if len(get_groups(self.node)) > 0:
+            # no groups here
+            self.Groups = True
 
     def init_dataset(self):
         """creates an empty dataset with a name and attributes"""


Test patch:
diff --git a/src/pydap/tests/data/dmrs/SimpleGroupFlat.dmr b/src/pydap/tests/data/dmrs/SimpleGroupFlat.dmr
new file mode 100644
index 0000000..a177195
--- /dev/null
+++ b/src/pydap/tests/data/dmrs/SimpleGroupFlat.dmr
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<Dataset xmlns="http://xml.opendap.org/ns/DAP/4.0#" xml:base="http://localhost:8001" dapVersion="4.0" dmrVersion="1.0" name="example%20dataset">
+    <Dimension name="time" size="1"/>
+    <Dimension name="nv" size="2"/>
+    <Dimension name="/SimpleGroup/Y" size="4"/>
+    <Dimension name="/SimpleGroup/X" size="4"/>
+    <Float32 name="/SimpleGroup/Temperature">
+        <Dim name="/time"/>
+        <Dim name="/SimpleGroup/Y"/>
+        <Dim name="/SimpleGroup/X"/>
+        <Attribute name="units" type="str">
+            <Value>degrees_celsius</Value>
+        </Attribute>
+        <Attribute name="_FillValue" type="float">
+            <Value>inf</Value>
+        </Attribute>
+    </Float32>
+    <Float32 name="/SimpleGroup/Salinity">
+        <Dim name="/time"/>
+        <Dim name="/SimpleGroup/Y"/>
+        <Dim name="/SimpleGroup/X"/>
+        <Attribute name="units" type="str">
+            <Value>psu</Value>
+        </Attribute>
+        <Attribute name="_FillValue" type="float">
+            <Value>nan</Value>
+        </Attribute>
+    </Float32>
+    <Int16 name="/SimpleGroup/Y">
+        <Dim name="/SimpleGroup/Y"/>
+    </Int16>
+    <Int16 name="/SimpleGroup/X">
+        <Dim name="/SimpleGroup/X"/>
+    </Int16>
+    <Float32 name="time">
+        <Dim name="/time"/>
+        <Attribute name="standard_name" type="str">
+            <Value>time</Value>
+        </Attribute>
+        <Attribute name="bounds" type="str">
+            <Value>time_bnds</Value>
+        </Attribute>
+    </Float32>
+    <Float32 name="time_bnds">
+        <Dim name="/time"/>
+        <Dim name="/nv"/>
+    </Float32>
+    <Attribute name="Description" type="str">
+        <Value>A simple flattened group for testing.</Value>
+    </Attribute>
+</Dataset>
diff --git a/src/pydap/tests/data/flatgroup.dds b/src/pydap/tests/data/flatgroup.dds
new file mode 100644
index 0000000..ff714c7
--- /dev/null
+++ b/src/pydap/tests/data/flatgroup.dds
@@ -0,0 +1,15 @@
+Dataset {
+    Grid {
+      Array:
+        Int16 /A/B/sst[ntime = 1857][nlat = 89][nlon = 180];
+      Maps:
+        Float64 /time[ntime = 1857];
+        Float32 /lat[nlat = 89];
+        Float32 /lon[nlon = 180];
+    } /A/B/sst;
+    Float32 /A/B/lat[nlat = 89];
+    Float32 /A/B/lon[nlon = 180];
+    Float64 /A/B/time[ntime = 1857];
+    Float64 /A/B/time_bnds[ntime = 1857][nbnds = 2];
+    Int16 /A/B/Vertical_binsize;
+} sst.mnmean.nc;
diff --git a/src/pydap/tests/test_lib.py b/src/pydap/tests/test_lib.py
index f8ea9cb..bb3aea5 100644
--- a/src/pydap/tests/test_lib.py
+++ b/src/pydap/tests/test_lib.py
@@ -17,7 +17,7 @@ from pydap.lib import (
     tree,
     walk,
 )
-from pydap.model import BaseType, DatasetType, GroupType, SequenceType, StructureType
+from pydap.model import BaseType, DatasetType, SequenceType, StructureType
 
 
 class TestQuote(unittest.TestCase):
@@ -254,10 +254,10 @@ class TestTree(unittest.TestCase):
     def setUp(self):
         self.dataset = DatasetType("name")
         self.dataset["a"] = BaseType("a")
-        self.dataset["Group1"] = GroupType("Group1")
-        self.dataset["/Group1/Seq1"] = SequenceType("Seq1")
-        self.dataset["/Group1/Seq1/b"] = BaseType("b")
-        self.dataset["/Group1/Seq1/c"] = BaseType("c")
+        self.dataset.createGroup("Group1")
+        self.dataset.createSequence("/Group1/Seq1")
+        self.dataset.createVariable("/Group1/Seq1/b")
+        self.dataset.createVariable("/Group1/Seq1/c")
         self.dataset["d"] = StructureType("d")
         self.dataset["e"] = SequenceType("e")
 
diff --git a/src/pydap/tests/test_model.py b/src/pydap/tests/test_model.py
index 28b96a1..c7ede9f 100644
--- a/src/pydap/tests/test_model.py
+++ b/src/pydap/tests/test_model.py
@@ -484,13 +484,17 @@ def test_DatasetType_get_item_directory_path():
 
 def test_DatasetType_set_item_directory_path(sequence_example):
     dataset = DatasetType("dataset")
+    # first create Groups, and then Sequence
+    dataset.createGroup("/Group1")
+    dataset.createGroup("/Group1/Group2")
+    dataset.createSequence("/Group1/Group2/example")
     path = "/Group1/Group2/example"
     dataset[path] = sequence_example
     assert "Group1" in list(dataset.keys())
     assert "Group2" in list(dataset["/Group1"].keys())
-    assert isinstance(dataset["Group1"], GroupType)
-    assert isinstance(dataset["Group1/Group2"], GroupType)
-    assert isinstance(dataset["Group1/Group2/example"], SequenceType)
+    assert isinstance(dataset["/Group1"], GroupType)
+    assert isinstance(dataset["/Group1/Group2"], GroupType)
+    assert isinstance(dataset["/Group1/Group2/example"], SequenceType)
 
 
 def test_DatasetType_fails_createDAPType():
@@ -522,10 +526,11 @@ def test_DatasetType_groups(sequence_example):
 def test_DatasetType_sequences(sequence_example):
     dataset = DatasetType("dataset")
     dataset.createGroup("/Group1")
+    dataset.createSequence("/Group1/example")
     dataset["/Group1/example"] = sequence_example
     seq_root_level = dataset.sequences()
     sqns = dataset["/Group1"].sequences()
-    assert seq_root_level == {}
+    assert seq_root_level == sqns
     assert sqns == {"example": ["index", "temperature", "site"]}
 
 
diff --git a/src/pydap/tests/test_parsers_dds.py b/src/pydap/tests/test_parsers_dds.py
index 27cc5f9..c6c1c63 100644
--- a/src/pydap/tests/test_parsers_dds.py
+++ b/src/pydap/tests/test_parsers_dds.py
@@ -1,5 +1,6 @@
 """Test DDS parsing functions."""
 
+import os
 import unittest
 
 import numpy as np
@@ -7,6 +8,18 @@ import numpy as np
 from pydap.model import BaseType, StructureType
 from pydap.parsers.dds import dds_to_dataset
 
+
+def load_dds_file(file_path):
+    abs_path = os.path.join(os.path.dirname(__file__), file_path)
+    with open(abs_path, "r") as dds_file:
+        dds = dds_file.read()
+    dataset = dds_to_dataset(dds)
+    return dataset
+
+
+# DDS_FLAT = os.path.join(os.path.dirname(__file__), "data/flatgroup.dds")
+
+
 DDS = """Dataset {
     Structure {
         Byte b;
@@ -43,6 +56,7 @@ class TestBuildDataset(unittest.TestCase):
     def setUp(self):
         """Parse the whole dataset."""
         self.dataset = dds_to_dataset(DDS)
+        self.dataset2 = load_dds_file("data/flatgroup.dds")
 
     def test_structure(self):
         """Test the structure."""
@@ -108,6 +122,20 @@ class TestBuildDataset(unittest.TestCase):
         self.assertEqual(self.dataset.structure.s.dtype, np.dtype("|S128"))
         self.assertEqual(self.dataset.structure.s.shape, ())
 
+    def test_flatgroup(self):
+        self.assertEqual(self.dataset2.groups(), {})
+        self.assertEqual(
+            list(self.dataset2.keys()),
+            [
+                "/A/B/sst",
+                "/A/B/lat",
+                "/A/B/lon",
+                "/A/B/time",
+                "/A/B/time_bnds",
+                "/A/B/Vertical_binsize",
+            ],
+        )
+
     def test_url(self):
         """Test url parsing."""
         self.assertIsInstance(self.dataset.structure.u, BaseType)
diff --git a/src/pydap/tests/test_parsers_dmr.py b/src/pydap/tests/test_parsers_dmr.py
index 7bded39..e7004e4 100644
--- a/src/pydap/tests/test_parsers_dmr.py
+++ b/src/pydap/tests/test_parsers_dmr.py
@@ -123,6 +123,24 @@ class TestDMRParser(unittest.TestCase):
         # assert nv is NOT a variable/array
         self.assertNotIn("nv", variables)
 
+    def tests_FlatGroups(self):
+        dataset = load_dmr_file("data/dmrs/SimpleGroupFlat.dmr")
+        # pick a single variable Maps
+        Groups = dataset.groups()
+        Variables = [item for (item, _) in dataset.variables().items()]
+        self.assertEqual(Groups, {})
+        self.assertEqual(
+            Variables,
+            [
+                "SimpleGroup/Temperature",
+                "SimpleGroup/Salinity",
+                "SimpleGroup/Y",
+                "SimpleGroup/X",
+                "time",
+                "time_bnds",
+            ],
+        )
+
     def test_get_groups(self):
         dmr_file = "data/dmrs/SimpleGroup.dmr"
         abs_path = os.path.join(os.path.dirname(__file__), dmr_file)
@@ -138,3 +156,111 @@ class TestDMRParser(unittest.TestCase):
         assert entry["dimensions"] == {"Y": 4, "X": 4}
         assert entry["path"] == "/"
         assert entry["Maps"] == ()
+
+
+class TestAttrsTypesDMRParser(unittest.TestCase):
+    """Test parsing a DMR with all types"""
+
+    def test_int8(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Int8">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_uint8(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="UInt8">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_Char(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Char">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_int16(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Int16">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_uint16(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="UInt16">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_int32(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Int32">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_uint32(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="UInt32">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_int64(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Int64">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_uint64(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="UInt64">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], int)
+
+    def test_float32(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Float32">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], float)
+
+    def test_float64(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Float64">\n
+                            <Value>1</Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], float)
+
+    def test_floatNone(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Float32">\n
+                            <Value></Value>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert ds["x"].attributes["attr"] is None
+
+    def test_TDSfloat64(self):
+        dmr = """<Dataset name="foo">\n    <Int32 name="x">\n
+                <Attribute name="attr" type="Float64">\n
+                            <Value value="0.0"/>\n        </Attribute>\n
+                                </Int32>\n</Dataset>"""
+        ds = dmr_to_dataset(dmr)
+        assert isinstance(ds["x"].attributes["attr"], float)