From 96a3f2db0147c9596c37825924fa3fd22fc59377 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 11:34:54 +0000 Subject: [PATCH 01/48] Init commit, part way through ensuring Nones aren't wrapped in np.array --- lib/iris/_data_manager.py | 44 +++++++++++++++++++++++++++++++++------ lib/iris/cube.py | 5 +++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index dbd122ba04..52809f2ace 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -6,6 +6,9 @@ import copy +import iris.exceptions +import iris.warnings +from warnings import warn import numpy as np import numpy.ma as ma @@ -15,7 +18,7 @@ class DataManager: """Provides a well defined API for management of real or lazy data.""" - def __init__(self, data): + def __init__(self, data, shape=None): """Create a data manager for the specified data. Parameters @@ -35,6 +38,20 @@ def __init__(self, data): # Enforce the manager contract. self._assert_axioms() + # if cube is empty + if (shape is None) and (data is not None): + self.data = data + self._shape = None + # if cube is dataless + elif (shape is not None) and (data is None): + self._shape = shape + elif (shape is not None) and (data is not None): + msg = f"A cube may not be created with both data and a custom shape." + raise iris.exceptions.InvalidCubeError(msg) + else: + msg = f"A cube may not be created without both data and a custom shape." + warn(msg, iris.warnings.IrisUserWarning) + def __copy__(self): """Forbid :class:`~iris._data_manager.DataManager` instance shallow-copy support.""" @@ -128,6 +145,7 @@ def _assert_axioms(self): # Ensure there is a valid data state. is_lazy = self._lazy_array is not None is_real = self._real_array is not None + has_shape = self._shape is not None emsg = "Unexpected data state, got {}lazy and {}real data." state = is_lazy ^ is_real assert state, emsg.format("" if is_lazy else "no ", "" if is_real else "no ") @@ -148,6 +166,7 @@ def _deepcopy(self, memo, data=None): :class:`~iris._data_manager.DataManager` instance. """ + # @TODO how to ask copy to make an empty cube, special value? flag? try: if data is None: # Copy the managed data. @@ -221,17 +240,22 @@ def data(self, data): """ # Ensure we have numpy-like data. if not (hasattr(data, "shape") and hasattr(data, "dtype")): - data = np.asanyarray(data) + # data = np.asanyarray(data) + if data is not None: + data = np.asanyarray(data) # Determine whether the class instance has been created, # as this method is called from within the __init__. init_done = self._lazy_array is not None or self._real_array is not None + # @TODO set self._shape every time you change the data if init_done and self.shape != data.shape: # The _ONLY_ data reshape permitted is converting a 0-dimensional # array i.e. self.shape == () into a 1-dimensional array of length # one i.e. data.shape == (1,) - if self.shape or data.shape != (1,): + if (not is_lazy_data(data)) and data is None: + self._shape = self.shape + elif self.shape or data.shape != (1,): emsg = "Require data with shape {!r}, got {!r}." raise ValueError(emsg.format(self.shape, data.shape)) @@ -242,7 +266,8 @@ def data(self, data): else: if not ma.isMaskedArray(data): # Coerce input data to ndarray (including ndarray subclasses). - data = np.asarray(data) + if data is not None: + data = np.asarray(data) if isinstance(data, ma.core.MaskedConstant): # Promote to a masked array so that the fill-value is # writeable to the data owner. @@ -261,12 +286,19 @@ def dtype(self): @property def ndim(self): """The number of dimensions covered by the data being managed.""" - return self.core_data().ndim + return len(self.shape) @property def shape(self): """The shape of the data being managed.""" - return self.core_data().shape + print("1", self.data) + if self.data is None: + print("2", self.data) + # if self._lazy_array is None and np.all(self.data == None): + result = self._shape + else: + result = self.core_data().shape + return result def copy(self, data=None): """Return a deep copy of this :class:`~iris._data_manager.DataManager` instance. diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 30ac3432b7..abeceff819 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1190,7 +1190,8 @@ def _walk_nodes(node): def __init__( self, - data: np.typing.ArrayLike, + data: np.typing.ArrayLike, # should this now be none? test this + shape: tuple | None = None, standard_name: str | None = None, long_name: str | None = None, var_name: str | None = None, @@ -1276,7 +1277,7 @@ def __init__( self._metadata_manager = metadata_manager_factory(CubeMetadata) # Initialise the cube data manager. - self._data_manager = DataManager(data) + self._data_manager = DataManager(data, shape) #: The "standard name" for the Cube's phenomenon. self.standard_name = standard_name From 7114f0f8c58c88e56de877db081f57a9804b43eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:38:12 +0000 Subject: [PATCH 02/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/_data_manager.py | 9 ++++----- lib/iris/cube.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 52809f2ace..12dff15512 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -5,14 +5,14 @@ """Management of common state and behaviour for cube and coordinate data.""" import copy - -import iris.exceptions -import iris.warnings from warnings import warn + import numpy as np import numpy.ma as ma from iris._lazy_data import as_concrete_data, as_lazy_data, is_lazy_data +import iris.exceptions +import iris.warnings class DataManager: @@ -52,7 +52,6 @@ def __init__(self, data, shape=None): msg = f"A cube may not be created without both data and a custom shape." warn(msg, iris.warnings.IrisUserWarning) - def __copy__(self): """Forbid :class:`~iris._data_manager.DataManager` instance shallow-copy support.""" name = type(self).__name__ @@ -294,7 +293,7 @@ def shape(self): print("1", self.data) if self.data is None: print("2", self.data) - # if self._lazy_array is None and np.all(self.data == None): + # if self._lazy_array is None and np.all(self.data == None): result = self._shape else: result = self.core_data().shape diff --git a/lib/iris/cube.py b/lib/iris/cube.py index abeceff819..66f694da8d 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1190,7 +1190,7 @@ def _walk_nodes(node): def __init__( self, - data: np.typing.ArrayLike, # should this now be none? test this + data: np.typing.ArrayLike, # should this now be none? test this shape: tuple | None = None, standard_name: str | None = None, long_name: str | None = None, From 84ea908e91a529e8c42e00c9b85e9c85879dcc64 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 12:17:59 +0000 Subject: [PATCH 03/48] None types are no longer wrapped --- lib/iris/_data_manager.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 52809f2ace..1e7a5171b3 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -32,6 +32,7 @@ def __init__(self, data, shape=None): # Initialise the instance. self._lazy_array = None self._real_array = None + self._shape = shape # Assign the data payload to be managed. self.data = data @@ -39,20 +40,15 @@ def __init__(self, data, shape=None): # Enforce the manager contract. self._assert_axioms() # if cube is empty - if (shape is None) and (data is not None): - self.data = data - self._shape = None - # if cube is dataless - elif (shape is not None) and (data is None): - self._shape = shape - elif (shape is not None) and (data is not None): + if (shape is not None) and (data is not None): msg = f"A cube may not be created with both data and a custom shape." raise iris.exceptions.InvalidCubeError(msg) - else: + elif (shape is None) and (data is None): msg = f"A cube may not be created without both data and a custom shape." warn(msg, iris.warnings.IrisUserWarning) + def __copy__(self): """Forbid :class:`~iris._data_manager.DataManager` instance shallow-copy support.""" name = type(self).__name__ @@ -145,10 +141,12 @@ def _assert_axioms(self): # Ensure there is a valid data state. is_lazy = self._lazy_array is not None is_real = self._real_array is not None - has_shape = self._shape is not None + is_dataless = not(is_lazy or is_real) and self._shape is not None # if I remove the second check, allows empty arrays, like old behaviour emsg = "Unexpected data state, got {}lazy and {}real data." - state = is_lazy ^ is_real - assert state, emsg.format("" if is_lazy else "no ", "" if is_real else "no ") + state = (is_lazy ^ is_real) or is_dataless + if not state: + raise iris.exceptions.InvalidCubeError(emsg.format("" if is_lazy else "no ", "" if is_real else "no ")) + def _deepcopy(self, memo, data=None): """Perform a deepcopy of the :class:`~iris._data_manager.DataManager` instance. @@ -239,21 +237,21 @@ def data(self, data): """ # Ensure we have numpy-like data. + dataless = data is None if not (hasattr(data, "shape") and hasattr(data, "dtype")): # data = np.asanyarray(data) - if data is not None: + if not dataless: data = np.asanyarray(data) # Determine whether the class instance has been created, # as this method is called from within the __init__. init_done = self._lazy_array is not None or self._real_array is not None - # @TODO set self._shape every time you change the data - if init_done and self.shape != data.shape: + if init_done and not dataless and self.shape != data.shape: # The _ONLY_ data reshape permitted is converting a 0-dimensional # array i.e. self.shape == () into a 1-dimensional array of length # one i.e. data.shape == (1,) - if (not is_lazy_data(data)) and data is None: + if (not is_lazy_data(data)) and dataless: self._shape = self.shape elif self.shape or data.shape != (1,): emsg = "Require data with shape {!r}, got {!r}." @@ -266,7 +264,7 @@ def data(self, data): else: if not ma.isMaskedArray(data): # Coerce input data to ndarray (including ndarray subclasses). - if data is not None: + if not dataless: data = np.asarray(data) if isinstance(data, ma.core.MaskedConstant): # Promote to a masked array so that the fill-value is @@ -291,9 +289,7 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - print("1", self.data) if self.data is None: - print("2", self.data) # if self._lazy_array is None and np.all(self.data == None): result = self._shape else: From cdc51d5f4216243ef7cd572ff4827edbf18d48f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:22:32 +0000 Subject: [PATCH 04/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/_data_manager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 87caf0738d..5200d57a93 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -139,12 +139,15 @@ def _assert_axioms(self): # Ensure there is a valid data state. is_lazy = self._lazy_array is not None is_real = self._real_array is not None - is_dataless = not(is_lazy or is_real) and self._shape is not None # if I remove the second check, allows empty arrays, like old behaviour + is_dataless = ( + not (is_lazy or is_real) and self._shape is not None + ) # if I remove the second check, allows empty arrays, like old behaviour emsg = "Unexpected data state, got {}lazy and {}real data." state = (is_lazy ^ is_real) or is_dataless if not state: - raise iris.exceptions.InvalidCubeError(emsg.format("" if is_lazy else "no ", "" if is_real else "no ")) - + raise iris.exceptions.InvalidCubeError( + emsg.format("" if is_lazy else "no ", "" if is_real else "no ") + ) def _deepcopy(self, memo, data=None): """Perform a deepcopy of the :class:`~iris._data_manager.DataManager` instance. From 7b402e03e3aa7496aae1293d649b9ba384f11027 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 14:00:47 +0000 Subject: [PATCH 05/48] clarified axiom check --- lib/iris/_data_manager.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 87caf0738d..2073875f30 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -41,10 +41,10 @@ def __init__(self, data, shape=None): self._assert_axioms() # if cube is empty if (shape is not None) and (data is not None): - msg = f"A cube may not be created with both data and a custom shape." + msg = "A cube may not be created with both data and a custom shape." raise iris.exceptions.InvalidCubeError(msg) elif (shape is None) and (data is None): - msg = f"A cube may not be created without both data and a custom shape." + msg = "A cube may not be created without both data and a custom shape." warn(msg, iris.warnings.IrisUserWarning) def __copy__(self): @@ -137,13 +137,14 @@ def __repr__(self): def _assert_axioms(self): """Definition of the manager state, that should never be violated.""" # Ensure there is a valid data state. - is_lazy = self._lazy_array is not None - is_real = self._real_array is not None - is_dataless = not(is_lazy or is_real) and self._shape is not None # if I remove the second check, allows empty arrays, like old behaviour - emsg = "Unexpected data state, got {}lazy and {}real data." - state = (is_lazy ^ is_real) or is_dataless - if not state: - raise iris.exceptions.InvalidCubeError(emsg.format("" if is_lazy else "no ", "" if is_real else "no ")) + empty = self._lazy_array is None and self._real_array is None + overfilled = self._lazy_array is not None and self._real_array is not None + if overfilled: + msg = "Unexpected data state, got both lazy and real data." + raise iris.exceptions.InvalidCubeError(msg) + elif empty and self._shape is None: # if I remove the second check, allows empty arrays, like old behaviour + msg = "Unexpected data state, got no lazy or real data, and no shape." + raise iris.exceptions.InvalidCubeError(msg) def _deepcopy(self, memo, data=None): From bc1ee6f8c69a700a606bfe6f43d8ffa26149f05a Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 16:07:38 +0000 Subject: [PATCH 06/48] moved shape order in Cube --- lib/iris/_data_manager.py | 7 +------ lib/iris/cube.py | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 2073875f30..e1d55c53d8 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -32,20 +32,15 @@ def __init__(self, data, shape=None): # Initialise the instance. self._lazy_array = None self._real_array = None - self._shape = shape # Assign the data payload to be managed. self.data = data + self._shape = shape - # Enforce the manager contract. - self._assert_axioms() # if cube is empty if (shape is not None) and (data is not None): msg = "A cube may not be created with both data and a custom shape." raise iris.exceptions.InvalidCubeError(msg) - elif (shape is None) and (data is None): - msg = "A cube may not be created without both data and a custom shape." - warn(msg, iris.warnings.IrisUserWarning) def __copy__(self): """Forbid :class:`~iris._data_manager.DataManager` instance shallow-copy support.""" diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 66f694da8d..074b42197b 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1190,8 +1190,7 @@ def _walk_nodes(node): def __init__( self, - data: np.typing.ArrayLike, # should this now be none? test this - shape: tuple | None = None, + data: np.typing.ArrayLike, standard_name: str | None = None, long_name: str | None = None, var_name: str | None = None, @@ -1205,6 +1204,7 @@ def __init__( cell_measures_and_dims: Iterable[tuple[CellMeasure, int]] | None = None, ancillary_variables_and_dims: Iterable[tuple[AncillaryVariable, int]] | None = None, + shape: tuple | None = None, ): """Create a cube with data and optional metadata. From 16779aa93ff89fa7a7fa5f3209c61595cd283fc2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:09:49 +0000 Subject: [PATCH 07/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/_data_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index e1d55c53d8..3e39e3f2c6 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -137,11 +137,12 @@ def _assert_axioms(self): if overfilled: msg = "Unexpected data state, got both lazy and real data." raise iris.exceptions.InvalidCubeError(msg) - elif empty and self._shape is None: # if I remove the second check, allows empty arrays, like old behaviour + elif ( + empty and self._shape is None + ): # if I remove the second check, allows empty arrays, like old behaviour msg = "Unexpected data state, got no lazy or real data, and no shape." raise iris.exceptions.InvalidCubeError(msg) - def _deepcopy(self, memo, data=None): """Perform a deepcopy of the :class:`~iris._data_manager.DataManager` instance. From f42c19d9918f27f007a568bb130277d1cad9a4b2 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 16:57:17 +0000 Subject: [PATCH 08/48] replace call to self.data with self.core_data() --- lib/iris/_data_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index e1d55c53d8..5dc516d411 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -34,10 +34,11 @@ def __init__(self, data, shape=None): self._real_array = None # Assign the data payload to be managed. - self.data = data self._shape = shape + self.data = data + - # if cube is empty + # if cube has shape and data if (shape is not None) and (data is not None): msg = "A cube may not be created with both data and a custom shape." raise iris.exceptions.InvalidCubeError(msg) @@ -233,7 +234,6 @@ def data(self, data): # Ensure we have numpy-like data. dataless = data is None if not (hasattr(data, "shape") and hasattr(data, "dtype")): - # data = np.asanyarray(data) if not dataless: data = np.asanyarray(data) @@ -283,7 +283,7 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - if self.data is None: + if self.core_data() is None: result = self._shape else: result = self.core_data().shape From 6d62c7d6133615a402b3eb2d45d2119c3c3f1645 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:58:15 +0000 Subject: [PATCH 09/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/_data_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 2731ba2c3f..c5f62b1966 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -37,7 +37,6 @@ def __init__(self, data, shape=None): self._shape = shape self.data = data - # if cube has shape and data if (shape is not None) and (data is not None): msg = "A cube may not be created with both data and a custom shape." From cc13e6d9156790d3ab70df267c7bdb1a966dea29 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 17:14:10 +0000 Subject: [PATCH 10/48] fixed test regex --- lib/iris/_data_manager.py | 1 - lib/iris/tests/unit/data_manager/test_DataManager.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index c5f62b1966..1ae00798a3 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -5,7 +5,6 @@ """Management of common state and behaviour for cube and coordinate data.""" import copy -from warnings import warn import numpy as np import numpy.ma as ma diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index b419e556a7..431ea615c4 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -3,7 +3,7 @@ # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. """Unit tests for the :class:`iris._data_manager.DataManager`.""" - +import iris.exceptions # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests # isort:skip @@ -167,14 +167,14 @@ def setUp(self): def test_array_none(self): self.dm._real_array = None - emsg = "Unexpected data state, got no lazy and no real data" - with self.assertRaisesRegex(AssertionError, emsg): + emsg = "Unexpected data state, got no lazy or real data, and no shape." + with self.assertRaisesRegex(iris.exceptions.InvalidCubeError, emsg): self.dm._assert_axioms() def test_array_all(self): self.dm._lazy_array = self.lazy_array - emsg = "Unexpected data state, got lazy and real data" - with self.assertRaisesRegex(AssertionError, emsg): + emsg = "Unexpected data state, got both lazy and real data." + with self.assertRaisesRegex(iris.exceptions.InvalidCubeError, emsg): self.dm._assert_axioms() From 71c7ae8eb6c5af40350a673c4f00e9f7cc8960c8 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 11 Dec 2024 17:16:25 +0000 Subject: [PATCH 11/48] precommit --- lib/iris/tests/unit/data_manager/test_DataManager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 431ea615c4..c8201726ba 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -3,7 +3,9 @@ # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. """Unit tests for the :class:`iris._data_manager.DataManager`.""" + import iris.exceptions + # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests # isort:skip From e59d5c9652c0c16174dfdbbd8d3415fb2330b7ed Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 12 Dec 2024 11:16:33 +0000 Subject: [PATCH 12/48] written tests, and refactored redundant checks --- lib/iris/_data_manager.py | 32 ++++++------- .../unit/data_manager/test_DataManager.py | 47 +++++++++++++++++++ 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 1ae00798a3..35677ddac6 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -28,6 +28,10 @@ def __init__(self, data, shape=None): managed. """ + if (shape is not None) and (data is not None): + msg = "A cube may not be created with both data and a custom shape." + raise iris.exceptions.InvalidCubeError(msg) + # Initialise the instance. self._lazy_array = None self._real_array = None @@ -36,11 +40,6 @@ def __init__(self, data, shape=None): self._shape = shape self.data = data - # if cube has shape and data - if (shape is not None) and (data is not None): - msg = "A cube may not be created with both data and a custom shape." - raise iris.exceptions.InvalidCubeError(msg) - def __copy__(self): """Forbid :class:`~iris._data_manager.DataManager` instance shallow-copy support.""" name = type(self).__name__ @@ -230,23 +229,24 @@ def data(self, data): managed. """ - # Ensure we have numpy-like data. + # If data is None, ensure previous shape is maintained, and that it is + # not wrapped in an np.array dataless = data is None - if not (hasattr(data, "shape") and hasattr(data, "dtype")): - if not dataless: - data = np.asanyarray(data) + if dataless: + self._shape = self.shape - # Determine whether the class instance has been created, - # as this method is called from within the __init__. - init_done = self._lazy_array is not None or self._real_array is not None + # Ensure we have numpy-like data. + elif not (hasattr(data, "shape") and hasattr(data, "dtype")): + data = np.asanyarray(data) - if init_done and not dataless and self.shape != data.shape: + # Determine whether the class already has a defined shape, + # as this method is called from __init__. + has_shape = self.shape is not None + if has_shape and not dataless and self.shape != data.shape: # The _ONLY_ data reshape permitted is converting a 0-dimensional # array i.e. self.shape == () into a 1-dimensional array of length # one i.e. data.shape == (1,) - if (not is_lazy_data(data)) and dataless: - self._shape = self.shape - elif self.shape or data.shape != (1,): + if self.shape or data.shape != (1,): emsg = "Require data with shape {!r}, got {!r}." raise ValueError(emsg.format(self.shape, data.shape)) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index c8201726ba..9308f4fa35 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -454,6 +454,48 @@ def test_nd_lazy_to_nd_lazy(self): self.assertTrue(dm.has_lazy_data()) self.assertArrayEqual(dm.data, lazy_array.compute()) + def test_nd_lazy_to_dataless(self): + shape = (2, 3, 4) + size = np.prod(shape) + real_array = np.arange(size).reshape(shape) + lazy_array = as_lazy_data(real_array) + dm = DataManager(lazy_array * 10) + self.assertTrue(dm.has_lazy_data()) + dm.data = None + self.assertTrue(dm.core_data() is None) + self.assertTrue(dm.shape == shape) + + def test_nd_real_to_dataless(self): + shape = (2, 3, 4) + size = np.prod(shape) + real_array = np.arange(size).reshape(shape) + dm = DataManager(real_array) + self.assertFalse(dm.has_lazy_data()) + dm.data = None + self.assertTrue(dm.core_data() is None) + self.assertTrue(dm.shape == shape) + + def test_dataless_to_nd_lazy(self): + shape = (2, 3, 4) + size = np.prod(shape) + real_array = np.arange(size).reshape(shape) + lazy_array = as_lazy_data(real_array) + dm = DataManager(None, shape) + self.assertTrue(dm.shape == shape) + dm.data = lazy_array + self.assertTrue(dm.has_lazy_data()) + self.assertArrayEqual(dm.data, lazy_array.compute()) + + def test_dataless_to_nd_real(self): + shape = (2, 3, 4) + size = np.prod(shape) + real_array = np.arange(size).reshape(shape) + dm = DataManager(None, shape) + self.assertTrue(dm.data is None) + dm.data = real_array + self.assertFalse(dm.has_lazy_data()) + self.assertArrayEqual(dm.data, real_array) + def test_coerce_to_ndarray(self): shape = (2, 3) size = np.prod(shape) @@ -527,6 +569,11 @@ def test_shape_nd(self): dm = DataManager(lazy_array) self.assertEqual(dm.shape, shape) + def test_shape_dataless(self): + shape = (2, 3, 4) + dm = DataManager(None, shape) + self.assertEqual(dm.shape, shape) + class Test_copy(tests.IrisTest): def setUp(self): From 95c09e134981db8d5ead713b87d54096f366efb7 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 12 Dec 2024 11:20:46 +0000 Subject: [PATCH 13/48] refactored tests --- .../tests/unit/data_manager/test_DataManager.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 9308f4fa35..472bfdff1c 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -545,10 +545,10 @@ def test_ndim_nd(self): shape = (2, 3, 4) real_array = np.arange(24).reshape(shape) dm = DataManager(real_array) - self.assertEqual(dm.ndim, len(shape)) + self.assertEqual(dm.ndim, 3) lazy_array = as_lazy_data(real_array) dm = DataManager(lazy_array) - self.assertEqual(dm.ndim, len(shape)) + self.assertEqual(dm.ndim, 3) class Test_shape(tests.IrisTest): @@ -569,10 +569,21 @@ def test_shape_nd(self): dm = DataManager(lazy_array) self.assertEqual(dm.shape, shape) - def test_shape_dataless(self): + def test_shape_data_to_dataless(self): shape = (2, 3, 4) + real_array = np.arange(24).reshape(shape) dm = DataManager(None, shape) self.assertEqual(dm.shape, shape) + dm.data = real_array + self.assertEqual(dm.shape, shape) + + def test_shape_dataless_to_data(self): + shape = (2, 3, 4) + real_array = np.arange(24).reshape(shape) + dm = DataManager(real_array) + self.assertEqual(dm.shape, shape) + dm.data = None + self.assertEqual(dm.shape, shape) class Test_copy(tests.IrisTest): From b2fb2f8de32d48665cfbc4fea45909994293f91f Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 12 Dec 2024 11:31:16 +0000 Subject: [PATCH 14/48] removed shape asserts within data tests --- lib/iris/tests/unit/data_manager/test_DataManager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 472bfdff1c..c95b7a0dfa 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -463,7 +463,6 @@ def test_nd_lazy_to_dataless(self): self.assertTrue(dm.has_lazy_data()) dm.data = None self.assertTrue(dm.core_data() is None) - self.assertTrue(dm.shape == shape) def test_nd_real_to_dataless(self): shape = (2, 3, 4) @@ -473,7 +472,6 @@ def test_nd_real_to_dataless(self): self.assertFalse(dm.has_lazy_data()) dm.data = None self.assertTrue(dm.core_data() is None) - self.assertTrue(dm.shape == shape) def test_dataless_to_nd_lazy(self): shape = (2, 3, 4) From c6510a56c064f0296a22ab4d49327b56d1f44df8 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 12 Dec 2024 15:35:35 +0000 Subject: [PATCH 15/48] copy cube now has FUTURE behaviour --- lib/iris/__init__.py | 9 ++++++--- lib/iris/_data_manager.py | 12 ++++++++---- lib/iris/cube.py | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index d4454efe89..98b3275769 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -143,7 +143,7 @@ def callback(cube, field, filename): class Future(threading.local): """Run-time configuration controller.""" - def __init__(self, datum_support=False, pandas_ndim=False, save_split_attrs=False): + def __init__(self, datum_support=False, pandas_ndim=False, save_split_attrs=False, dataless_cube=False): """Container for run-time options controls. To adjust the values simply update the relevant attribute from @@ -181,6 +181,7 @@ def __init__(self, datum_support=False, pandas_ndim=False, save_split_attrs=Fals self.__dict__["datum_support"] = datum_support self.__dict__["pandas_ndim"] = pandas_ndim self.__dict__["save_split_attrs"] = save_split_attrs + self.__dict__["dataless_cube"] = dataless_cube # TODO: next major release: set IrisDeprecation to subclass # DeprecationWarning instead of UserWarning. @@ -188,8 +189,8 @@ def __init__(self, datum_support=False, pandas_ndim=False, save_split_attrs=Fals def __repr__(self): # msg = ('Future(example_future_flag={})') # return msg.format(self.example_future_flag) - msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={})" - return msg.format(self.datum_support, self.pandas_ndim, self.save_split_attrs) + msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={}, dataless_cubes={})" + return msg.format(self.datum_support, self.pandas_ndim, self.save_split_attrs, self.dataless_cube) # deprecated_options = {'example_future_flag': 'warning',} deprecated_options: dict[str, Literal["error", "warning"]] = {} @@ -832,3 +833,5 @@ def use_plugin(plugin_name): significance of the import statement and warn that it is an unused import. """ importlib.import_module(f"iris.plugins.{plugin_name}") + +MAINTAIN_DATA = "MAINTAINDATA" \ No newline at end of file diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 35677ddac6..e0093a746f 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -157,9 +157,14 @@ def _deepcopy(self, memo, data=None): :class:`~iris._data_manager.DataManager` instance. """ - # @TODO how to ask copy to make an empty cube, special value? flag? + shape = None try: - if data is None: + if (iris.FUTURE.dataless_cube and data is None): + shape = self.shape + elif ( + (iris.FUTURE.dataless_cube and data == iris.MAINTAIN_DATA) + or (data is None) + ): # Copy the managed data. if self.has_lazy_data(): data = copy.deepcopy(self._lazy_array, memo) @@ -172,11 +177,10 @@ def _deepcopy(self, memo, data=None): dm_check.data = data # If the replacement data is valid, then use it but # without copying it. - result = DataManager(data) + result = DataManager(data=data, shape=shape) except ValueError as error: emsg = "Cannot copy {!r} - {}" raise ValueError(emsg.format(type(self).__name__, error)) - return result @property diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 074b42197b..e90cac7b45 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -4084,6 +4084,7 @@ def _deepcopy(self, memo, data=None): aux_coords_and_dims=new_aux_coords_and_dims, cell_measures_and_dims=new_cell_measures_and_dims, ancillary_variables_and_dims=new_ancillary_variables_and_dims, + shape=(dm.shape if dm.core_data() is None else None) ) new_cube.metadata = deepcopy(self.metadata, memo) From af7d727d9635063169a045d8863b5986fe5acb90 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 12 Dec 2024 15:38:26 +0000 Subject: [PATCH 16/48] pre-commit --- lib/iris/__init__.py | 18 +++++++++++++++--- lib/iris/_data_manager.py | 9 ++++----- lib/iris/cube.py | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 98b3275769..37b6a89c9a 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -143,7 +143,13 @@ def callback(cube, field, filename): class Future(threading.local): """Run-time configuration controller.""" - def __init__(self, datum_support=False, pandas_ndim=False, save_split_attrs=False, dataless_cube=False): + def __init__( + self, + datum_support=False, + pandas_ndim=False, + save_split_attrs=False, + dataless_cube=False, + ): """Container for run-time options controls. To adjust the values simply update the relevant attribute from @@ -190,7 +196,12 @@ def __repr__(self): # msg = ('Future(example_future_flag={})') # return msg.format(self.example_future_flag) msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={}, dataless_cubes={})" - return msg.format(self.datum_support, self.pandas_ndim, self.save_split_attrs, self.dataless_cube) + return msg.format( + self.datum_support, + self.pandas_ndim, + self.save_split_attrs, + self.dataless_cube, + ) # deprecated_options = {'example_future_flag': 'warning',} deprecated_options: dict[str, Literal["error", "warning"]] = {} @@ -834,4 +845,5 @@ def use_plugin(plugin_name): """ importlib.import_module(f"iris.plugins.{plugin_name}") -MAINTAIN_DATA = "MAINTAINDATA" \ No newline at end of file + +MAINTAIN_DATA = "MAINTAINDATA" diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index e0093a746f..5f30d33cec 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -159,12 +159,11 @@ def _deepcopy(self, memo, data=None): """ shape = None try: - if (iris.FUTURE.dataless_cube and data is None): + if iris.FUTURE.dataless_cube and data is None: shape = self.shape - elif ( - (iris.FUTURE.dataless_cube and data == iris.MAINTAIN_DATA) - or (data is None) - ): + elif (iris.FUTURE.dataless_cube and data == iris.MAINTAIN_DATA) or ( + data is None + ): # Copy the managed data. if self.has_lazy_data(): data = copy.deepcopy(self._lazy_array, memo) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index e90cac7b45..902999b112 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -4084,7 +4084,7 @@ def _deepcopy(self, memo, data=None): aux_coords_and_dims=new_aux_coords_and_dims, cell_measures_and_dims=new_cell_measures_and_dims, ancillary_variables_and_dims=new_ancillary_variables_and_dims, - shape=(dm.shape if dm.core_data() is None else None) + shape=(dm.shape if dm.core_data() is None else None), ) new_cube.metadata = deepcopy(self.metadata, memo) From 144e164b078b2c0ad77bfb80e439e3279b9abeb7 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 11:46:02 +0000 Subject: [PATCH 17/48] written tests for cube.copy --- lib/iris/tests/unit/cube/test_Cube.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 1f01efd90f..4104b74ffc 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2082,6 +2082,21 @@ def test_copy_cell_measures(self): cube.add_cell_measure(cms, 0) self._check_copy(cube, cube.copy()) + def test_copy_new_data(self): + cube = stock.simple_3d() + new_data = np.ones(cube.shape) + new_cube = cube.copy(data=new_data) + assert new_cube.metadata == new_cube.metadata + _shared_utils.assert_array_equal(new_cube.data, new_data) + + def test_copy_remove_data(self): + iris.FUTURE.dataless_cube = True + cube = stock.simple_3d() + new_cube = cube.copy() + assert new_cube.metadata == cube.metadata + assert new_cube.data is None + assert new_cube.shape == cube.shape + def test__masked_emptymask(self): cube = Cube(ma.array([0, 1])) self._check_copy(cube, cube.copy()) From ba84553bd9b39b8da73875c218a2aed9c4c6bb63 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 13:20:19 +0000 Subject: [PATCH 18/48] fixed cbe.copy failure in dim coords --- lib/iris/coords.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 06a271cbba..bdd2ac231e 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -2708,7 +2708,8 @@ def __deepcopy__(self, memo): # numpydoc ignore=SS02 """ new_coord = copy.deepcopy(super(), memo) # Ensure points and bounds arrays are read-only. - new_coord._values_dm.data.flags.writeable = False + if not (new_coord._values_dm.data is None and iris.FUTURE.dataless_cube): + new_coord._values_dm.data.flags.writeable = False if new_coord._bounds_dm is not None: new_coord._bounds_dm.data.flags.writeable = False return new_coord From ea3c1505c5c75adf834d3b61ddf48268584b49ca Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 13:24:57 +0000 Subject: [PATCH 19/48] experimenting with 4d cube with everything; doesn't run locally --- lib/iris/tests/unit/cube/test_Cube.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 4104b74ffc..b6332af97d 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -642,8 +642,8 @@ def _assert_warn_collapse_without_weight(self, coords, warn): msg = "Collapsing spatial coordinate {!r} without weighting" for coord in coords: assert ( - mock.call(msg.format(coord), category=IrisUserWarning) - in warn.call_args_list + mock.call(msg.format(coord), category=IrisUserWarning) + in warn.call_args_list ) def _assert_nowarn_collapse_without_weight(self, coords, warn): @@ -735,7 +735,7 @@ def _assert_warn_cannot_check_contiguity(self, warn): f"'{coord}'. Ignoring bounds." ) assert ( - mock.call(msg, category=IrisVagueMetadataWarning) in warn.call_args_list + mock.call(msg, category=IrisVagueMetadataWarning) in warn.call_args_list ) def _assert_cube_as_expected(self, cube): @@ -839,7 +839,7 @@ def test_long_components(self): # For lines with any columns : check that columns are where expected for col_ind in colon_inds: # Chop out chars before+after each expected column. - assert line[col_ind - 1 : col_ind + 2] == " x " + assert line[col_ind - 1: col_ind + 2] == " x " # Finally also: compare old with new, but replacing new name and ignoring spacing differences def collapse_space(string): @@ -2091,7 +2091,7 @@ def test_copy_new_data(self): def test_copy_remove_data(self): iris.FUTURE.dataless_cube = True - cube = stock.simple_3d() + cube = stock.realistic_4d_w_everything() new_cube = cube.copy() assert new_cube.metadata == cube.metadata assert new_cube.data is None From 0cec4caf1a9061621774b200e9ff5f431b5c7894 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 13:26:14 +0000 Subject: [PATCH 20/48] pre-c --- lib/iris/tests/unit/cube/test_Cube.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index b6332af97d..5595bc29c9 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -642,8 +642,8 @@ def _assert_warn_collapse_without_weight(self, coords, warn): msg = "Collapsing spatial coordinate {!r} without weighting" for coord in coords: assert ( - mock.call(msg.format(coord), category=IrisUserWarning) - in warn.call_args_list + mock.call(msg.format(coord), category=IrisUserWarning) + in warn.call_args_list ) def _assert_nowarn_collapse_without_weight(self, coords, warn): @@ -735,7 +735,7 @@ def _assert_warn_cannot_check_contiguity(self, warn): f"'{coord}'. Ignoring bounds." ) assert ( - mock.call(msg, category=IrisVagueMetadataWarning) in warn.call_args_list + mock.call(msg, category=IrisVagueMetadataWarning) in warn.call_args_list ) def _assert_cube_as_expected(self, cube): @@ -839,7 +839,7 @@ def test_long_components(self): # For lines with any columns : check that columns are where expected for col_ind in colon_inds: # Chop out chars before+after each expected column. - assert line[col_ind - 1: col_ind + 2] == " x " + assert line[col_ind - 1 : col_ind + 2] == " x " # Finally also: compare old with new, but replacing new name and ignoring spacing differences def collapse_space(string): From 6ed270de6cf746edb75df173fecb2ccb79ac6c4d Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 13:57:07 +0000 Subject: [PATCH 21/48] edited Coord.copy --- lib/iris/coords.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/iris/coords.py b/lib/iris/coords.py index bdd2ac231e..7888f44e4f 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -200,7 +200,7 @@ def _sanitise_array(self, src, ndmin): else: extended_shape = tuple([1] * ndims_missing + list(src.shape)) result = src.reshape(extended_shape) - else: + elif src is None: # Real data : a few more things to do in this case. # Ensure the array is writeable. # NB. Returns the *same object* if src is already writeable. @@ -1535,6 +1535,10 @@ def copy(self, points=None, bounds=None): """ if points is None and bounds is not None: raise ValueError("If bounds are specified, points must also be specified") + if points is None and iris.FUTURE.dataless_cube: + # dataless coords are not currently implemented, so points should never + # be removed in a copy + points = iris.MAINTAIN_DATA new_coord = super().copy(values=points) if points is not None: From d79ad270810c382f68c80005e75cf8de4fadfa3c Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 14:05:14 +0000 Subject: [PATCH 22/48] Revert "edited Coord.copy" This reverts commit 6ed270de6cf746edb75df173fecb2ccb79ac6c4d. --- lib/iris/coords.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 7888f44e4f..bdd2ac231e 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -200,7 +200,7 @@ def _sanitise_array(self, src, ndmin): else: extended_shape = tuple([1] * ndims_missing + list(src.shape)) result = src.reshape(extended_shape) - elif src is None: + else: # Real data : a few more things to do in this case. # Ensure the array is writeable. # NB. Returns the *same object* if src is already writeable. @@ -1535,10 +1535,6 @@ def copy(self, points=None, bounds=None): """ if points is None and bounds is not None: raise ValueError("If bounds are specified, points must also be specified") - if points is None and iris.FUTURE.dataless_cube: - # dataless coords are not currently implemented, so points should never - # be removed in a copy - points = iris.MAINTAIN_DATA new_coord = super().copy(values=points) if points is not None: From 2bb6e61a3938cfed76779d9462b4494fa2385c0b Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 16 Dec 2024 16:09:32 +0000 Subject: [PATCH 23/48] tried tearingdown FUTUREFLAG --- lib/iris/_data_manager.py | 2 +- lib/iris/tests/unit/cube/test_Cube.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 5f30d33cec..c5b7df8e1c 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -161,7 +161,7 @@ def _deepcopy(self, memo, data=None): try: if iris.FUTURE.dataless_cube and data is None: shape = self.shape - elif (iris.FUTURE.dataless_cube and data == iris.MAINTAIN_DATA) or ( + elif (iris.FUTURE.dataless_cube and type(data) is str and data == iris.MAINTAIN_DATA) or ( data is None ): # Copy the managed data. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 5595bc29c9..7dc3068866 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2090,12 +2090,12 @@ def test_copy_new_data(self): _shared_utils.assert_array_equal(new_cube.data, new_data) def test_copy_remove_data(self): - iris.FUTURE.dataless_cube = True - cube = stock.realistic_4d_w_everything() - new_cube = cube.copy() - assert new_cube.metadata == cube.metadata - assert new_cube.data is None - assert new_cube.shape == cube.shape + with iris.FUTURE.context(dataless_cube = True): + cube = stock.simple_3d() + new_cube = cube.copy() + assert new_cube.metadata == cube.metadata + assert new_cube.data is None + assert new_cube.shape == cube.shape def test__masked_emptymask(self): cube = Cube(ma.array([0, 1])) From 063e45b8e3dd7583f44d4bc44f0ccde336e892ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:11:48 +0000 Subject: [PATCH 24/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/_data_manager.py | 8 +++++--- lib/iris/tests/unit/cube/test_Cube.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index c5b7df8e1c..d27d759161 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -161,9 +161,11 @@ def _deepcopy(self, memo, data=None): try: if iris.FUTURE.dataless_cube and data is None: shape = self.shape - elif (iris.FUTURE.dataless_cube and type(data) is str and data == iris.MAINTAIN_DATA) or ( - data is None - ): + elif ( + iris.FUTURE.dataless_cube + and type(data) is str + and data == iris.MAINTAIN_DATA + ) or (data is None): # Copy the managed data. if self.has_lazy_data(): data = copy.deepcopy(self._lazy_array, memo) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 7dc3068866..cf832719a0 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2090,7 +2090,7 @@ def test_copy_new_data(self): _shared_utils.assert_array_equal(new_cube.data, new_data) def test_copy_remove_data(self): - with iris.FUTURE.context(dataless_cube = True): + with iris.FUTURE.context(dataless_cube=True): cube = stock.simple_3d() new_cube = cube.copy() assert new_cube.metadata == cube.metadata From 79d7d6720b3eb63b320074dcced33480ba02add9 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 17 Dec 2024 10:22:20 +0000 Subject: [PATCH 25/48] made dataless copy opt-in behaviour --- lib/iris/__init__.py | 7 ++----- lib/iris/_data_manager.py | 11 ++++------- lib/iris/coords.py | 3 +-- lib/iris/tests/unit/cube/test_Cube.py | 11 +++++------ 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 37b6a89c9a..5a6920d4ce 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -148,7 +148,6 @@ def __init__( datum_support=False, pandas_ndim=False, save_split_attrs=False, - dataless_cube=False, ): """Container for run-time options controls. @@ -187,7 +186,6 @@ def __init__( self.__dict__["datum_support"] = datum_support self.__dict__["pandas_ndim"] = pandas_ndim self.__dict__["save_split_attrs"] = save_split_attrs - self.__dict__["dataless_cube"] = dataless_cube # TODO: next major release: set IrisDeprecation to subclass # DeprecationWarning instead of UserWarning. @@ -195,12 +193,11 @@ def __init__( def __repr__(self): # msg = ('Future(example_future_flag={})') # return msg.format(self.example_future_flag) - msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={}, dataless_cubes={})" + msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={})" return msg.format( self.datum_support, self.pandas_ndim, self.save_split_attrs, - self.dataless_cube, ) # deprecated_options = {'example_future_flag': 'warning',} @@ -846,4 +843,4 @@ def use_plugin(plugin_name): importlib.import_module(f"iris.plugins.{plugin_name}") -MAINTAIN_DATA = "MAINTAINDATA" +DATALESS_COPY = "NONE" diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index d27d759161..00db3ff329 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -159,18 +159,15 @@ def _deepcopy(self, memo, data=None): """ shape = None try: - if iris.FUTURE.dataless_cube and data is None: - shape = self.shape - elif ( - iris.FUTURE.dataless_cube - and type(data) is str - and data == iris.MAINTAIN_DATA - ) or (data is None): + if data is None: # Copy the managed data. if self.has_lazy_data(): data = copy.deepcopy(self._lazy_array, memo) else: data = self._real_array.copy() + elif type(data) is str and data == iris.DATALESS_COPY: + shape = self.shape + data = None else: # Check that the replacement data is valid relative to # the currently managed data. diff --git a/lib/iris/coords.py b/lib/iris/coords.py index bdd2ac231e..06a271cbba 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -2708,8 +2708,7 @@ def __deepcopy__(self, memo): # numpydoc ignore=SS02 """ new_coord = copy.deepcopy(super(), memo) # Ensure points and bounds arrays are read-only. - if not (new_coord._values_dm.data is None and iris.FUTURE.dataless_cube): - new_coord._values_dm.data.flags.writeable = False + new_coord._values_dm.data.flags.writeable = False if new_coord._bounds_dm is not None: new_coord._bounds_dm.data.flags.writeable = False return new_coord diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index cf832719a0..54fb49ea73 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2090,12 +2090,11 @@ def test_copy_new_data(self): _shared_utils.assert_array_equal(new_cube.data, new_data) def test_copy_remove_data(self): - with iris.FUTURE.context(dataless_cube=True): - cube = stock.simple_3d() - new_cube = cube.copy() - assert new_cube.metadata == cube.metadata - assert new_cube.data is None - assert new_cube.shape == cube.shape + cube = stock.simple_3d() + new_cube = cube.copy(iris.DATALESS_COPY) + assert new_cube.metadata == cube.metadata + assert new_cube.data is None + assert new_cube.shape == cube.shape def test__masked_emptymask(self): cube = Cube(ma.array([0, 1])) From 9b52067a1ff90454327f4524ac0402c0cc895c68 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 17 Dec 2024 11:39:14 +0000 Subject: [PATCH 26/48] cube data is optional --- lib/iris/cube.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 902999b112..fe497ef228 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1190,7 +1190,7 @@ def _walk_nodes(node): def __init__( self, - data: np.typing.ArrayLike, + data: np.typing.ArrayLike | None = None, standard_name: str | None = None, long_name: str | None = None, var_name: str | None = None, @@ -1251,6 +1251,9 @@ def __init__( A list of CellMeasures with dimension mappings. ancillary_variables_and_dims : A list of AncillaryVariables with dimension mappings. + shape : + An alternative to providing data, this defines the shape of the + cube, but initialises the cube as dataless. Examples -------- From 2f2b56ad8638d828c6cc31d8b3dbebae47b93f15 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 17 Dec 2024 14:38:23 +0000 Subject: [PATCH 27/48] added is_dataless, and corrected data manager exceptions to not be cube specific --- lib/iris/_data_manager.py | 19 ++++++++++++++----- lib/iris/cube.py | 10 ++++++++++ .../unit/data_manager/test_DataManager.py | 13 +++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 00db3ff329..d7956b9f18 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -25,12 +25,18 @@ def __init__(self, data, shape=None): data : The :class:`~numpy.ndarray` or :class:`~numpy.ma.core.MaskedArray` real data, or :class:`~dask.array.core.Array` lazy data to be - managed. + managed. If a value of None is given, the data manager will be + considered dataless. + + shape : + A tuple, representing the shape of the data manager. This can only + be used in the case of `data=None`, and will render the data manager + dataless. """ if (shape is not None) and (data is not None): - msg = "A cube may not be created with both data and a custom shape." - raise iris.exceptions.InvalidCubeError(msg) + msg = "`shape` should only be provided if `data is None`" + raise ValueError(msg) # Initialise the instance. self._lazy_array = None @@ -134,12 +140,12 @@ def _assert_axioms(self): overfilled = self._lazy_array is not None and self._real_array is not None if overfilled: msg = "Unexpected data state, got both lazy and real data." - raise iris.exceptions.InvalidCubeError(msg) + raise ValueError(msg) elif ( empty and self._shape is None ): # if I remove the second check, allows empty arrays, like old behaviour msg = "Unexpected data state, got no lazy or real data, and no shape." - raise iris.exceptions.InvalidCubeError(msg) + raise ValueError(msg) def _deepcopy(self, memo, data=None): """Perform a deepcopy of the :class:`~iris._data_manager.DataManager` instance. @@ -290,6 +296,9 @@ def shape(self): result = self.core_data().shape return result + def is_dataless(self): + return (self.core_data() is None) and (self.shape is not None) + def copy(self, data=None): """Return a deep copy of this :class:`~iris._data_manager.DataManager` instance. diff --git a/lib/iris/cube.py b/lib/iris/cube.py index fe497ef228..fca5539135 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2887,6 +2887,16 @@ def has_lazy_data(self) -> bool: """ return self._data_manager.has_lazy_data() + def is_dataless(self) -> bool: + """Detail whether this :class:`~iris.cube.Cube` is dataless. + + Returns + ------- + bool + + """ + return self._data_manager.is_dataless() + @property def dim_coords(self) -> tuple[DimCoord, ...]: """Return a tuple of all the dimension coordinates, ordered by dimension. diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index c95b7a0dfa..02cefc1e7e 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -647,6 +647,19 @@ def test_with_lazy_array(self): self.assertTrue(dm.has_lazy_data()) self.assertIs(result, dm._lazy_array) +class Test_is_dataless(tests.IrisTest): + def setUp(self): + self.data = np.array(0) + self.shape = (0) + + def test_with_data(self): + dm = DataManager(self.data) + self.assertFalse(dm.is_dataless()) + + def test_without_data(self): + dm = DataManager(None, self.shape) + self.assertTrue(dm.is_dataless()) + if __name__ == "__main__": tests.main() From c8d2c073ae28333618a12afea066d307ac8368b1 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 17 Dec 2024 14:40:07 +0000 Subject: [PATCH 28/48] pre-c --- lib/iris/tests/unit/data_manager/test_DataManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 02cefc1e7e..e88536f975 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -647,10 +647,11 @@ def test_with_lazy_array(self): self.assertTrue(dm.has_lazy_data()) self.assertIs(result, dm._lazy_array) + class Test_is_dataless(tests.IrisTest): def setUp(self): self.data = np.array(0) - self.shape = (0) + self.shape = 0 def test_with_data(self): dm = DataManager(self.data) From 67fb787f8e2900c52d4369cf82bc5b6be8aa402d Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 17 Dec 2024 15:07:48 +0000 Subject: [PATCH 29/48] fixed failing tests --- lib/iris/tests/unit/data_manager/test_DataManager.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index e88536f975..83ba548653 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -4,8 +4,6 @@ # See LICENSE in the root of the repository for full licensing details. """Unit tests for the :class:`iris._data_manager.DataManager`.""" -import iris.exceptions - # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests # isort:skip @@ -170,13 +168,13 @@ def setUp(self): def test_array_none(self): self.dm._real_array = None emsg = "Unexpected data state, got no lazy or real data, and no shape." - with self.assertRaisesRegex(iris.exceptions.InvalidCubeError, emsg): + with self.assertRaisesRegex(ValueError, emsg): self.dm._assert_axioms() def test_array_all(self): self.dm._lazy_array = self.lazy_array emsg = "Unexpected data state, got both lazy and real data." - with self.assertRaisesRegex(iris.exceptions.InvalidCubeError, emsg): + with self.assertRaisesRegex(ValueError, emsg): self.dm._assert_axioms() From a04245171e4ce9058628ba637c5890d706bfdaa7 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 17 Dec 2024 16:17:21 +0000 Subject: [PATCH 30/48] fixed copying from dataless --- lib/iris/_data_manager.py | 4 +++- lib/iris/tests/unit/cube/test_Cube.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index d7956b9f18..0382afa373 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -169,8 +169,10 @@ def _deepcopy(self, memo, data=None): # Copy the managed data. if self.has_lazy_data(): data = copy.deepcopy(self._lazy_array, memo) - else: + elif self._real_array is not None: data = self._real_array.copy() + else: + shape = self.shape elif type(data) is str and data == iris.DATALESS_COPY: shape = self.shape data = None diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 54fb49ea73..3d12fcc6cc 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2082,20 +2082,28 @@ def test_copy_cell_measures(self): cube.add_cell_measure(cms, 0) self._check_copy(cube, cube.copy()) - def test_copy_new_data(self): + def test_copy_replace_data(self): cube = stock.simple_3d() new_data = np.ones(cube.shape) new_cube = cube.copy(data=new_data) assert new_cube.metadata == new_cube.metadata _shared_utils.assert_array_equal(new_cube.data, new_data) - def test_copy_remove_data(self): + def test_copy_to_dataless(self): cube = stock.simple_3d() new_cube = cube.copy(iris.DATALESS_COPY) assert new_cube.metadata == cube.metadata assert new_cube.data is None assert new_cube.shape == cube.shape + def test_copy_from_dataless(self): + cube = stock.simple_3d() + cube.data = None + new_cube = cube.copy() + assert new_cube.metadata == cube.metadata + assert new_cube.data is cube.data + assert new_cube.shape == cube.shape + def test__masked_emptymask(self): cube = Cube(ma.array([0, 1])) self._check_copy(cube, cube.copy()) From 21489d572fdb500fae5852a839a09afc7f89890f Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 12:49:30 +0000 Subject: [PATCH 31/48] added in exceptions, not tested --- lib/iris/cube.py | 24 +++++++++++++++++++++++- lib/iris/exceptions.py | 11 +++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index fca5539135..e5d0573b0d 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -174,6 +174,8 @@ def _assert_is_cube(obj): if not hasattr(obj, "add_aux_coord"): msg = r"Object {obj} cannot be put in a cubelist, as it is not a Cube." raise ValueError(msg) + elif obj.is_dataless(): + raise iris.exceptions.DatalessError("CubeList") def _repr_html_(self): from iris.experimental.representation import CubeListRepresentation @@ -1479,6 +1481,8 @@ def convert_units(self, unit: str | Unit) -> None: """ # If the cube has units convert the data. + if self.is_dataless(): + raise iris.exceptions.DatalessError("convert_units") if self.units.is_unknown(): raise iris.exceptions.UnitConversionError( "Cannot convert from unknown units. " @@ -3105,6 +3109,8 @@ def subset(self, coord: AuxCoord | DimCoord) -> Cube | None: whole cube is returned. As such, the operation is not strict. """ + if self.is_dataless(): + raise iris.exceptions.DatalessError("subset") if not isinstance(coord, iris.coords.Coord): raise ValueError("coord_to_extract must be a valid Coord.") @@ -3226,6 +3232,8 @@ def intersection(self, *args, **kwargs) -> Cube: which intersects with the requested coordinate intervals. """ + if self.is_dataless(): + raise iris.exceptions.DatalessError("intersection") result = self ignore_bounds = kwargs.pop("ignore_bounds", False) threshold = kwargs.pop("threshold", 0) @@ -3750,6 +3758,9 @@ def slices( dimension index. """ # noqa: D214, D406, D407, D410, D411 + if self.is_dataless(): + raise iris.exceptions.DatalessError("slices") + if not isinstance(ordered, bool): raise TypeError("'ordered' argument to slices must be boolean.") @@ -3837,7 +3848,8 @@ def transpose(self, new_order: list[int] | None = None) -> None: # Transpose the data payload. dm = self._data_manager - data = dm.core_data().transpose(new_order) + if not self.is_dataless(): + data = dm.core_data().transpose(new_order) self._data_manager = DataManager(data) dim_mapping = {src: dest for dest, src in enumerate(new_order)} @@ -4325,6 +4337,8 @@ def collapsed( cube.collapsed(['latitude', 'longitude'], iris.analysis.VARIANCE) """ + if self.is_dataless(): + raise iris.exceptions.DatalessError("collapsed") # Update weights kwargs (if necessary) to handle different types of # weights weights_info = None @@ -4545,6 +4559,8 @@ def aggregated_by( STASH m01s00i024 """ + if self.is_dataless(): + raise iris.exceptions.DatalessError("aggregated_by") # Update weights kwargs (if necessary) to handle different types of # weights weights_info = None @@ -4844,6 +4860,8 @@ def rolling_window( """ # noqa: D214, D406, D407, D410, D411 # Update weights kwargs (if necessary) to handle different types of # weights + if self.is_dataless(): + raise iris.exceptions.DatalessError("rolling_window") weights_info = None if kwargs.get("weights") is not None: weights_info = _Weights(kwargs["weights"], self) @@ -5049,6 +5067,8 @@ def interpolate( True """ + if self.is_dataless(): + raise iris.exceptions.DatalessError("interoplate") coords, points = zip(*sample_points) interp = scheme.interpolator(self, coords) # type: ignore[arg-type] return interp(points, collapse_scalar=collapse_scalar) @@ -5094,6 +5114,8 @@ def regrid(self, grid: Cube, scheme: iris.analysis.RegriddingScheme) -> Cube: this function is not applicable. """ + if self.is_dataless(): + raise iris.exceptions.DatalessError("regrid") regridder = scheme.regridder(self, grid) return regridder(self) diff --git a/lib/iris/exceptions.py b/lib/iris/exceptions.py index d6d2084d3c..450afce3a6 100644 --- a/lib/iris/exceptions.py +++ b/lib/iris/exceptions.py @@ -161,3 +161,14 @@ class CannotAddError(ValueError): """Raised when an object (e.g. coord) cannot be added to a :class:`~iris.cube.Cube`.""" pass + + +class DatalessError(ValueError): + """Raised when an method cannot be performed on a dataless :class:`~iris.cube.Cube`.""" + + def __str__(self): + msg = ( + "Dataless cubes are still early in implementation, and dataless {} " + "operations are not currently supported." + ) + return msg.format(super().__str__()) From a059c5e483429d32a133af88b8e5a76484e7b25e Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 13:54:41 +0000 Subject: [PATCH 32/48] renamed DATALESS_COPY to DATALESS, and added comment --- lib/iris/__init__.py | 3 ++- lib/iris/_data_manager.py | 2 +- lib/iris/tests/unit/cube/test_Cube.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 5a6920d4ce..90d5ac62d2 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -843,4 +843,5 @@ def use_plugin(plugin_name): importlib.import_module(f"iris.plugins.{plugin_name}") -DATALESS_COPY = "NONE" +# To be used when copying a cube to make the new cube dataless. +DATALESS = "NONE" diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 0382afa373..ab7d1bef89 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -173,7 +173,7 @@ def _deepcopy(self, memo, data=None): data = self._real_array.copy() else: shape = self.shape - elif type(data) is str and data == iris.DATALESS_COPY: + elif type(data) is str and data == iris.DATALESS: shape = self.shape data = None else: diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 3d12fcc6cc..a2bed9685b 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2091,7 +2091,7 @@ def test_copy_replace_data(self): def test_copy_to_dataless(self): cube = stock.simple_3d() - new_cube = cube.copy(iris.DATALESS_COPY) + new_cube = cube.copy(iris.DATALESS) assert new_cube.metadata == cube.metadata assert new_cube.data is None assert new_cube.shape == cube.shape From 8178809bfa31bfb7be83dc1cd632c139d889d528 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 14:24:39 +0000 Subject: [PATCH 33/48] made DATALESS docstring a docstring, rather than a comment --- lib/iris/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 82c1cec41b..fd1b757ed1 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -853,5 +853,5 @@ def use_plugin(plugin_name): importlib.import_module(f"iris.plugins.{plugin_name}") -# To be used when copying a cube to make the new cube dataless. +#: To be used when copying a cube to make the new cube dataless. DATALESS = "NONE" From 2123b8f1c274c03a0e36e615aaff4a6adc95f438 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 14:49:22 +0000 Subject: [PATCH 34/48] added whatsnew --- docs/src/whatsnew/latest.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index ae13b8a883..0c3bac110e 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -38,6 +38,17 @@ This document explains the changes made to Iris for this release your code for new floating point problems if activating this (e.g. when using the :class:`~iris.Constraint` API). (:pull:`6260`) +#. `@ESadek-MO`_ made :attr:`~iris.cube.Cube.data` optional in a + :class:`~iris.cube.Cube`, when :attr:`~iris.cube.Cube.shape` is provided + instead. `dataless cubes` can currently be used as targets in regridding, or + for templates to add data to at a later time. + + This is the first step in making `dataless cubes`. Currently, most cube methods + don't work on `dataless cubes`, and will raise in an error if attempted. + :meth:`~iris.cube.Cube.transpose` will work, as will :meth:`~iris.cube.Cube.copy`. + `my_cube.copy(data = iris.DATALESS)` will copy the cube and remove data in + the process. + (:issue:`4447`, :pull:`6253`) 🐛 Bugs Fixed ============= From 703bbb6826c3a67d7ac8b35aaf65bd622a515015 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 16:45:43 +0000 Subject: [PATCH 35/48] added cubelist errors --- lib/iris/_concatenate.py | 6 +++++- lib/iris/_lazy_data.py | 6 +++++- lib/iris/_merge.py | 4 ++++ lib/iris/cube.py | 3 --- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py index ac9e699790..6caee79c4f 100644 --- a/lib/iris/_concatenate.py +++ b/lib/iris/_concatenate.py @@ -572,7 +572,11 @@ def concatenate( A :class:`iris.cube.CubeList` of concatenated :class:`iris.cube.Cube` instances. """ - cube_signatures = [_CubeSignature(cube) for cube in cubes] + cube_signatures = [] + for cube in cubes: + if cube.is_dataless(): + raise iris.exceptions.DatalessError("concatenate") + cube_signatures.append(_CubeSignature(cube)) proto_cubes: list[_ProtoCube] = [] # Initialise the nominated axis (dimension) of concatenation diff --git a/lib/iris/_lazy_data.py b/lib/iris/_lazy_data.py index a3dfa1edb4..e3b95e685a 100644 --- a/lib/iris/_lazy_data.py +++ b/lib/iris/_lazy_data.py @@ -16,6 +16,7 @@ import dask.array as da import dask.config import dask.utils +import iris.exceptions import numpy as np import numpy.ma as ma @@ -317,7 +318,10 @@ def _co_realise_lazy_arrays(arrays): # Note : in some cases dask (and numpy) will return a scalar # numpy.int/numpy.float object rather than an ndarray. # Recorded in https://github.com/dask/dask/issues/2111. - real_out = np.asanyarray(real_out) + if real_out is not None: + real_out = np.asanyarray(real_out) + else: + raise iris.exceptions.DatalessError("realising") if isinstance(real_out, ma.core.MaskedConstant): # Convert any masked constants into NumPy masked arrays. # NOTE: in this case, also apply the original lazy-array dtype, as diff --git a/lib/iris/_merge.py b/lib/iris/_merge.py index 5e00d9b2f0..633ac0f2da 100644 --- a/lib/iris/_merge.py +++ b/lib/iris/_merge.py @@ -1109,6 +1109,8 @@ def __init__(self, cube): source-cube. """ + if cube.is_dataless(): + raise iris.exceptions.DatalessError("merge") # Default hint ordering for candidate dimension coordinates. self._hints = [ "time", @@ -1289,6 +1291,8 @@ def register(self, cube, error_on_mismatch=False): this :class:`ProtoCube`. """ + if cube.is_dataless(): + raise iris.exceptions.DatalessError("merge") cube_signature = self._cube_signature other = self._build_signature(cube) match = cube_signature.match(other, error_on_mismatch) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index e5d0573b0d..744502e390 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -174,8 +174,6 @@ def _assert_is_cube(obj): if not hasattr(obj, "add_aux_coord"): msg = r"Object {obj} cannot be put in a cubelist, as it is not a Cube." raise ValueError(msg) - elif obj.is_dataless(): - raise iris.exceptions.DatalessError("CubeList") def _repr_html_(self): from iris.experimental.representation import CubeListRepresentation @@ -412,7 +410,6 @@ def merge_cube(self): """ if not self: raise ValueError("can't merge an empty CubeList") - # Register each of our cubes with a single ProtoCube. proto_cube = iris._merge.ProtoCube(self[0]) for c in self[1:]: From 33649c11260761937a147f259b7ca66365cc153f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:46:30 +0000 Subject: [PATCH 36/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/_lazy_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/_lazy_data.py b/lib/iris/_lazy_data.py index e3b95e685a..cfa5fde4b7 100644 --- a/lib/iris/_lazy_data.py +++ b/lib/iris/_lazy_data.py @@ -16,10 +16,11 @@ import dask.array as da import dask.config import dask.utils -import iris.exceptions import numpy as np import numpy.ma as ma +import iris.exceptions + def non_lazy(func): """Turn a lazy function into a function that returns a result immediately.""" From 021bd3135199c72ee2ad896aa78a0ad0bd0a0dee Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 17:26:04 +0000 Subject: [PATCH 37/48] review comments --- lib/iris/__init__.py | 7 ++- lib/iris/_data_manager.py | 111 +++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 61 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index fd1b757ed1..64d1fe64c9 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -118,6 +118,7 @@ def callback(cube, field, filename): __all__ = [ "AttributeConstraint", "Constraint", + "DATALESS", "FUTURE", "Future", "IrisDeprecation", @@ -139,6 +140,8 @@ def callback(cube, field, filename): AttributeConstraint = iris._constraints.AttributeConstraint NameConstraint = iris._constraints.NameConstraint +#: To be used when copying a cube to make the new cube dataless. +DATALESS = "NONE" class Future(threading.local): """Run-time configuration controller.""" @@ -851,7 +854,3 @@ def use_plugin(plugin_name): significance of the import statement and warn that it is an unused import. """ importlib.import_module(f"iris.plugins.{plugin_name}") - - -#: To be used when copying a cube to make the new cube dataless. -DATALESS = "NONE" diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 5102c9c2f9..2268ec39f5 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -22,28 +22,29 @@ def __init__(self, data, shape=None): Parameters ---------- - data : + data : np.typing.ArrayLike, optional The :class:`~numpy.ndarray` or :class:`~numpy.ma.core.MaskedArray` real data, or :class:`~dask.array.core.Array` lazy data to be managed. If a value of None is given, the data manager will be considered dataless. - shape : + shape : tuple, optional A tuple, representing the shape of the data manager. This can only - be used in the case of `data=None`, and will render the data manager + be used in the case of ``data=None``, and will render the data manager dataless. """ if (shape is not None) and (data is not None): - msg = "`shape` should only be provided if `data is None`" + msg = '"shape" should only be provided if "data" is None' raise ValueError(msg) + self._shape = shape + # Initialise the instance. self._lazy_array = None self._real_array = None # Assign the data payload to be managed. - self._shape = shape self.data = data def __copy__(self): @@ -136,16 +137,16 @@ def __repr__(self): def _assert_axioms(self): """Definition of the manager state, that should never be violated.""" # Ensure there is a valid data state. - empty = self._lazy_array is None and self._real_array is None - overfilled = self._lazy_array is not None and self._real_array is not None - if overfilled: - msg = "Unexpected data state, got both lazy and real data." - raise ValueError(msg) - elif ( - empty and self._shape is None - ): # if I remove the second check, allows empty arrays, like old behaviour - msg = "Unexpected data state, got no lazy or real data, and no shape." - raise ValueError(msg) + is_lazy = self._lazy_array is not None + is_real = self._real_array is not None + + if not (is_lazy ^ is_real): + if is_lazy and is_real: + msg = "Unexpected data state, got both lazy and real data." + raise ValueError(msg) + if not self._shape and not (is_lazy or is_real): + msg = "Unexpected data state, got no lazy or real data, and no shape." + raise ValueError(msg) def _deepcopy(self, memo, data=None): """Perform a deepcopy of the :class:`~iris._data_manager.DataManager` instance. @@ -172,7 +173,7 @@ def _deepcopy(self, memo, data=None): elif self._real_array is not None: data = self._real_array.copy() else: - shape = self.shape + shape = self._shape elif type(data) is str and data == iris.DATALESS: shape = self.shape data = None @@ -192,6 +193,7 @@ def _deepcopy(self, memo, data=None): @property def data(self): """Return the real data. Any lazy data being managed will be realised. + ``None`` will be returned if in the dataless state. Returns ------- @@ -236,47 +238,46 @@ def data(self, data): data : The :class:`~numpy.ndarray` or :class:`~numpy.ma.core.MaskedArray` real data, or :class:`~dask.array.core.Array` lazy data to be - managed. + managed. If data is None, the current shape will be maintained. """ - # If data is None, ensure previous shape is maintained, and that it is - # not wrapped in an np.array - dataless = data is None - if dataless: + if dataless := data is None: self._shape = self.shape + self._lazy_array = None + self._real_array = None # Ensure we have numpy-like data. - elif not (hasattr(data, "shape") and hasattr(data, "dtype")): - data = np.asanyarray(data) - - # Determine whether the class already has a defined shape, - # as this method is called from __init__. - has_shape = self.shape is not None - if has_shape and not dataless and self.shape != data.shape: - # The _ONLY_ data reshape permitted is converting a 0-dimensional - # array i.e. self.shape == () into a 1-dimensional array of length - # one i.e. data.shape == (1,) - if self.shape or data.shape != (1,): - emsg = "Require data with shape {!r}, got {!r}." - raise ValueError(emsg.format(self.shape, data.shape)) - - # Set lazy or real data, and reset the other. - if is_lazy_data(data): - self._lazy_array = data - self._real_array = None else: - if not ma.isMaskedArray(data): - # Coerce input data to ndarray (including ndarray subclasses). - if not dataless: + if not (hasattr(data, "shape") and hasattr(data, "dtype")): + data = np.asanyarray(data) + + # Determine whether the class already has a defined shape, + # as this method is called from __init__. + has_shape = self.shape is not None + if has_shape and self.shape != data.shape: + # The _ONLY_ data reshape permitted is converting a 0-dimensional + # array i.e. self.shape == () into a 1-dimensional array of length + # one i.e. data.shape == (1,) + if self.shape or data.shape != (1,): + emsg = "Require data with shape {!r}, got {!r}." + raise ValueError(emsg.format(self.shape, data.shape)) + + # Set lazy or real data, and reset the other. + if is_lazy_data(data): + self._lazy_array = data + self._real_array = None + else: + if not ma.isMaskedArray(data): + # Coerce input data to ndarray (including ndarray subclasses). data = np.asarray(data) - if isinstance(data, ma.core.MaskedConstant): - # Promote to a masked array so that the fill-value is - # writeable to the data owner. - data = ma.array(data.data, mask=data.mask, dtype=data.dtype) - self._lazy_array = None - self._real_array = data + if isinstance(data, ma.core.MaskedConstant): + # Promote to a masked array so that the fill-value is + # writeable to the data owner. + data = ma.array(data.data, mask=data.mask, dtype=data.dtype) + self._lazy_array = None + self._real_array = data - # Check the manager contract, as the managed data has changed. + # Check the manager contract, as the managed data has changed. self._assert_axioms() @property @@ -292,21 +293,17 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - if self.core_data() is None: - result = self._shape - else: - result = self.core_data().shape - return result + return self._shape if self._shape else self.core_data().shape - def is_dataless(self): - """Determine whether the cube is dataless. + def is_dataless(self) -> bool: + """Determine whether the cube has no data. Returns ------- bool """ - return (self.core_data() is None) and (self.shape is not None) + return self.core_data() is None def copy(self, data=None): """Return a deep copy of this :class:`~iris._data_manager.DataManager` instance. From abb5a57a0a4f29cee81ea150a65f25b882003ec1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:27:12 +0000 Subject: [PATCH 38/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 64d1fe64c9..96095bb74a 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -143,6 +143,7 @@ def callback(cube, field, filename): #: To be used when copying a cube to make the new cube dataless. DATALESS = "NONE" + class Future(threading.local): """Run-time configuration controller.""" From 94d02eac498a99570c404269cb4578abcda1fef6 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 18 Dec 2024 17:34:32 +0000 Subject: [PATCH 39/48] fixed broken review suggestion --- lib/iris/__init__.py | 1 + lib/iris/_data_manager.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 64d1fe64c9..96095bb74a 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -143,6 +143,7 @@ def callback(cube, field, filename): #: To be used when copying a cube to make the new cube dataless. DATALESS = "NONE" + class Future(threading.local): """Run-time configuration controller.""" diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 2268ec39f5..823c9afdba 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -193,11 +193,10 @@ def _deepcopy(self, memo, data=None): @property def data(self): """Return the real data. Any lazy data being managed will be realised. - ``None`` will be returned if in the dataless state. Returns ------- - :class:`~numpy.ndarray` or :class:`numpy.ma.core.MaskedArray`. + :class:`~numpy.ndarray` or :class:`numpy.ma.core.MaskedArray` or None. """ if self.has_lazy_data(): @@ -241,7 +240,7 @@ def data(self, data): managed. If data is None, the current shape will be maintained. """ - if dataless := data is None: + if data is None: self._shape = self.shape self._lazy_array = None self._real_array = None @@ -293,7 +292,11 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - return self._shape if self._shape else self.core_data().shape + if self.core_data() is None: + result = self._shape + else: + result = self.core_data().shape + return result def is_dataless(self) -> bool: """Determine whether the cube has no data. From 5c5bbf81ea10d87c8a887e20f80305d2343af831 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Fri, 20 Dec 2024 19:23:23 +0000 Subject: [PATCH 40/48] fixed some problems, and ensured self._shape is always set --- lib/iris/_data_manager.py | 12 +++++------- lib/iris/tests/unit/data_manager/test_DataManager.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 823c9afdba..26a4750062 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -144,7 +144,7 @@ def _assert_axioms(self): if is_lazy and is_real: msg = "Unexpected data state, got both lazy and real data." raise ValueError(msg) - if not self._shape and not (is_lazy or is_real): + elif not self._shape: msg = "Unexpected data state, got no lazy or real data, and no shape." raise ValueError(msg) @@ -252,7 +252,7 @@ def data(self, data): # Determine whether the class already has a defined shape, # as this method is called from __init__. - has_shape = self.shape is not None + has_shape = self._shape is not None if has_shape and self.shape != data.shape: # The _ONLY_ data reshape permitted is converting a 0-dimensional # array i.e. self.shape == () into a 1-dimensional array of length @@ -275,6 +275,8 @@ def data(self, data): data = ma.array(data.data, mask=data.mask, dtype=data.dtype) self._lazy_array = None self._real_array = data + if not has_shape: + self._shape = self.core_data().shape # Check the manager contract, as the managed data has changed. self._assert_axioms() @@ -292,11 +294,7 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - if self.core_data() is None: - result = self._shape - else: - result = self.core_data().shape - return result + return self._shape if self._shape else self.core_data().shape def is_dataless(self) -> bool: """Determine whether the cube has no data. diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 83ba548653..878809f3d7 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -649,7 +649,7 @@ def test_with_lazy_array(self): class Test_is_dataless(tests.IrisTest): def setUp(self): self.data = np.array(0) - self.shape = 0 + self.shape = (0, ) def test_with_data(self): dm = DataManager(self.data) From de348f96482b28a3a05846af1316932505b36fed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:24:13 +0000 Subject: [PATCH 41/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/tests/unit/data_manager/test_DataManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 878809f3d7..342f31637b 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -649,7 +649,7 @@ def test_with_lazy_array(self): class Test_is_dataless(tests.IrisTest): def setUp(self): self.data = np.array(0) - self.shape = (0, ) + self.shape = (0,) def test_with_data(self): dm = DataManager(self.data) From 7c74460d60799102a6c00d1ad3aceaf56cbc85a4 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Fri, 20 Dec 2024 19:57:46 +0000 Subject: [PATCH 42/48] fixed __eq__ and __repr__, and maybe dtype of dataless equal None --- lib/iris/_data_manager.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 26a4750062..4ce74ff23f 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -94,12 +94,14 @@ def __eq__(self, other): result = NotImplemented if isinstance(other, type(self)): - result = False - same_lazy = self.has_lazy_data() == other.has_lazy_data() - same_dtype = self.dtype == other.dtype - if same_lazy and same_dtype: - result = array_equal(self.core_data(), other.core_data()) - + if self.is_dataless() and other.is_dataless(): + result = self.shape == other.shape + else: + result = False + same_lazy = self.has_lazy_data() == other.has_lazy_data() + same_dtype = self.dtype == other.dtype + if same_lazy and same_dtype: + result = array_equal(self.core_data(), other.core_data()) return result def __ne__(self, other): @@ -131,6 +133,8 @@ def __repr__(self): """Return an string representation of the instance.""" fmt = "{cls}({data!r})" result = fmt.format(data=self.core_data(), cls=type(self).__name__) + if self.is_dataless(): + result = f"{result}, shape={self.shape}" return result @@ -284,7 +288,7 @@ def data(self, data): @property def dtype(self): """The dtype of the realised lazy data or the dtype of the real data.""" - return self.core_data().dtype + return self.core_data().dtype if not self.is_dataless() else None @property def ndim(self): @@ -294,7 +298,7 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - return self._shape if self._shape else self.core_data().shape + return self._shape def is_dataless(self) -> bool: """Determine whether the cube has no data. @@ -365,7 +369,9 @@ def lazy_data(self): This method will never realise any lazy data. """ - if self.has_lazy_data(): + if self.is_dataless(): + result = None + elif self.has_lazy_data(): result = self._lazy_array else: result = as_lazy_data(self._real_array) From 8bddec761eaa5b9df69390a1ffeb8b69767767df Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Mon, 23 Dec 2024 17:29:23 +0000 Subject: [PATCH 43/48] included self.core_data().shape when shape is none, for Coord cases --- lib/iris/_data_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 4ce74ff23f..7047da69b9 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -298,7 +298,7 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - return self._shape + return self._shape if self._shape else self.core_data().shape def is_dataless(self) -> bool: """Determine whether the cube has no data. From 2a6705013c359b0d9b99be3d98e2402cf3b10ee6 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 24 Dec 2024 19:21:23 +0000 Subject: [PATCH 44/48] made sure _shape is set in case of prexisting _shape of () --- lib/iris/_data_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index 7047da69b9..ecfafabe62 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -279,7 +279,9 @@ def data(self, data): data = ma.array(data.data, mask=data.mask, dtype=data.dtype) self._lazy_array = None self._real_array = data - if not has_shape: + # sets ``self._shape`` if it is None, or if it is being converted from + # ( ) to (1, ) + if not has_shape or (self._shape == () and data.shape == (1,)): self._shape = self.core_data().shape # Check the manager contract, as the managed data has changed. From f8c2daddf44424ae7683b2ca614200d40b21ddbc Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 24 Dec 2024 20:21:47 +0000 Subject: [PATCH 45/48] added eq and repr tests, and fixed shape of () not being valid --- lib/iris/_data_manager.py | 4 ++-- .../tests/unit/data_manager/test_DataManager.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index ecfafabe62..ea044cc180 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -148,7 +148,7 @@ def _assert_axioms(self): if is_lazy and is_real: msg = "Unexpected data state, got both lazy and real data." raise ValueError(msg) - elif not self._shape: + elif self._shape is None: msg = "Unexpected data state, got no lazy or real data, and no shape." raise ValueError(msg) @@ -300,7 +300,7 @@ def ndim(self): @property def shape(self): """The shape of the data being managed.""" - return self._shape if self._shape else self.core_data().shape + return self._shape def is_dataless(self) -> bool: """Determine whether the cube has no data. diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 342f31637b..f9ee73bc18 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -87,6 +87,16 @@ def test_lazy_with_lazy__dtype_failure(self): dm2 = DataManager(as_lazy_data(self.real_array).astype(int)) self.assertFalse(dm1 == dm2) + def test_none(self): + dm1 = DataManager(data=None, shape=(1, )) + dm2 = DataManager(data=None, shape=(1, )) + self.assertTrue(dm1 == dm2) + + def test_none_with_real(self): + dm1 = DataManager(data=None, shape=(1, )) + dm2 = DataManager(self.real_array) + self.assertFalse(dm1 == dm2) + def test_non_DataManager_failure(self): dm = DataManager(np.array(0)) self.assertFalse(dm == 0) @@ -158,6 +168,13 @@ def test_lazy(self): expected = "{}({!r})".format(self.name, self.lazy_array) self.assertEqual(result, expected) + def test_dataless(self): + print(self.real_array.shape) + dm = DataManager(None, self.real_array.shape) + result = repr(dm) + expected = "{}({!r}), shape={}".format(self.name, None, self.real_array.shape) + self.assertEqual(result, expected) + class Test__assert_axioms(tests.IrisTest): def setUp(self): From ed75485e857de426b86f116c92bf1643fa8c5bd7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 20:22:30 +0000 Subject: [PATCH 46/48] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/iris/tests/unit/data_manager/test_DataManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index f9ee73bc18..58004b120e 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -88,12 +88,12 @@ def test_lazy_with_lazy__dtype_failure(self): self.assertFalse(dm1 == dm2) def test_none(self): - dm1 = DataManager(data=None, shape=(1, )) - dm2 = DataManager(data=None, shape=(1, )) + dm1 = DataManager(data=None, shape=(1,)) + dm2 = DataManager(data=None, shape=(1,)) self.assertTrue(dm1 == dm2) def test_none_with_real(self): - dm1 = DataManager(data=None, shape=(1, )) + dm1 = DataManager(data=None, shape=(1,)) dm2 = DataManager(self.real_array) self.assertFalse(dm1 == dm2) From 922191779a1da9d5423971127d18f4314b3cf7ba Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 24 Dec 2024 20:30:32 +0000 Subject: [PATCH 47/48] fixed test now that self._shape is always set --- lib/iris/tests/unit/data_manager/test_DataManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 58004b120e..8e6df63158 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -184,6 +184,7 @@ def setUp(self): def test_array_none(self): self.dm._real_array = None + self.dm._shape = None emsg = "Unexpected data state, got no lazy or real data, and no shape." with self.assertRaisesRegex(ValueError, emsg): self.dm._assert_axioms() From f8c5d9512341f406ca831d170b778385dd625b14 Mon Sep 17 00:00:00 2001 From: Elias <110238618+ESadek-MO@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:33:52 +0000 Subject: [PATCH 48/48] move `self._shape = shape` to init block --- lib/iris/_data_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/iris/_data_manager.py b/lib/iris/_data_manager.py index ea044cc180..6a0297adcd 100644 --- a/lib/iris/_data_manager.py +++ b/lib/iris/_data_manager.py @@ -38,9 +38,8 @@ def __init__(self, data, shape=None): msg = '"shape" should only be provided if "data" is None' raise ValueError(msg) - self._shape = shape - # Initialise the instance. + self._shape = shape self._lazy_array = None self._real_array = None