-
Notifications
You must be signed in to change notification settings - Fork 300
Ensure cubelists only contain cubes #3238
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
Changes from 7 commits
4ab7cfa
0b293ec
8a74df9
2622254
39d5f4d
4187162
569f35e
5b13dcc
8f272fb
f7134c1
bc7eb7a
d961a68
a84823e
7fe5d0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| # (C) British Crown Copyright 2014 - 2018, Met Office | ||
| # (C) British Crown Copyright 2014 - 2019, Met Office | ||
| # | ||
| # This file is part of Iris. | ||
| # | ||
|
|
@@ -24,6 +24,8 @@ | |
| import iris.tests as tests | ||
| import iris.tests.stock | ||
|
|
||
| import copy | ||
|
|
||
| from cf_units import Unit | ||
| import numpy as np | ||
|
|
||
|
|
@@ -34,6 +36,25 @@ | |
| from iris.fileformats.pp import STASH | ||
| from iris.tests import mock | ||
|
|
||
| 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): | ||
| self.cubelist.append(None) | ||
|
|
||
|
|
||
| class Test_concatenate_cube(tests.IrisTest): | ||
| def setUp(self): | ||
|
|
@@ -64,6 +85,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): | ||
| self.cubelist1.extend(range(3)) | ||
|
|
||
|
|
||
| class Test_extract_overlapping(tests.IrisTest): | ||
| def setUp(self): | ||
| shape = (6, 14, 19) | ||
|
|
@@ -118,6 +165,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 | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Travis output showing the above:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test passes if I point it at the
I'm officially confused. |
||
| 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): | ||
| 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): | ||
| self.cubelist.insert(0, None) | ||
|
|
||
|
|
||
| class Test_merge_cube(tests.IrisTest): | ||
| def setUp(self): | ||
| self.cube1 = Cube([1, 2, 3], "air_temperature", units="K") | ||
|
|
@@ -241,6 +330,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): | ||
| self.cubelist[0] = None | ||
| with self.assertWarnsRegexp(NOT_CUBE_MSG): | ||
| self.cubelist[0:2] = [self.cube3, None] | ||
rcomer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def test_fail(self): | ||
rcomer marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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)), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style question: Is this the right place for these checking functions, or should they be defined outside the class?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My take : these don't use instance properties, so they could be static methods.
... They don't use any class properties either, so they don't really need to be in the class at all.
In fact, they don't use private properties of Cube, so they don't really need to be in the module.
At that point (they are just functions), they could go somewhere else.
However, for personal preference, I'd remove them from the class but keep them as private methods in cube.py, just in case they might need to use 'private' cube concepts in future.
Also ... the use of
isinstanceprevents any duck typing (lookalike objects can't masquerade as Cubes), which is arguably un-Pythonic.We have previously used
hasattr('add_aux_coord')for this elsewhereThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so if we check whether the object quacks with an auxcoord instead of checking the type, is there any reason not to revert to raising an exception rather than a warning?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh dear, I had somehow skimmed over that latest discussion without taking it in.
Now I see that @pelson and I are just advocating different approaches, and I honestly don't know how to choose between.
Personally though, I must say I do hate all the warnings in Iris. There are still far too many, most occurrences are a pointless nuisance, and on the rare occasions when they aren't no-one is listening any more.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem: I'm partly using this as a learning exercise, so exploring different solutions to the same issue is fine 👍
I think I prefer to have an exception on the grounds that, if my code is going to fail, it's better for it to fail sooner rather than later. Also, having the failure at the point that the object is included into the cubelist means that the traceback is going to point me a lot closer to where I made the mistake. Which so far has always been
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nicely done! 😆
As a user, I'd still rather have an exception if possible, for the reasons I gave above.
If something does go wrong with my cubelist, the first thing I'm going to do in an attempt to debug is print it. So if we're looking for a minimal set of cube-like attributes,
summaryought to be up there.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we thinking about this the wrong way round? Rather than trying to define which types should be allowed in a cubelist, it might be easier to focus on which types should definitely be rejected.
This started because a rogue
Nonein a cubelist caused problems, and I wanted a more informative error message. So far I’m not aware of any other types that have caused issues. So my case would be solved by simply throwing an exceptionif object is None. We could generalise that a bit if we decide that, at minimum, the cubelist should be printable, so reject any objects that don’t have have asummaryattribute.The exception message needn’t say anything about how similar to a
Cubethe object is, but could just say ”object of type [whichever] does not belong in a cubelist”.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so in this case. Because users can create and use their own CubeList instances, the minimum set of behaviour required for an entry into a CubeList is precisely the Cube's behaviour (and no less).
Useful to know, thank you. So my biggest concern is that we are essentially introducing a breaking change if we do this as an exception - if @rcomer has been adding
Noneinto a cube list by accident, just think of all the wild things that some of our less educated users have been doing! 😭I guess there is a workaround though... if users really want to do this they can still do
list.append(cube_list_instance, thing_that_isnt_a_cube)until they sort their 💩 out.In an attempt to get consensus and prevent this conversation from being open-ended, my refined suggestion:
CubeList._assert_is_iterable_of_cubes-> just construct a CubeList of the subset - that way you can honour iterators, and then add the constructed CubeList as necessary.CubeList.__new__to use_assert_is_cube.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't considered the possibility of cubelists being used to store random types 😮 . Semi-serious question: how far away is Iris 3?
Just to check I've understood: we make it strict so only
Cubeinstances are allowed. Because the check is restricted to one method, someone who wants to include ducks in the cubelist just needs to replace that one method?Points 1 and 2 sound good to me.
Point 3: I think I need to wait for #3264 to be merged, and then update
__init__!Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it helps, but..
In my mind, even if it was previously possible to put non-cubes into a CubeList, that was never intended behaviour -- evidence the code covered by #3264.
So that is a bug, and fixing a bug is not a "breaking" change.
Weaselly, but we've accepted that principle before.