Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cubelist contain only cubes -- resurrected #4767

Merged
merged 17 commits into from
May 27, 2022
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* The `append`, `extend` and `insert` methods of :class:`iris.cube.CubeList`
now perform a check to ensure that only :class:`iris.cube.Cube` instances are
added.
84 changes: 73 additions & 11 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,26 @@ def merged(self, unique=False):
[pair.merged(unique) for pair in self.pairs]
)

def _check_iscube(obj):
"""
Raise a warning if obj does not look like a cube.
"""
if not hasattr(obj, 'add_aux_coord'):
msg = ("Cubelist now contains object of type '{}'. This may "
"adversely affect subsequent operations.")
warnings.warn(msg.format(type(obj).__name__))

def _check_cube_sequence(sequence):
"""
Raise one or more warnings if sequence contains elements that are not
Cubes (or cube-like). Skip this if the sequence is a CubeList, as we can
assume it was already checked.
"""
if (isinstance(sequence, collections.Iterable) and
not isinstance(sequence, Cube) and
not isinstance(sequence, CubeList)):
for obj in sequence:
_check_iscube(obj)

class CubeList(list):
"""
Expand All @@ -156,10 +176,7 @@ def __new__(cls, list_of_cubes=None):
"""Given a :class:`list` of cubes, return a CubeList instance."""
cube_list = list.__new__(cls, list_of_cubes)

# Check that all items in the incoming list are cubes. Note that this
# checking does not guarantee that a CubeList instance *always* has
# just cubes in its list as the append & __getitem__ methods have not
# been overridden.
# Check that all items in the incoming list are cubes.
if not all([isinstance(cube, Cube) for cube in cube_list]):
raise ValueError(
"All items in list_of_cubes must be Cube " "instances."
Expand All @@ -180,15 +197,10 @@ def __str__(self):

def __repr__(self):
"""Runs repr on every cube."""
return "[%s]" % ",\n".join([repr(cube) for cube in self])

def _repr_html_(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask why the HTML representation for CubeList was removed please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I think was a plain mistake.
On closer inspection, it looks like we test the usage of CubeListRepresentation(cubelist).repr_html, but not actually "cubelist.repr_html()".

I think this needs fixing : #4973

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @pp-mo!

from iris.experimental.representation import CubeListRepresentation

representer = CubeListRepresentation(self)
return representer.repr_html()
return '[%s]' % ',\n'.join([repr(cube) for cube in self])

# TODO #370 Which operators need overloads?

def __add__(self, other):
return CubeList(list.__add__(self, other))

Expand All @@ -210,6 +222,56 @@ def __getslice__(self, start, stop):
result = CubeList(result)
return result

def __iadd__(self, other_cubes):
"""
Add a sequence of cubes to the cubelist in place.
"""
_check_cube_sequence(other_cubes)
super(CubeList, self).__iadd__(other_cubes)

def __setitem__(self, key, cube_or_sequence):
"""Set self[key] to cube or sequence of cubes"""
if isinstance(key, int):
# should have single cube.
_check_iscube(cube_or_sequence)
else:
# key is a slice (or exception will come from list method).
_check_cube_sequence(cube_or_sequence)

super(CubeList, self).__setitem__(key, cube_or_sequence)

# __setslice__ is only required for python2.7 compatibility.
def __setslice__(self, *args):
cubes = args[-1]
_check_cube_sequence(cubes)
super(CubeList, self).__setslice__(*args)

def append(self, cube):
"""
Append a cube.
"""
_check_iscube(cube)
super(CubeList, self).append(cube)

def extend(self, other_cubes):
"""
Extend cubelist by appending the cubes contained in other_cubes.

Args:

* other_cubes:
A cubelist or other sequence of cubes.
"""
_check_cube_sequence(other_cubes)
super(CubeList, self).extend(other_cubes)

def insert(self, index, cube):
"""
Insert a cube before index.
"""
_check_iscube(cube)
super(CubeList, self).insert(index, cube)

def xml(self, checksum=False, order=True, byteorder=True):
"""Return a string of the XML that this list of cubes represents."""

Expand Down
118 changes: 118 additions & 0 deletions lib/iris/tests/unit/cube/test_CubeList.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import collections
from unittest import mock

import copy

from cf_units import Unit
import numpy as np

Expand All @@ -23,6 +25,25 @@
from iris.fileformats.pp import STASH
import iris.tests.stock

NOT_CUBE_MSG = "Cubelist now contains object of type '{}'"


class Test_append(tests.IrisTest):
def setUp(self):
self.cubelist = iris.cube.CubeList()
self.cube1 = iris.cube.Cube(1, long_name='foo')
self.cube2 = iris.cube.Cube(1, long_name='bar')

def test_pass(self):
self.cubelist.append(self.cube1)
self.assertEqual(self.cubelist[-1], self.cube1)
self.cubelist.append(self.cube2)
self.assertEqual(self.cubelist[-1], self.cube2)

def test_warn(self):
with self.assertWarnsRegexp(NOT_CUBE_MSG.format('NoneType')):
self.cubelist.append(None)


class Test_concatenate_cube(tests.IrisTest):
def setUp(self):
Expand Down Expand Up @@ -70,6 +91,32 @@ def test_empty(self):
CubeList([]).concatenate_cube()


class Test_extend(tests.IrisTest):
def setUp(self):
self.cube1 = iris.cube.Cube(1, long_name='foo')
self.cube2 = iris.cube.Cube(1, long_name='bar')
self.cubelist1 = iris.cube.CubeList([self.cube1])
self.cubelist2 = iris.cube.CubeList([self.cube2])

def test_pass(self):
cubelist = copy.copy(self.cubelist1)
cubelist.extend(self.cubelist2)
self.assertEqual(cubelist, self.cubelist1 + self.cubelist2)
cubelist.extend([self.cube2])
self.assertEqual(cubelist[-1], self.cube2)

def test_fail(self):
with self.assertRaisesRegexp(TypeError, 'Cube is not iterable'):
self.cubelist1.extend(self.cube1)
msg = "'NoneType' object is not iterable"
with self.assertRaisesRegexp(TypeError, msg):
self.cubelist1.extend(None)

def test_warn(self):
with self.assertWarnsRegexp(NOT_CUBE_MSG.format('int')):
self.cubelist1.extend(range(3))


class Test_extract_overlapping(tests.IrisTest):
def setUp(self):
shape = (6, 14, 19)
Expand Down Expand Up @@ -130,6 +177,48 @@ def test_different_orders(self):
self.assertEqual(b.coord("time"), self.cube.coord("time")[2:4])


class Test_iadd(tests.IrisTest):
def setUp(self):
self.cube1 = iris.cube.Cube(1, long_name='foo')
self.cube2 = iris.cube.Cube(1, long_name='bar')
self.cubelist1 = iris.cube.CubeList([self.cube1])
self.cubelist2 = iris.cube.CubeList([self.cube2])

def test_pass(self):
cubelist = copy.copy(self.cubelist1)
cubelist += self.cubelist2
self.assertEqual(cubelist, self.cubelist1 + self.cubelist2)
cubelist += [self.cube2]
self.assertEqual(cubelist[-1], self.cube2)

def test_fail(self):
msg = 'Cube is not iterable'
with self.assertRaisesRegexp(TypeError, msg):
self.cubelist1 += self.cube1
msg = "'float' object is not iterable"
with self.assertRaisesRegexp(TypeError, msg):
self.cubelist1 += 1.

def test_warn(self):
with self.assertWarnsRegexp(NOT_CUBE_MSG.format('int')):
self.cubelist1 += range(3)


class Test_insert(tests.IrisTest):
def setUp(self):
self.cube1 = iris.cube.Cube(1, long_name='foo')
self.cube2 = iris.cube.Cube(1, long_name='bar')
self.cubelist = iris.cube.CubeList([self.cube1] * 3)

def test_pass(self):
self.cubelist.insert(1, self.cube2)
self.assertEqual(self.cubelist[1], self.cube2)

def test_warn(self):
with self.assertWarnsRegexp(NOT_CUBE_MSG.format('NoneType')):
self.cubelist.insert(0, None)


class Test_merge_cube(tests.IrisTest):
def setUp(self):
self.cube1 = Cube([1, 2, 3], "air_temperature", units="K")
Expand Down Expand Up @@ -274,6 +363,35 @@ def test_combination_with_extra_triple(self):
self.assertCML(cube, checksum=False)


class Test_setitem(tests.IrisTest):
def setUp(self):
self.cube1 = iris.cube.Cube(1, long_name='foo')
self.cube2 = iris.cube.Cube(1, long_name='bar')
self.cube3 = iris.cube.Cube(1, long_name='boo')
self.cubelist = iris.cube.CubeList([self.cube1] * 3)

def test_pass(self):
self.cubelist[1] = self.cube2
self.assertEqual(self.cubelist[1], self.cube2)
self.cubelist[:2] = (self.cube2, self.cube3)
self.assertEqual(
self.cubelist,
iris.cube.CubeList([self.cube2, self.cube3, self.cube1]))

def test_warn(self):
with self.assertWarnsRegexp(NOT_CUBE_MSG.format('NoneType')):
self.cubelist[0] = None
with self.assertWarnsRegexp(NOT_CUBE_MSG.format('NoneType')):
self.cubelist[0:2] = [self.cube3, None]

def test_fail(self):
msg = "can only assign an iterable"
with self.assertRaisesRegexp(TypeError, msg):
self.cubelist[:1] = 2.5
with self.assertRaisesRegexp(TypeError, msg):
self.cubelist[:1] = self.cube1


class Test_xml(tests.IrisTest):
def setUp(self):
self.cubes = CubeList([Cube(np.arange(3)), Cube(np.arange(3))])
Expand Down