From 4a69c08a71383578538e4fbb5f5a1ab04ccd295f Mon Sep 17 00:00:00 2001 From: Ed Campbell Date: Tue, 2 Oct 2012 19:23:12 +0100 Subject: [PATCH 1/2] Using tuples to contain dimensions in coord to dim mapping rather than lists to prevent inadvertent side effects from mutable type. Cube.coord_dims() now returns a tuple of ints. --- lib/iris/analysis/__init__.py | 2 +- lib/iris/cube.py | 42 ++++++++++--------- .../tests/results/COLPEX/uwind_and_orog.cml | 2 +- .../results/cube_io/pickling/cubelist.cml | 2 +- .../results/cube_io/pickling/single_cube.cml | 2 +- lib/iris/tests/results/derived/column.cml | 2 +- lib/iris/tests/results/derived/no_orog.cml | 2 +- .../tests/results/derived/removed_orog.cml | 2 +- .../tests/results/derived/removed_sigma.cml | 2 +- lib/iris/tests/results/derived/transposed.cml | 2 +- .../merge/theta_two_forecast_periods.cml | 2 +- .../netcdf/netcdf_save_load_hybrid_height.cml | 2 +- lib/iris/tests/results/stock/realistic_4d.cml | 2 +- 13 files changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 408be3ea1f..fcc59f3dc2 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -240,7 +240,7 @@ def coord_comparison(*cubes): # get all coordinate groups which don't describe a dimension # (None -> doesn't describe a dimension) - no_data_dim_fn = lambda cube, coord: cube.coord_dims(coord=coord) == [] + no_data_dim_fn = lambda cube, coord: cube.coord_dims(coord=coord) == () if coord_group.matches_all(no_data_dim_fn): no_data_dimension.add(coord_group) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index be625c3cdc..f58e75aaab 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -272,7 +272,7 @@ def __init__(self, data, standard_name=None, long_name=None, units=None, * dim_coords_and_dims A list of coordinates with scalar dimension mappings, e.g ``[(lat_coord, 0), (lon_coord, 1)]``. * aux_coords_and_dims - A list of coordinates with dimension mappings, e.g ``[(lat_coord, [0]), (lon_coord, [0,1])]``. + A list of coordinates with dimension mappings, e.g ``[(lat_coord, 0), (lon_coord, (0, 1))]``. See also :meth:`Cube.add_dim_coord()` and :meth:`Cube.add_aux_coord()`. * aux_factories A list of auxiliary coordinate factories. See :mod:`iris.aux_factory`. @@ -381,7 +381,7 @@ def add_aux_coord(self, coord, data_dims=None): Kwargs: * data_dims - Integer/list of integers giving the data dimensions spanned by the coordinate. + Integer or iterable of integers giving the data dimensions spanned by the coordinate. Raises a ValueError if a coordinate with identical metadata already exists on the cube. @@ -390,14 +390,13 @@ def add_aux_coord(self, coord, data_dims=None): """ + # Convert to a tuple of integers if data_dims is None: - data_dims = [] - - # Convert to list of integers - if isinstance(data_dims, collections.Container): - data_dims = [int(d) for d in data_dims] + data_dims = tuple() + elif isinstance(data_dims, collections.Container): + data_dims = tuple(int(d) for d in data_dims) else: - data_dims = [int(data_dims)] + data_dims = (int(data_dims),) if data_dims: if len(data_dims) != coord.ndim: @@ -528,10 +527,12 @@ def replace_coord(self, new_coord): def coord_dims(self, coord): """ - Returns the list of data dimensions relevant to the given coordinate. + Returns a tuple of the data dimensions relevant to the given + coordinate. - When searching for the given coordinate in the cube the comparison is made using coordinate metadata - equality. Hence the given coordinate instance need not exist on the cube, and may contain different + When searching for the given coordinate in the cube the comparison is + made using coordinate metadata equality. Hence the given coordinate + instance need not exist on the cube, and may contain different coordinate values. Args: @@ -545,7 +546,7 @@ def coord_dims(self, coord): ### Search by coord definition first # Search dim coords first - matches = [[dim] for coord_, dim in self._dim_coords_and_dims if coord_._as_defn() == target_defn] + matches = [(dim,) for coord_, dim in self._dim_coords_and_dims if coord_._as_defn() == target_defn] # Search aux coords if not matches: @@ -561,7 +562,7 @@ def coord_dims(self, coord): # XXX Where did this come from? And why isn't it reflected in the docstring? if not matches: - matches = [[dim] for coord_, dim in self._dim_coords_and_dims if coord_.name() == coord.name()] + matches = [(dim,) for coord_, dim in self._dim_coords_and_dims if coord_.name() == coord.name()] # Search aux coords if not matches: @@ -653,8 +654,8 @@ def coords(self, name=None, standard_name=None, long_name=None, attributes=None, * contains_dimension The desired coordinate contains the data dimension. If None, does not check for the dimension. * dimensions - The exact data dimensions of the desired coordinate. Coordinates with no data dimension can be found with an empty list (i.e. ``[]``). - If None, does not check for dimensions. + The exact data dimensions of the desired coordinate. Coordinates with no data dimension can be found with an + empty tuple or list (i.e. ``()`` or ``[]``). If None, does not check for dimensions. * coord Whether the desired coordinates have metadata equal to the given coordinate instance. If None, no check is done. Accepts either a :class:'iris.coords.DimCoord`, :class:`iris.coords.AuxCoord` or :class:`iris.coords.CoordDefn`. @@ -711,7 +712,7 @@ def coords(self, name=None, standard_name=None, long_name=None, attributes=None, if dimensions is not None: if not isinstance(dimensions, collections.Container): dimensions = [dimensions] - coords_and_factories = filter(lambda coord_: dimensions == self.coord_dims(coord_), coords_and_factories) + coords_and_factories = filter(lambda coord_: tuple(dimensions) == self.coord_dims(coord_), coords_and_factories) # If any factories remain after the above filters we have to make the coords so they can be returned def extract_coord(coord_or_factory): @@ -1441,9 +1442,10 @@ def remap_dim_coord(coord_and_dim): return coord, dim_mapping[dim] self._dim_coords_and_dims = map(remap_dim_coord, self._dim_coords_and_dims) - for coord, dims in self._aux_coords_and_dims: - for i, dim in enumerate(dims): - dims[i] = dim_mapping[dim] + def remap_aux_coord(coord_and_dims): + coord, dims = coord_and_dims + return coord, tuple(dim_mapping[dim] for dim in dims) + self._aux_coords_and_dims = map(remap_aux_coord, self._aux_coords_and_dims) def xml(self, checksum=False): """ @@ -1488,7 +1490,7 @@ def _xml_element(self, doc, checksum=False): cube_coord_xml_element = doc.createElement("coord") coords_xml_element.appendChild(cube_coord_xml_element) - dims = self.coord_dims(coord) + dims = list(self.coord_dims(coord)) if dims: cube_coord_xml_element.setAttribute("datadims", repr(dims)) diff --git a/lib/iris/tests/results/COLPEX/uwind_and_orog.cml b/lib/iris/tests/results/COLPEX/uwind_and_orog.cml index 2a73f6d3b0..bfcb52e5fc 100644 --- a/lib/iris/tests/results/COLPEX/uwind_and_orog.cml +++ b/lib/iris/tests/results/COLPEX/uwind_and_orog.cml @@ -6,7 +6,7 @@ - + - + - + - + - + - + - + - + - + - + - + = self.ndim: raise ValueError('The cube does not have the specified dimension (%d)' % data_dim) + # Check dimension is available + if self.coords(dimensions=data_dim, dim_coords=True): + raise ValueError('A dim_coord is already associated with dimension %d.' % data_dim) + # Check compatibility with the shape of the data if dim_coord.shape[0] != self.shape[data_dim]: msg = 'Unequal lengths. Cube dimension {} => {}; coord {!r} => {}.' diff --git a/lib/iris/tests/test_cdm.py b/lib/iris/tests/test_cdm.py index 17f73e6dac..0a2375e664 100644 --- a/lib/iris/tests/test_cdm.py +++ b/lib/iris/tests/test_cdm.py @@ -169,6 +169,39 @@ def test_remove_coord(self): self.cube.remove_coord('y') self.assertEqual(self.cube.coords(), []) + def test_immutable_dimcoord_dims(self): + # Add DimCoord to dimension 1 + dims = [1] + self.cube.add_dim_coord(self.x, dims) + self.assertEqual(self.cube.coord_dims(self.x), (1,)) + + # Change dims object + dims[0] = 0 + # Check the cube is unchanged + self.assertEqual(self.cube.coord_dims(self.x), (1,)) + + # Check coord_dims cannot be changed + dims = self.cube.coord_dims(self.x) + with self.assertRaises(TypeError): + dims[0] = 0 + + def test_immutable_auxcoord_dims(self): + # Add AuxCoord to dimensions (0, 1) + dims = [0, 1] + self.cube.add_aux_coord(self.xy, dims) + self.assertEqual(self.cube.coord_dims(self.xy), (0, 1)) + + # Change dims object + dims[0] = 1 + dims[1] = 0 + # Check the cube is unchanged + self.assertEqual(self.cube.coord_dims(self.xy), (0, 1)) + + # Check coord_dims cannot be changed + dims = self.cube.coord_dims(self.xy) + with self.assertRaises(TypeError): + dims[0] = 1 + class TestStockCubeStringRepresentations(tests.IrisTest): def setUp(self):