Repository: NCAS-CMS/cfdm

Issue:
Subspaces of constructs can currently have one or more size 0 axes
`numpy` (and similar data objects that share its API, such as `dask`, `cfdm.Data`) reasonably allow subspaces to have size 0 axes:
```python
>>> import numpy as np
>>> a = np.arange(9)
>>> a[[False] * 9]
array([], dtype=int64)
```
At cfdm `v1.10.0.0`, cfdm _constructs_ also allow this:

```python
>>> import cfdm
>>> f = cfdm.example_field(0)
>>> f.shape
(5, 8)
>>> f[[False] * 5]
<Field: specific_humidity(latitude(0), longitude(8)) 1>
```
... but they shouldn't, because it is not allowed by the CF data model. I.e. in the above example, `f[[False] * 5]` should raise an exception.

PR to follow.



Discussion:

Patch:
diff --git a/cfdm/field.py b/cfdm/field.py
index 00ee583fd..8414a356d 100644
--- a/cfdm/field.py
+++ b/cfdm/field.py
@@ -297,6 +297,13 @@ class Field(
         # ------------------------------------------------------------
         new_data = data[tuple(indices)]
 
+        if 0 in new_data.shape:
+            raise IndexError(
+                f"Indices {indices!r} result in a subspaced shape of "
+                f"{new_data.shape}, but can't create a subspace of "
+                f"{self.__class__.__name__} that has a size 0 axis"
+            )
+
         # Replace domain axes
         domain_axes = new.domain_axes(todict=True)
         for key, size in zip(data_axes, new_data.shape):
diff --git a/cfdm/mixin/propertiesdata.py b/cfdm/mixin/propertiesdata.py
index e595265c8..a744d0f02 100644
--- a/cfdm/mixin/propertiesdata.py
+++ b/cfdm/mixin/propertiesdata.py
@@ -71,6 +71,13 @@ class PropertiesData(Properties):
         if data is not None:
             new.set_data(data[indices], copy=False)
 
+        if 0 in new.shape:
+            raise IndexError(
+                f"Indices {indices!r} result in a subspaced shape of "
+                f"{new.shape}, but can't create a subspace of "
+                f"{self.__class__.__name__} that has a size 0 axis"
+            )
+
         return new
 
     def __str__(self):


Test patch:
diff --git a/cfdm/test/test_DimensionCoordinate.py b/cfdm/test/test_DimensionCoordinate.py
index 0513f9ada..76c345140 100644
--- a/cfdm/test/test_DimensionCoordinate.py
+++ b/cfdm/test/test_DimensionCoordinate.py
@@ -125,6 +125,15 @@ class DimensionCoordinateTest(unittest.TestCase):
         )
         self.assertEqual(t.datetime_array, t.data.datetime_array)
 
+    def test_DimensiconCoordinate__getitem__(self):
+        """Test the `DimensionCoordinate.__getitem__` method."""
+        dim = self.dim
+        self.assertTrue((dim[1:3].array == dim.array[1:3]).all())
+
+        # Indices result in a subspaced shape that has a size 0 axis
+        with self.assertRaises(IndexError):
+            dim[[False] * dim.size]
+
 
 if __name__ == "__main__":
     print("Run date:", datetime.datetime.now())
diff --git a/cfdm/test/test_Field.py b/cfdm/test/test_Field.py
index 2b5ba7e81..84617e17a 100644
--- a/cfdm/test/test_Field.py
+++ b/cfdm/test/test_Field.py
@@ -152,6 +152,10 @@ class FieldTest(unittest.TestCase):
         self.assertEqual(c.data.shape, (4,))
         self.assertEqual(b.data.shape, (4, 2))
 
+        # Indices result in a subspaced shape that has a size 0 axis
+        with self.assertRaises(IndexError):
+            f[..., [False] * f.shape[-1]]
+
     #    def test_Field___setitem__(self):
     #        f = self.f.squeeze()
     #