Repository: NCAS-CMS/cf-python

Issue:
LAMA to Dask: managing default(-argument) `Data` instance
(Opening as a general issue since `__init__` isn't listed under our table of methods in #295, so I am not sure of the migration status of `Data` initialisation.) Whilst I was trying to create diverse cases of instances of `Data` (in the context of https://github.com/NCAS-CMS/cf-python/pull/376#discussion_r847319179), I noticed `cf.Data()` initialised as-is, i.e. with no arguments, which is seemingly documented as being valid (initial argument `array=None` with `array: optional` stated), runs into an error:

```pycon
>>> cf.Data()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/sadie/cf-python/cf/data/data.py", line 964, in __repr__
    return super().__repr__().replace("<", "<CF ", 1)
  File "/home/sadie/anaconda3/envs/cf-env/lib/python3.8/site-packages/cfdm/data/data.py", line 214, in __repr__
    shape = self.shape
  File "/home/sadie/cf-python/cf/data/data.py", line 158, in wrapper
    return method(*args, **kwargs)
  File "/home/sadie/cf-python/cf/data/data.py", line 6288, in shape
    dx = self._get_dask()
  File "/home/sadie/cf-python/cf/data/data.py", line 1387, in _get_dask
    return self._custom["dask"]
KeyError: 'dask'
```

The fix is probably a quite simple adaptation, though I don't have time to look into it now; once I have finished reviewing my backlog of Dask migration PRs I will sort this, if you haven't already tackled it @davidhassell.

Discussion:

Patch:
diff --git a/cf/data/data.py b/cf/data/data.py
index 97baabafd..402fc7e78 100644
--- a/cf/data/data.py
+++ b/cf/data/data.py
@@ -336,12 +336,6 @@ class Data(DataClassDeprecationsMixin, Container, cfdm.Data):
         >>> d = cf.Data(tuple('fly'))
 
         """
-        if array is None and source is None:  # don't create no/empty Data
-            raise ValueError(
-                "Can't create empty data: some input data or datum must be "
-                "provided via the 'source' or 'array' parameters."
-            )
-
         if source is None and isinstance(array, self.__class__):
             source = array
 
@@ -387,6 +381,7 @@ class Data(DataClassDeprecationsMixin, Container, cfdm.Data):
         self.hardmask = hardmask
 
         if array is None:
+            # No data has been set
             return
 
         try:
@@ -8318,13 +8313,16 @@ class Data(DataClassDeprecationsMixin, Container, cfdm.Data):
         dask.array<cf_soften_mask, shape=(4,), dtype=int64, chunksize=(4,), chunktype=numpy.ndarray>
 
         """
-        if apply_mask_hardness:
+        if apply_mask_hardness and "dask" in self._custom:
             if self.hardmask:
                 self.harden_mask()
             else:
                 self.soften_mask()
 
-        return self._custom["dask"]
+        try:
+            return self._custom["dask"]
+        except KeyError:
+            raise ValueError(f"{self.__class__.__name__} object has no data")
 
     def datum(self, *index):
         """Return an element of the data array as a standard Python


Test patch:
diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py
index 48ade5f9e..219f5c30d 100644
--- a/cf/test/test_Data.py
+++ b/cf/test/test_Data.py
@@ -151,15 +151,27 @@ class DataTest(unittest.TestCase):
 
     def test_Data__init__basic(self):
         """Test basic `__init__` cases for Data."""
-        # Most __init__ parameters are covered by the various other tests,
-        # so this is mainly to check trivial cases and especially the edge
-        # case of 'default' Data i.e. if no parameters are specified.
+        # Most __init__ parameters are covered by the various other
+        # tests, so this is mainly to check trivial cases.
         cf.Data(0, "s")
         cf.Data(array=np.arange(5))
         cf.Data(source=self.filename)
 
+        d = cf.Data()
         with self.assertRaises(ValueError):
-            cf.Data()
+            d.ndim
+
+        with self.assertRaises(ValueError):
+            d.get_filenames()
+
+    def test_Data__init__no_args(self):
+        """Test `__init__` with no arg."""
+        # Most __init__ parameters are covered by the various other
+        # tests, so this is mainly to check trivial cases.
+        cf.Data()
+        cf.Data(0, "s")
+        cf.Data(array=np.arange(5))
+        cf.Data(source=self.filename)
 
     def test_Data_equals(self):
         """Test the equality-testing Data method."""