Repository: pydap/pydap

Issue:
das parser does not flatten container type attributes (`NC_Global` and `DODS_Extra`)
My understanding is that `NC_Global` and `DODS_extra` are global, container type attributes that opendap servers should no longer support, but many servers continue to support these. As a result, pydap is able to flatten these container-type attributes via its DMR parser (DAP4). However, the ~`dds`~ `das` parser does not flatten these attributes, resulting in a nested dictionary of attributes. This has a performance penalty upstream on the xarray end. 


## minimal example
```python
from pydap.client import open_url
from pydap.net import create_session

session =create_session() # requests session, using defaults
url = "http://test.opendap.org/opendap/data/nc/coads_climatology.nc"
```

### dap4 (dmr) parser works well
```python
pyds = open_client(url, session=session, protocol="dap4")
pyds.attributes
>>> {'history': 'FERRET V4.30 (debug/no GUI) 15-Aug-96',
 'Unlimited_Dimension': 'TIME'}
```
### dap2 ~(dds)~ (das) parser continues to return nested dictionary
```python
pyds = open_client(url, session=session, protocol="dap2")
pyds.attributes
>>> {'NC_GLOBAL': {'history': 'FERRET V4.30 (debug/no GUI) 15-Aug-96'},
 'DODS_EXTRA': {'Unlimited_Dimension': 'TIME'}}
```

Discussion:

Patch:
diff --git a/src/pydap/parsers/das.py b/src/pydap/parsers/das.py
index c2c4daf..bda6dcc 100644
--- a/src/pydap/parsers/das.py
+++ b/src/pydap/parsers/das.py
@@ -108,8 +108,20 @@ def add_attributes(dataset, attributes):
     Returns the dataset with added attributes.
 
     """
-    dataset.attributes["NC_GLOBAL"] = attributes.get("NC_GLOBAL", {})
-    dataset.attributes["DODS_EXTRA"] = attributes.get("DODS_EXTRA", {})
+    # identify all global attributes that are dicts but that
+    # do not have a variable associated with them
+    deprecated_global_attrs = ["NC_GLOBAL", "DODS_EXTRA"]
+    global_dict_attrs_keys = [
+        key
+        for key in attributes
+        if isinstance(attributes[key], dict) and key in deprecated_global_attrs
+    ]
+    global_dict_attrs = {}
+    for key in global_dict_attrs_keys:
+        dicts = attributes.pop(key, {})
+        global_dict_attrs = {**global_dict_attrs, **dicts}
+
+    dataset.attributes = global_dict_attrs
 
     for var in list(walk(dataset))[::-1]:
         # attributes can be flat, eg, "foo.bar" : {...}


Test patch:
diff --git a/src/pydap/tests/test_client.py b/src/pydap/tests/test_client.py
index fc7fbbc..69c5ef5 100644
--- a/src/pydap/tests/test_client.py
+++ b/src/pydap/tests/test_client.py
@@ -210,8 +210,8 @@ def test_open_dods_16bits(sequence_app):
 def test_open_dods_with_attributes(sequence_app):
     """Open the dods response together with the das response."""
     dataset = open_dods_url(".dods", metadata=True, application=sequence_app)
-    assert dataset.NC_GLOBAL == {}
-    assert dataset.DODS_EXTRA == {}
+    assert "NC_GLOBAL" not in dataset.attributes
+    assert "DODS_EXTRA" not in dataset.attributes
     assert dataset.description == "A simple sequence for testing."
     assert dataset.cast.lon.axis == "X"
     assert dataset.cast.lat.axis == "Y"
diff --git a/src/pydap/tests/test_parsers_das.py b/src/pydap/tests/test_parsers_das.py
index 08150a8..7fb10c8 100644
--- a/src/pydap/tests/test_parsers_das.py
+++ b/src/pydap/tests/test_parsers_das.py
@@ -83,3 +83,12 @@ class TestParseDAS(unittest.TestCase):
         self.assertEqual(self.dataset.floats["b"], float("-inf"))
         self.assertEqual(self.dataset.floats["c"], float("inf"))
         self.assertEqual(self.dataset.floats["d"], 17.0)
+
+    def test_global_attributes(self):
+        """test that there are no global nor dods_extra attributes on dataset"""
+        attrs = parse_das(DAS)
+        dataset = dds_to_dataset(DDS)
+        dataset = add_attributes(dataset, attrs)  # new dataset instance
+        self.assertNotIn("NC_GLOBAL", dataset.attributes)
+        self.assertNotIn("DODS_EXTRA", dataset.attributes)
+        # self.assertTrue(all(not isinstance(v, dict) for v in attrs.values()))