From 9dcb65e8a74197ab77a505112586850e1d52fe6a Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 14 Jan 2025 11:30:50 +0000 Subject: [PATCH 1/4] Support == for dims and attrs; copy arrays in attribute values. --- lib/ncdata/_core.py | 26 ++++++- lib/ncdata/utils/_copy.py | 2 +- tests/unit/core/test_AttributeAccessMixin.py | 8 +- tests/unit/core/test_NcAttribute.py | 77 +++++++++++++++----- tests/unit/core/test_NcDimension.py | 34 +++++++++ tests/unit/utils/test_ncdata_copy.py | 32 ++++++-- 6 files changed, 147 insertions(+), 32 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index 1b687e4..b0d7cb4 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -374,6 +374,14 @@ def copy(self): """Copy self.""" return NcDimension(self.name, size=self.size, unlimited=self.unlimited) + def __eq__(self, other): + """Support simply equality testing.""" + return ( + self.name == other.name + and self.size == other.size + and self.unlimited == other.unlimited + ) + class NcVariable(_AttributeAccessMixin): """ @@ -581,4 +589,20 @@ def copy(self): Does not duplicate array content. See :func:`ncdata.utils.ncdata_copy`. """ - return NcAttribute(self.name, self.value) + return NcAttribute(self.name, self.value.copy()) + + def __eq__(self, other): + """Support simple equality testing.""" + if not isinstance(other, NcAttribute): + result = NotImplemented + else: + result = self.name == other.name + if result: + v1 = self.value + v2 = other.value + result = ( + v1.shape == v2.shape + and v1.dtype == v2.dtype + and np.all(v1 == v2) + ) + return result diff --git a/lib/ncdata/utils/_copy.py b/lib/ncdata/utils/_copy.py index efd80a3..30e876b 100644 --- a/lib/ncdata/utils/_copy.py +++ b/lib/ncdata/utils/_copy.py @@ -15,7 +15,7 @@ def ncdata_copy(ncdata: NcData) -> NcData: Return a copy of the data. The operation makes fresh copies of all ncdata objects, but does not copy arrays in - either variable data or attribute values. + variable data. Parameters ---------- diff --git a/tests/unit/core/test_AttributeAccessMixin.py b/tests/unit/core/test_AttributeAccessMixin.py index 32386c2..b122cc5 100644 --- a/tests/unit/core/test_AttributeAccessMixin.py +++ b/tests/unit/core/test_AttributeAccessMixin.py @@ -21,7 +21,7 @@ class Test_AttributeAccesses: def test_gettattr(self, sample_object): content = np.array([1, 2]) sample_object.attributes.add(NcAttribute("x", content)) - assert sample_object.get_attrval("x") is content + assert np.all(sample_object.get_attrval("x") == content) def test_getattr_absent(self, sample_object): # Check that fetching a non-existent attribute returns None. @@ -30,15 +30,15 @@ def test_getattr_absent(self, sample_object): def test_setattr(self, sample_object): content = np.array([1, 2]) sample_object.set_attrval("x", content) - assert sample_object.attributes["x"].value is content + assert np.all(sample_object.attributes["x"].value == content) def test_setattr__overwrite(self, sample_object): content = np.array([1, 2]) sample_object.set_attrval("x", content) - assert sample_object.attributes["x"].value is content + assert np.all(sample_object.attributes["x"].value == content) sample_object.set_attrval("x", "replaced") assert list(sample_object.attributes.keys()) == ["x"] - assert sample_object.attributes["x"].value == "replaced" + assert np.all(sample_object.attributes["x"].value == "replaced") def test_setattr_getattr_none(self, sample_object): # Check behaviour when an attribute is given a Python value of 'None'. diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index b040ab0..9005c07 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -150,29 +150,70 @@ def test_repr_same(self, datatype, structuretype): class Test_NcAttribute_copy: - @staticmethod - def eq(attr1, attr2): - # Capture the expected equality of an original - # attribute and its copy. - # In the case of its value, if it is a numpy array, - # then it should be the **same identical object** - # -- i.e. not a copy (not even a view). - result = attr1 is not attr2 - if result: - result = attr1.name == attr1.name and np.all( - attr1.value == attr2.value - ) - if result and hasattr(attr1.value, "dtype"): - result = attr1.value is attr2.value - return result - def test_empty(self): attr = NcAttribute("x", None) result = attr.copy() - assert self.eq(result, attr) + assert result == attr def test_value(self, datatype, structuretype): value = attrvalue(datatype, structuretype) attr = NcAttribute("x", value=value) result = attr.copy() - assert self.eq(result, attr) + assert result == attr + assert result.name == attr.name + assert result.value is not attr.value + assert ( + result.value.dtype == attr.value.dtype + and result.value.shape == attr.value.shape + and np.all(result.value == attr.value) + ) + + +class Test_NcAttribute__eq__: + def test_eq(self, datatype, structuretype): + value = attrvalue(datatype, structuretype) + attr1 = NcAttribute("x", value=value) + attr2 = NcAttribute("x", value=value) + assert attr1 == attr2 + + def test_neq_name(self): + attr1 = NcAttribute("x", value=1) + attr2 = NcAttribute("y", value=1) + assert attr1 != attr2 + + def test_neq_dtype(self): + attr1 = NcAttribute("x", value=1) + attr2 = NcAttribute("x", value=np.array(1, dtype=np.int32)) + assert attr1 != attr2 + + def test_neq_shape(self): + attr1 = NcAttribute("x", value=1) + attr2 = NcAttribute("x", value=[1, 2]) + assert attr1 != attr2 + + def test_neq_value_numeric(self): + attr1 = NcAttribute("x", value=1.0) + attr2 = NcAttribute("x", value=1.1) + assert attr1 != attr2 + + def test_neq_value_string(self): + attr1 = NcAttribute("x", value="ping") + attr2 = NcAttribute("x", value="pong") + assert attr1 != attr2 + + def test_eq_onechar_arrayofonestring(self): + # NOTE: vector of char is really no different to vector of string, + # but we will get an 'U1' (single char length) dtype + attr1 = NcAttribute("x", value="t") + attr2 = NcAttribute("x", value=np.array("t")) + assert attr1 == attr2 + assert attr1.value.dtype == " Date: Wed, 15 Jan 2025 12:16:29 +0000 Subject: [PATCH 2/4] Test attribute value assignment. --- tests/unit/core/test_NcAttribute.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index 9005c07..283257b 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -217,3 +217,17 @@ def test_eq_onestring_arrayofonestring(self): assert attr1.value.dtype == " Date: Wed, 15 Jan 2025 12:23:18 +0000 Subject: [PATCH 3/4] Update docstrings wrt attribute copying behaviour. --- lib/ncdata/_core.py | 13 ++++--------- lib/ncdata/utils/_copy.py | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index b0d7cb4..71bc06d 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -330,8 +330,8 @@ def copy(self): """ Copy self. - This duplicates structure with new ncdata core objects, but does not duplicate - data arrays. See :func:`ncdata.utils.ncdata_copy`. + This duplicates structure with all-new ncdata core objects, but does not + duplicate variable data arrays. See :func:`ncdata.utils.ncdata_copy`. """ from ncdata.utils import ncdata_copy @@ -479,7 +479,7 @@ def copy(self): """ Copy self. - Does not duplicate arrays oin data or attribute content. + Does not duplicate arrays in data content. See :func:`ncdata.utils.ncdata_copy`. """ from ncdata.utils._copy import _attributes_copy @@ -583,12 +583,7 @@ def __str__(self): # noqa: D105 return repr(self) def copy(self): - """ - Copy self. - - Does not duplicate array content. - See :func:`ncdata.utils.ncdata_copy`. - """ + """Copy self, including any array value content.""" return NcAttribute(self.name, self.value.copy()) def __eq__(self, other): diff --git a/lib/ncdata/utils/_copy.py b/lib/ncdata/utils/_copy.py index 30e876b..f631df4 100644 --- a/lib/ncdata/utils/_copy.py +++ b/lib/ncdata/utils/_copy.py @@ -14,8 +14,8 @@ def ncdata_copy(ncdata: NcData) -> NcData: """ Return a copy of the data. - The operation makes fresh copies of all ncdata objects, but does not copy arrays in - variable data. + The operation makes fresh copies of all ncdata objects, but does not copy variable + data arrays. Parameters ---------- From ac166ec1e6723b0528e1044dbf718442396da1e7 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 15 Jan 2025 12:42:24 +0000 Subject: [PATCH 4/4] Updated RTD deps. --- requirements/readthedocs.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements/readthedocs.yml b/requirements/readthedocs.yml index 4cce034..520e71a 100644 --- a/requirements/readthedocs.yml +++ b/requirements/readthedocs.yml @@ -4,13 +4,15 @@ channels: - conda-forge dependencies: - - netCDF4>=1.4 - - numpy <2.0 - iris - - xarray - - filelock - iris-sample-data + - filelock + - netCDF4>=1.4 + - numpy + - pip + - pydata-sphinx-theme - pytest + - python<3.13 - sphinx - sphinxcontrib-napoleon - - pydata-sphinx-theme + - xarray