diff --git a/src/earthkit/data/utils/metadata/dict.py b/src/earthkit/data/utils/metadata/dict.py index fa22d020c..7623750cb 100644 --- a/src/earthkit/data/utils/metadata/dict.py +++ b/src/earthkit/data/utils/metadata/dict.py @@ -7,6 +7,7 @@ # nor does it submit to any jurisdiction. # +import copy import logging from functools import cached_property from math import prod @@ -452,7 +453,26 @@ def geography(self): return make_geography(self, values_shape=self._shape) def override(self, *args, **kwargs): - raise NotImplementedError("override is not implemented for UserMetadata") + r"""Create a new metadata object by cloning the underlying metadata and setting the keys in it. + + Parameters + ---------- + *args: tuple + Positional arguments. When present must be a dict with the keys to set in + the new metadata. + **kwargs: dict, optional + Other keyword arguments specifying the metadata keys to set. + + Returns + ------- + :class:`UserMetadata` + The new metadata object. A copy of the original metadata with the keys set in it. + """ + d = dict(*args, **kwargs) + existing = copy.deepcopy(self._data) + existing.update(d) + + return UserMetadata(existing, shape=copy.deepcopy(self._shape)) def namespaces(self): return [] @@ -461,7 +481,35 @@ def as_namespace(self, namespace=None): return {} def dump(self, **kwargs): - return None + r"""Generate dump with all the metadata keys. + + In a Jupyter notebook it is represented as a tabbed interface. + + Parameters + ---------- + **kwargs: dict, optional + Other keyword arguments used for testing only + + Returns + ------- + NamespaceDump + Dict-like object with one item per namespace. In a Jupyter notebook represented + as a tabbed interface to browse the dump contents. + + Examples + -------- + :ref:`/examples/grib_metadata.ipynb` + + """ + from earthkit.data.utils.summary import format_namespace_dump + + r = [ + { + "title": "metadata", + "data": self._data, + } + ] + return format_namespace_dump(r, selected="parameter", details=self.__class__.__name__, **kwargs) def ls_keys(self): return self.LS_KEYS diff --git a/tests/array_fieldlist/test_array_field_usermetadata.py b/tests/array_fieldlist/test_array_field_usermetadata.py index 2ac6146f8..ff1774e40 100644 --- a/tests/array_fieldlist/test_array_field_usermetadata.py +++ b/tests/array_fieldlist/test_array_field_usermetadata.py @@ -9,6 +9,7 @@ # nor does it submit to any jurisdiction. # +import copy import datetime import numpy as np @@ -73,3 +74,59 @@ def test_array_field_usermetadata_geom(_kwargs): assert np.allclose(lat, meta["latitudes"]) assert np.allclose(lon, meta["longitudes"]) assert np.allclose(f.values, vals) + + +@pytest.mark.parametrize( + "initial, update, expected", + [ + ({"shortName": "2t"}, {}, {"shortName": "2t"}), # No update + ({"shortName": "2t"}, {"shortName": "msl"}, {"shortName": "msl"}), # Update existing key + ( + {"shortName": "2t"}, + {"longName": "Temperature"}, + {"shortName": "2t", "longName": "Temperature"}, + ), # Add new key + ({}, {"shortName": "2t"}, {"shortName": "2t"}), # Add key to empty metadata + ( + {"shortName": "2t", "longName": "Temperature"}, + {"shortName": "temperature"}, + {"shortName": "temperature", "longName": "Temperature"}, + ), # Update one key, keep others + ], +) +def test_array_field_usermetadata_override(initial, update, expected): + + initial_copied = copy.deepcopy(initial) + update_copied = copy.deepcopy(update) + + meta = UserMetadata(initial) + new_meta = meta.override(update) + _ = meta.override(**update) # Check that the override method works with keyword arguments + + # Check that the updated metadata matches the expected result + for k, v in expected.items(): + assert new_meta[k] == v + + # Ensure the original metadata remains unchanged + for k, v in initial_copied.items(): + assert meta[k] == v + + # Check that the updated metadata contains all expected keys + for k in update_copied: + if k in initial: + assert meta[k] == initial[k] + else: + assert k not in meta + + +def test_array_field_usermetadata_override_shape(): + meta = UserMetadata({}, shape=(10, 1)) + new_meta = meta.override( + { + "shortName": "2t", + "longName": "Temperature", + } + ) + new_meta._shape = None + assert new_meta._shape is None + assert meta._shape is not None