Repository: ecmwf/earthkit-data (python)

Issue:
Some methods are not available on a NetCDF fieldlist
### What happened?

When we read a NetCDF file the resulting object does not behave like a proper fieldlist because some methods like `to_latlon()` and `to_points()` are not available.

### What are the steps to reproduce the bug?

```python 
import earthkit.data as ekd
ds = ekd.from_source("file", "R06a.nc")
ds.to_latlon()
```
AttributeError: 'File' object has no attribute 'to_latlon'


### Version

any

### Platform (OS and architecture)

all

### Relevant log output

_No response_

### Accompanying data

_No response_

### Organisation

ECMWF

Pull request:
Fix netcdf fieldlist implementation
Fixes #521 

Patch:
diff --git a/src/earthkit/data/mergers/__init__.py b/src/earthkit/data/mergers/__init__.py
index 0e5b02a..8dafa05 100644
--- a/src/earthkit/data/mergers/__init__.py
+++ b/src/earthkit/data/mergers/__init__.py
@@ -10,6 +10,7 @@
 import logging
 
 from earthkit.data.readers import Reader
+from earthkit.data.sources import Source
 from earthkit.data.sources.file import FileSource
 from earthkit.data.utils import string_to_args
 
@@ -67,6 +68,22 @@ class Merger:
         elif issubclass(self.common, Reader):
             self.reader_class = self.common
             self.paths = [s.path for s in self.sources]
+        elif issubclass(self.common, Source):
+            # to enable the merging of a FieldList and a FileSource
+            # needed for test_netcdf_wrong_concat_var
+            readers = []
+            paths = []
+            for s in self.sources:
+                if isinstance(s, FileSource):
+                    readers.append(s._reader)
+                    paths.append(s.path)
+                elif isinstance(s, Reader):
+                    readers.append(s)
+                    paths.append(s.path)
+
+            if len(readers) == len(self.sources):
+                self.reader_class = _nearest_common_class(readers)
+                self.paths = paths
 
     @property
     def paths_or_sources(self):
diff --git a/src/earthkit/data/readers/netcdf/__init__.py b/src/earthkit/data/readers/netcdf/__init__.py
index d7becb6..da4e668 100644
--- a/src/earthkit/data/readers/netcdf/__init__.py
+++ b/src/earthkit/data/readers/netcdf/__init__.py
@@ -21,9 +21,9 @@ class NetCDFFieldListReader(NetCDFFieldListFromFile, Reader):
     def __repr__(self):
         return "NetCDFFieldListReader(%s)" % (self.path,)
 
-    # def mutate_source(self):
-    #     # A NetCDFReader is a source itself
-    #     return self
+    def mutate_source(self):
+        # A NetCDFReader is a source itself
+        return self
 
 
 class NetCDFFieldListUrlReader(NetCDFFieldListFromURL, Reader):
diff --git a/src/earthkit/data/readers/netcdf/field.py b/src/earthkit/data/readers/netcdf/field.py
index 3fc75d7..c8a55f1 100644
--- a/src/earthkit/data/readers/netcdf/field.py
+++ b/src/earthkit/data/readers/netcdf/field.py
@@ -51,7 +51,7 @@ class XArrayFieldGeography(Geography):
         return tuple([self.data_array.coords[v].size for v in coords])
 
     def _unique_grid_id(self):
-        return self.shape
+        return self.shape()
 
     def projection(self):
         from earthkit.data.utils.projections import Projection
@@ -80,12 +80,10 @@ class XArrayFieldGeography(Geography):
         # TODO: implement resolution
         return None
 
-    @property
     def mars_grid(self):
         # TODO: implement mars_grid
         return None
 
-    @property
     def mars_area(self):
         # TODO: code me
         return [self.north, self.west, self.south, self.east]


Test patch:
diff --git a/tests/netcdf/test_netcdf_geography.py b/tests/netcdf/test_netcdf_geography.py
index 2e20d2f..fa9aae4 100644
--- a/tests/netcdf/test_netcdf_geography.py
+++ b/tests/netcdf/test_netcdf_geography.py
@@ -58,6 +58,14 @@ def test_netcdf_to_points_1(dtype, expected_dtype):
         eps=eps,
     )
 
+    # fieldlist
+    v = ds.to_points(flatten=True, dtype=dtype)
+    assert isinstance(v, dict)
+    assert isinstance(v["x"], np.ndarray)
+    assert isinstance(v["y"], np.ndarray)
+    assert v["x"].dtype == expected_dtype
+    assert v["y"].dtype == expected_dtype
+
 
 def test_netcdf_to_points_2():
     ds = from_source("file", earthkit_examples_file("test.nc"))
@@ -127,7 +135,7 @@ def test_netcdf_to_latlon():
         assert np.isclose(v["lat"][y, x], 57)
 
 
-def test_bbox():
+def test_netcdf_bbox():
     ds = from_source("file", earthkit_examples_file("test.nc"))
     bb = ds.bounding_box()
     assert len(bb) == 2
@@ -135,6 +143,12 @@ def test_bbox():
         assert b.as_tuple() == (73, -27, 33, 45)
 
 
+def test_netcdf_mars_area():
+    ds = from_source("file", earthkit_examples_file("test.nc"))
+    ref = [73, -27, 33, 45]
+    assert np.allclose(np.asarray(ds[0].mars_area), np.asarray(ref))
+
+
 def test_netcdf_proj_string_non_cf():
     f = from_source("file", earthkit_examples_file("test.nc"))
     with pytest.raises(AttributeError):
diff --git a/tests/netcdf/test_netcdf_output.py b/tests/netcdf/test_netcdf_output.py
index 8f39e22..46f5794 100644
--- a/tests/netcdf/test_netcdf_output.py
+++ b/tests/netcdf/test_netcdf_output.py
@@ -22,11 +22,13 @@ def test_netcdf_fieldlist_save():
     ds = from_source("file", earthkit_examples_file("test.nc"))
 
     # the file must be saved without loading the fields
-    assert ds._reader._fields is None
+    # assert ds._reader._fields is None
+    assert ds._fields is None
 
     with temp_file() as tmp:
         ds.save(tmp)
-        assert ds._reader._fields is None
+        # assert ds._reader._fields is None
+        assert ds._fields is None
         assert os.path.exists(tmp)
         r_tmp = from_source("file", tmp)
         assert len(r_tmp) == 2
diff --git a/tests/readers/test_netcdf_reader.py b/tests/readers/test_netcdf_reader.py
index 730d365..da3e854 100644
--- a/tests/readers/test_netcdf_reader.py
+++ b/tests/readers/test_netcdf_reader.py
@@ -269,9 +269,11 @@ def test_netcdf_non_fieldlist():
 @pytest.mark.no_eccodes
 def test_netcdf_lazy_fieldlist_scan():
     ds = from_source("file", earthkit_examples_file("test.nc"))
-    assert ds._reader._fields is None
+    # assert ds._reader._fields is None
+    assert ds._fields is None
     assert len(ds) == 2
-    assert len(ds._reader._fields) == 2
+    # assert len(ds._reader._fields) == 2
+    assert len(ds._fields) == 2
 
 
 if __name__ == "__main__":