From ad34259427dc7275c2fdb7ab5ad33cd5e2f4b4e9 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 14 Oct 2019 16:19:57 +0100 Subject: [PATCH 01/14] Add NameConstraint with relaxed name loading --- .../iris/src/userguide/loading_iris_cubes.rst | 26 ++- lib/iris/__init__.py | 2 + lib/iris/_constraints.py | 62 +++++- lib/iris/_cube_coord_common.py | 14 ++ lib/iris/tests/test_constraints.py | 209 ++++++++++++++++-- .../cube_coord_common/test_CFVariableMixin.py | 33 +++ 6 files changed, 317 insertions(+), 29 deletions(-) diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index 2cb3b9b259..bf50acc614 100644 --- a/docs/iris/src/userguide/loading_iris_cubes.rst +++ b/docs/iris/src/userguide/loading_iris_cubes.rst @@ -166,18 +166,36 @@ As we have seen, loading the following file creates several Cubes:: cubes = iris.load(filename) Specifying a name as a constraint argument to :py:func:`iris.load` will mean -only cubes with a matching :meth:`name ` +only cubes with matching :meth:`name ` will be returned:: filename = iris.sample_data_path('uk_hires.pp') - cubes = iris.load(filename, 'specific_humidity') + cubes = iris.load(filename, 'surface_altitude') -To constrain the load to multiple distinct constraints, a list of constraints +Note that, the provided name will match against either the standard name, +long name, NetCDF variable name or STASH metadata of a cube. Therefore, the +previous example using the ``surface_altitude`` standard name constraint can +also be achieved using the STASH value of ``m01s00i033``:: + + filename = iris.sample_data_path('uk_hires.pp') + cubes = iris.load(filename, 'm01s00i033') + +If further specific name constraint control is required i.e., to constrain +against a combination of standard name, long name, NetCDF variable name and/or +STASH metadata, consider using the :class:`iris.NameConstraint`. For example, +to constrain against both a standard name of ``surface_altitude`` **and** a STASH +of ``m01s00i033``:: + + filename = iris.sample_data_path('uk_hires.pp') + constraint = iris.NameConstraint(standard_name='surface_altitude', STASH='m01s00i033') + cubes = iris.load(filename, constraint) + +To constrain the load to multiple distinct constraints, a list of constraints can be provided. This is equivalent to running load once for each constraint but is likely to be more efficient:: filename = iris.sample_data_path('uk_hires.pp') - cubes = iris.load(filename, ['air_potential_temperature', 'specific_humidity']) + cubes = iris.load(filename, ['air_potential_temperature', 'surface_altitude']) The :class:`iris.Constraint` class can be used to restrict coordinate values on load. For example, to constrain the load to match diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 1a9baa3066..969fe54e9b 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -117,6 +117,7 @@ def callback(cube, field, filename): "save", "Constraint", "AttributeConstraint", + "NameConstraint" "sample_data_path", "site_configuration", "Future", @@ -127,6 +128,7 @@ def callback(cube, field, filename): Constraint = iris._constraints.Constraint AttributeConstraint = iris._constraints.AttributeConstraint +NameConstraint = iris._constraints.NameConstraint class Future(threading.local): diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index b0e97cdcd9..dd027db3b4 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -128,7 +128,7 @@ def _coordless_match(self, cube): """ match = True if self._name: - match = self._name == cube.name() + match = self._name in cube.names if match and self._cube_func: match = self._cube_func(cube) return match @@ -490,3 +490,63 @@ def _cube_func(self, cube): def __repr__(self): return "AttributeConstraint(%r)" % self._attributes + + +class NameConstraint(Constraint): + """ + Provides a simple Cube name based :class:`Constraint`, which matches + against each of the names provided, which may be either standard name, + long name, NetCDF variable name and/or the STASH from the attributes + dictionary. + + The name constraint will only succeed if all of the provided names match. + + """ + def __init__(self, **names): + """ + Example usage:: + + iris.NameConstraint(long_name='air temp') + + iris.NameConstraint(standard_name='air_temperature', + STASH=lambda stash: stash.item == 203 + + .. note:: Name constraint names are case sensitive. + + """ + self._names = names + super(NameConstraint, self).__init__(cube_func=self._cube_func) + + def _cube_func(self, cube): + def matcher(target, value): + if callable(value): + result = value(target) + else: + result = value == target + return result + + match = True + for name, value in self._names.items(): + if name == 'standard_name': + match = matcher(cube.standard_name, value) + if match is False: + break + if name == 'long_name': + match = matcher(cube.long_name, value) + if match is False: + break + if name == 'var_name': + match = matcher(cube.var_name, value) + if match is False: + break + if name == 'STASH': + match = matcher(cube.attributes.get('STASH'), value) + if match is False: + break + return match + + def __repr__(self): + names = [] + for name, value in self._names.items(): + names.append('{}={!r}'.format(name, value)) + return '{}({})'.format(self.__class__.__name__, ', '.join(names)) diff --git a/lib/iris/_cube_coord_common.py b/lib/iris/_cube_coord_common.py index c1060f2001..45dc612407 100644 --- a/lib/iris/_cube_coord_common.py +++ b/lib/iris/_cube_coord_common.py @@ -181,6 +181,20 @@ def _check(item): return result + @property + def names(self): + """ + A tuple containing all of the metadata names. This includes the + standard name, long name, NetCDF variable name, and attributes + STASH name. + + """ + standard_name = self.standard_name + long_name = self.long_name + var_name = self.var_name + stash_name = str(self.attributes.get('STASH', '')) + return (standard_name, long_name, var_name, stash_name) + def rename(self, name): """ Changes the human-readable name. diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index 57cb33fafc..ec4880d0c7 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -14,6 +14,7 @@ import datetime import iris +from iris import AttributeConstraint, NameConstraint import iris.tests.stock as stock @@ -295,6 +296,173 @@ def load_match(self, files, constraints): return cubes +@tests.skip_data +class TestCubeExtract__names(TestMixin, tests.IrisTest): + def setUp(self): + TestMixin.setUp(self) + self.cube = iris.load_cube(self.theta_path) + # Expected names... + self.standard_name = 'air_potential_temperature' + self.long_name = 'air potential temperature' + self.var_name = 'apt' + self.stash = 'm01s00i004' + # Configure missing names... + self.cube.long_name = self.long_name + self.cube.var_name = self.var_name + + def test_standard_name(self): + constraint = iris.Constraint(self.standard_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + + def test_long_name(self): + constraint = iris.Constraint(self.long_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + self.assertEqual(result.long_name, self.long_name) + + def test_var_name(self): + constraint = iris.Constraint(self.var_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + self.assertEqual(result.var_name, self.var_name) + + def test_stash(self): + constraint = iris.Constraint(self.stash) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + self.assertEqual(str(result.attributes['STASH']), self.stash) + + +@tests.skip_data +class TestCubeExtract__name_constraint(TestMixin, tests.IrisTest): + def setUp(self): + TestMixin.setUp(self) + self.cube = iris.load_cube(self.theta_path) + # Expected names... + self.standard_name = 'air_potential_temperature' + self.long_name = 'air potential temperature' + self.var_name = 'apt' + self.stash = 'm01s00i004' + # Configure missing names... + self.cube.long_name = self.long_name + self.cube.var_name = self.var_name + + def test_standard_name(self): + # No match. + constraint = NameConstraint(standard_name='wibble') + result = self.cube.extract(constraint) + self.assertIsNone(result) + + # Match. + constraint = NameConstraint(standard_name=self.standard_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # Match - callable. + kwargs = dict(standard_name=lambda item: item.startswith('air_pot')) + constraint = NameConstraint(**kwargs) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + def test_long_name(self): + # No match. + constraint = NameConstraint(long_name='wibble') + result = self.cube.extract(constraint) + self.assertIsNone(result) + + # Match. + constraint = NameConstraint(long_name=self.long_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # Match - callable. + kwargs = dict(long_name=lambda item: item.startswith('air pot')) + constraint = NameConstraint(**kwargs) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + def test_var_name(self): + # No match. + constraint = NameConstraint(var_name='wibble') + result = self.cube.extract(constraint) + self.assertIsNone(result) + + # Match. + constraint = NameConstraint(var_name=self.var_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # Match - callable. + kwargs = dict(var_name=lambda item: item.startswith('ap')) + constraint = NameConstraint(**kwargs) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + def test_stash(self): + # No match. + constraint = NameConstraint(STASH='m01s00i444') + result = self.cube.extract(constraint) + self.assertIsNone(result) + + # Match. + constraint = NameConstraint(STASH=self.stash) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # Match - callable. + kwargs = dict(STASH=lambda stash: stash.item==4) + constraint = NameConstraint(**kwargs) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + def test_compound(self): + # Match. + constraint = NameConstraint(standard_name=self.standard_name, + long_name=self.long_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # No match - var_name. + constraint = NameConstraint(standard_name=self.standard_name, + long_name=self.long_name, + var_name='wibble') + result = self.cube.extract(constraint) + self.assertIsNone(result) + + # Match. + constraint = NameConstraint(standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # No match - STASH. + constraint = NameConstraint(standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH='m01s00i444') + result = self.cube.extract(constraint) + self.assertIsNone(result) + + # Match. + constraint = NameConstraint(standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH=self.stash) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + + # No match - standard_name. + constraint = NameConstraint(standard_name='wibble', + long_name=self.long_name, + var_name=self.var_name, + STASH=self.stash) + result = self.cube.extract(constraint) + self.assertIsNone(result) + + @tests.skip_data class TestCubeExtract(TestMixin, tests.IrisTest): def setUp(self): @@ -302,43 +470,36 @@ def setUp(self): self.cube = iris.load_cube(self.theta_path) def test_attribute_constraint(self): - # there is no my_attribute attribute on the cube, so ensure it returns None - cube = self.cube.extract( - iris.AttributeConstraint(my_attribute="foobar") - ) + # There is no my_attribute on the cube, so ensure it returns None. + constraint = AttributeConstraint(my_attribute="foobar") + cube = self.cube.extract(constraint) self.assertIsNone(cube) orig_cube = self.cube # add an attribute to the cubes orig_cube.attributes["my_attribute"] = "foobar" - cube = orig_cube.extract( - iris.AttributeConstraint(my_attribute="foobar") - ) + constraint = AttributeConstraint(my_attribute="foobar") + cube = orig_cube.extract(constraint) self.assertCML(cube, ("constrained_load", "attribute_constraint.cml")) - cube = orig_cube.extract( - iris.AttributeConstraint(my_attribute="not me") - ) + constraint = AttributeConstraint(my_attribute="not me") + cube = orig_cube.extract(constraint) self.assertIsNone(cube) - cube = orig_cube.extract( - iris.AttributeConstraint( - my_attribute=lambda val: val.startswith("foo") - ) - ) + kwargs = dict(my_attribute=lambda val: val.startswith("foo")) + constraint = AttributeConstraint(**kwargs) + cube = orig_cube.extract(constraint) self.assertCML(cube, ("constrained_load", "attribute_constraint.cml")) - cube = orig_cube.extract( - iris.AttributeConstraint( - my_attribute=lambda val: not val.startswith("foo") - ) - ) + kwargs = dict(my_attribute=lambda val: not val.startswith("foo")) + constraint = AttributeConstraint(**kwargs) + cube = orig_cube.extract(constraint) self.assertIsNone(cube) - cube = orig_cube.extract( - iris.AttributeConstraint(my_non_existant_attribute="hello world") - ) + kwargs = dict(my_non_existant_attribute="hello world") + constraint = AttributeConstraint(**kwargs) + cube = orig_cube.extract(constraint) self.assertIsNone(cube) def test_standard_name(self): @@ -360,7 +521,7 @@ def test_empty_data(self): cube = self.cube.extract(self.level_10).extract(self.level_10) self.assertTrue(cube.has_lazy_data()) - def test_non_existant_coordinate(self): + def test_non_existent_coordinate(self): # Check the behaviour when a constraint is given for a coordinate which does not exist/span a dimension self.assertEqual(self.cube[0, :, :].extract(self.level_10), None) diff --git a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py index 8cebdc73b8..16dbc87215 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py @@ -131,6 +131,39 @@ def test_fail_token_default(self): self.cf_var.name(default="_nope", token=True) +class Test_names(tests.IrisTest): + def setUp(self): + self.cf_var = CFVariableMixin() + self.cf_var.standard_name = None + self.cf_var.long_name = None + self.cf_var.var_name = None + self.cf_var.attributes = dict() + + def test_standard_name(self): + standard_name = 'air_temperature' + self.cf_var.standard_name = standard_name + expected = (standard_name, None, None, '') + self.assertEqual(expected, self.cf_var.names) + + def test_long_name(self): + long_name = 'air temperature' + self.cf_var.long_name = long_name + expected = (None, long_name, None, '') + self.assertEqual(expected, self.cf_var.names) + + def test_var_name(self): + var_name = 'atemp' + self.cf_var.var_name = var_name + expected = (None, None, var_name, '') + self.assertEqual(expected, self.cf_var.names) + + def test_STASH(self): + stash = 'm01s16i203' + self.cf_var.attributes = dict(STASH=stash) + expected = (None, None, None, stash) + self.assertEqual(expected, self.cf_var.names) + + class Test_standard_name__setter(tests.IrisTest): def test_valid_standard_name(self): cf_var = CFVariableMixin() From 58f12f6e8112bf5425e0136624fd8645794baf59 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 14 Oct 2019 19:27:47 +0100 Subject: [PATCH 02/14] appease the god of stickler-ci --- lib/iris/tests/test_constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index ec4880d0c7..3888e47fe9 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -412,7 +412,7 @@ def test_stash(self): self.assertIsNotNone(result) # Match - callable. - kwargs = dict(STASH=lambda stash: stash.item==4) + kwargs = dict(STASH=lambda stash: stash.item == 4) constraint = NameConstraint(**kwargs) result = self.cube.extract(constraint) self.assertIsNotNone(result) From 9d24096a61bcd1b6e42168edd388988d2021d58f Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Oct 2019 10:40:16 +0100 Subject: [PATCH 03/14] add whatsnew newfeature entries --- .../newfeature_2019-Oct-15_nameconstraint.txt | 1 + .../newfeature_2019-Oct-15_names_property.txt | 1 + .../newfeature_2019-Oct-15_relaxed_name_loading.txt | 1 + lib/iris/_constraints.py | 5 ++++- 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt new file mode 100644 index 0000000000..eeb40990e2 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt @@ -0,0 +1 @@ +* The :class:`~iris.NameConstraint` provides richer name constraint matching when loading or extracting against cubes, by supporting a constraint against any combination of ``standard_name``, ``long_name``, NetCDF ``var_name`` and ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt new file mode 100644 index 0000000000..a092631152 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt @@ -0,0 +1 @@ +* Cubes and coordinates now have a new ``names`` property that contains a tuple of the ``standard_name``, ``long_name``, NetCDF ``var_name``, and ``STASH`` attributes metadata. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt new file mode 100644 index 0000000000..6773ac28b1 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt @@ -0,0 +1 @@ +* Name constraint matching against cubes during loading or extracting has been relaxed from strictly matching against the :meth:`~iris.cube.Cube.name`, to matching against either the ``standard_name``, ``long_name``, NetCDF ``var_name``, or ``STASH`` attributes metadata of a cube. diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index dd027db3b4..c317a65001 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -511,7 +511,10 @@ def __init__(self, **names): iris.NameConstraint(standard_name='air_temperature', STASH=lambda stash: stash.item == 203 - .. note:: Name constraint names are case sensitive. + .. note:: + + Name constraint names are case sensitive i.e., use the + ``STASH`` keyword argument and not ``stash``. """ self._names = names From a25e8e4d016c74ed6019c317a5df774783b7933f Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Oct 2019 11:13:35 +0100 Subject: [PATCH 04/14] review actions --- lib/iris/_constraints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index c317a65001..e853652e69 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -466,7 +466,7 @@ def __init__(self, **attributes): """ self._attributes = attributes - Constraint.__init__(self, cube_func=self._cube_func) + super().__init__(cube_func=self._cube_func) def _cube_func(self, cube): match = True @@ -518,7 +518,7 @@ def __init__(self, **names): """ self._names = names - super(NameConstraint, self).__init__(cube_func=self._cube_func) + super().__init__(cube_func=self._cube_func) def _cube_func(self, cube): def matcher(target, value): From 3a4f9c23fa9806f38718bcda97026097d75a0150 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Oct 2019 12:54:10 +0100 Subject: [PATCH 05/14] review actions --- lib/iris/_constraints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index e853652e69..63d9dc0768 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -35,7 +35,8 @@ def __init__(self, name=None, cube_func=None, coord_values=None, **kwargs): Args: * name: string or None - If a string, it is used as the name to match against Cube.name(). + If a string, it is used as the name to match against the + `~iris.cube.Cube.names` property. * cube_func: callable or None If a callable, it must accept a Cube as its first and only argument and return either True or False. From d73621aa46ef6e3befdcd5d4448b6276425e0746 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 15 Oct 2019 14:16:01 +0100 Subject: [PATCH 06/14] review actions --- lib/iris/_constraints.py | 4 +++- lib/iris/tests/test_constraints.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index 63d9dc0768..27bcf4c760 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -13,6 +13,7 @@ import numpy as np +from iris._cube_coord_common import CFVariableMixin import iris.coords import iris.exceptions @@ -129,7 +130,8 @@ def _coordless_match(self, cube): """ match = True if self._name: - match = self._name in cube.names + match = (self._name in cube.names or + self._name == CFVariableMixin._DEFAULT_NAME) if match and self._cube_func: match = self._cube_func(cube) return match diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index 3888e47fe9..53340d8749 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -334,6 +334,17 @@ def test_stash(self): self.assertIsNotNone(result) self.assertEqual(str(result.attributes['STASH']), self.stash) + def test_unknown(self): + # Clear the cube metadata. + self.cube.standard_name = None + self.cube.long_name = None + self.cube.var_name = None + self.cube.attributes = None + # Extract the unknown cube. + constraint = iris.Constraint('unknown') + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + @tests.skip_data class TestCubeExtract__name_constraint(TestMixin, tests.IrisTest): From 6d1b42b96e2b493ac1afed663048c73c5cc31a5a Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 17 Oct 2019 01:50:25 +0100 Subject: [PATCH 07/14] make NameConstraint convenient --- lib/iris/_constraints.py | 44 ++-- lib/iris/_cube_coord_common.py | 4 +- lib/iris/tests/test_constraints.py | 9 + lib/iris/tests/unit/constraints/__init__.py | 20 ++ .../unit/constraints/test_NameConstraint.py | 208 ++++++++++++++++++ .../cube_coord_common/test_CFVariableMixin.py | 6 +- 6 files changed, 266 insertions(+), 25 deletions(-) create mode 100644 lib/iris/tests/unit/constraints/__init__.py create mode 100644 lib/iris/tests/unit/constraints/test_NameConstraint.py diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index 27bcf4c760..7ad8722c81 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -505,7 +505,8 @@ class NameConstraint(Constraint): The name constraint will only succeed if all of the provided names match. """ - def __init__(self, **names): + def __init__(self, standard_name='none', long_name='none', + var_name='none', STASH='none'): """ Example usage:: @@ -520,7 +521,11 @@ def __init__(self, **names): ``STASH`` keyword argument and not ``stash``. """ - self._names = names + self.standard_name = standard_name + self.long_name = long_name + self.var_name = var_name + self.STASH = STASH + self._names = ('standard_name', 'long_name', 'var_name', 'STASH') super().__init__(cube_func=self._cube_func) def _cube_func(self, cube): @@ -532,27 +537,24 @@ def matcher(target, value): return result match = True - for name, value in self._names.items(): - if name == 'standard_name': - match = matcher(cube.standard_name, value) - if match is False: - break - if name == 'long_name': - match = matcher(cube.long_name, value) - if match is False: - break - if name == 'var_name': - match = matcher(cube.var_name, value) - if match is False: - break - if name == 'STASH': - match = matcher(cube.attributes.get('STASH'), value) - if match is False: - break + for name in self._names: + if name == 'STASH' and self.STASH != 'none': + match = matcher(cube.attributes.get('STASH'), self.STASH) + else: + expected = getattr(self, name) + if expected != 'none': + actual = getattr(cube, name) + match = matcher(actual, expected) + # This is a short-cut match. + if match is False: + break + return match def __repr__(self): names = [] - for name, value in self._names.items(): - names.append('{}={!r}'.format(name, value)) + for name in self._names: + value = getattr(self, name) + if value != 'none': + names.append('{}={!r}'.format(name, value)) return '{}({})'.format(self.__class__.__name__, ', '.join(names)) diff --git a/lib/iris/_cube_coord_common.py b/lib/iris/_cube_coord_common.py index 45dc612407..4cd744af19 100644 --- a/lib/iris/_cube_coord_common.py +++ b/lib/iris/_cube_coord_common.py @@ -192,7 +192,9 @@ def names(self): standard_name = self.standard_name long_name = self.long_name var_name = self.var_name - stash_name = str(self.attributes.get('STASH', '')) + stash_name = self.attributes.get('STASH') + if stash_name is not None: + stash_name = str(stash_name) return (standard_name, long_name, var_name, stash_name) def rename(self, name): diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index 53340d8749..b96a585878 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -473,6 +473,15 @@ def test_compound(self): result = self.cube.extract(constraint) self.assertIsNone(result) + def test_unknown(self): + self.cube.standard_name = None + self.cube.long_name = None + self.cube.var_name = None + self.cube.attributes = None + constraint = NameConstraint(None, None, None, None) + result = self.cube.extract(constraint) + self.assertIsNotNone(result) + @tests.skip_data class TestCubeExtract(TestMixin, tests.IrisTest): diff --git a/lib/iris/tests/unit/constraints/__init__.py b/lib/iris/tests/unit/constraints/__init__.py new file mode 100644 index 0000000000..146a4e5a8e --- /dev/null +++ b/lib/iris/tests/unit/constraints/__init__.py @@ -0,0 +1,20 @@ +# (C) British Crown Copyright 2019, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the :mod:`iris._constraints` module.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa diff --git a/lib/iris/tests/unit/constraints/test_NameConstraint.py b/lib/iris/tests/unit/constraints/test_NameConstraint.py new file mode 100644 index 0000000000..c21be1b3b0 --- /dev/null +++ b/lib/iris/tests/unit/constraints/test_NameConstraint.py @@ -0,0 +1,208 @@ +# (C) British Crown Copyright 2019, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the `iris._constraints.NameConstraint` class.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from unittest.mock import Mock, sentinel + +from iris._constraints import NameConstraint + + +class Test___init__(tests.IrisTest): + def setUp(self): + self.default = 'none' + + def test_default(self): + constraint = NameConstraint() + self.assertEqual(constraint.standard_name, self.default) + self.assertEqual(constraint.long_name, self.default) + self.assertEqual(constraint.var_name, self.default) + self.assertEqual(constraint.STASH, self.default) + + def test_standard_name(self): + standard_name = sentinel.standard_name + constraint = NameConstraint(standard_name=standard_name) + self.assertEqual(constraint.standard_name, standard_name) + constraint = NameConstraint(standard_name) + self.assertEqual(constraint.standard_name, standard_name) + + def test_long_name(self): + long_name = sentinel.long_name + constraint = NameConstraint(long_name=long_name) + self.assertEqual(constraint.standard_name, self.default) + self.assertEqual(constraint.long_name, long_name) + constraint = NameConstraint(None, long_name) + self.assertIsNone(constraint.standard_name) + self.assertEqual(constraint.long_name, long_name) + + def test_var_name(self): + var_name = sentinel.var_name + constraint = NameConstraint(var_name=var_name) + self.assertEqual(constraint.standard_name, self.default) + self.assertEqual(constraint.long_name, self.default) + self.assertEqual(constraint.var_name, var_name) + constraint = NameConstraint(None, None, var_name) + self.assertIsNone(constraint.standard_name) + self.assertIsNone(constraint.long_name) + self.assertEqual(constraint.var_name, var_name) + + def test_STASH(self): + STASH = sentinel.STASH + constraint = NameConstraint(STASH=STASH) + self.assertEqual(constraint.standard_name, self.default) + self.assertEqual(constraint.long_name, self.default) + self.assertEqual(constraint.var_name, self.default) + self.assertEqual(constraint.STASH, STASH) + constraint = NameConstraint(None, None, None, STASH) + self.assertIsNone(constraint.standard_name) + self.assertIsNone(constraint.long_name) + self.assertIsNone(constraint.var_name) + self.assertEqual(constraint.STASH, STASH) + + +class Test__cube_func(tests.IrisTest): + def setUp(self): + self.standard_name = sentinel.standard_name + self.long_name = sentinel.long_name + self.var_name = sentinel.var_name + self.STASH = sentinel.STASH + self.cube = Mock(standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + attributes=dict(STASH=self.STASH)) + + def test_standard_name(self): + # Match. + constraint = NameConstraint(standard_name=self.standard_name) + self.assertTrue(constraint._cube_func(self.cube)) + # Match. + constraint = NameConstraint(self.standard_name) + self.assertTrue(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(standard_name='wibble') + self.assertFalse(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint('wibble') + self.assertFalse(constraint._cube_func(self.cube)) + + def test_long_name(self): + # Match. + constraint = NameConstraint(long_name=self.long_name) + self.assertTrue(constraint._cube_func(self.cube)) + # Match. + constraint = NameConstraint(self.standard_name, self.long_name) + self.assertTrue(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(long_name=None) + self.assertFalse(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(None, self.long_name) + self.assertFalse(constraint._cube_func(self.cube)) + + def test_var_name(self): + # Match. + constraint = NameConstraint(var_name=self.var_name) + self.assertTrue(constraint._cube_func(self.cube)) + # Match. + constraint = NameConstraint(self.standard_name, self.long_name, + self.var_name) + self.assertTrue(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(var_name=None) + self.assertFalse(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(None, None, self.var_name) + self.assertFalse(constraint._cube_func(self.cube)) + + def test_STASH(self): + # Match. + constraint = NameConstraint(STASH=self.STASH) + self.assertTrue(constraint._cube_func(self.cube)) + # Match. + constraint = NameConstraint(self.standard_name, self.long_name, + self.var_name, self.STASH) + self.assertTrue(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(STASH=None) + self.assertFalse(constraint._cube_func(self.cube)) + # No match. + constraint = NameConstraint(None, None, None, self.STASH) + self.assertFalse(constraint._cube_func(self.cube)) + + +class Test___repr__(tests.IrisTest): + def setUp(self): + self.standard_name = sentinel.standard_name + self.long_name = sentinel.long_name + self.var_name = sentinel.var_name + self.STASH = sentinel.STASH + self.msg = 'NameConstraint({})' + self.f_standard_name = 'standard_name={!r}'.format(self.standard_name) + self.f_long_name = 'long_name={!r}'.format(self.long_name) + self.f_var_name = 'var_name={!r}'.format(self.var_name) + self.f_STASH = 'STASH={!r}'.format(self.STASH) + + def test(self): + constraint = NameConstraint() + expected = self.msg.format('') + self.assertEqual(repr(constraint), expected) + + def test_standard_name(self): + constraint = NameConstraint(self.standard_name) + expected = self.msg.format(self.f_standard_name) + self.assertEqual(repr(constraint), expected) + + def test_long_name(self): + constraint = NameConstraint(long_name=self.long_name) + expected = self.msg.format(self.f_long_name) + self.assertEqual(repr(constraint), expected) + constraint = NameConstraint(self.standard_name, self.long_name) + args = '{}, {}'.format(self.f_standard_name, self.f_long_name) + expected = self.msg.format(args) + self.assertEqual(repr(constraint), expected) + + def test_var_name(self): + constraint = NameConstraint(var_name=self.var_name) + expected = self.msg.format(self.f_var_name) + self.assertEqual(repr(constraint), expected) + constraint = NameConstraint(self.standard_name, self.long_name, + self.var_name) + args = '{}, {}, {}'.format(self.f_standard_name, self.f_long_name, + self.f_var_name) + expected = self.msg.format(args) + self.assertEqual(repr(constraint), expected) + + def test_STASH(self): + constraint = NameConstraint(STASH=self.STASH) + expected = self.msg.format(self.f_STASH) + self.assertEqual(repr(constraint), expected) + constraint = NameConstraint(self.standard_name, self.long_name, + self.var_name, self.STASH) + args = '{}, {}, {}, {}'.format(self.f_standard_name, self.f_long_name, + self.f_var_name, self.f_STASH) + expected = self.msg.format(args) + self.assertEqual(repr(constraint), expected) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py index 16dbc87215..bf42015c64 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py @@ -142,19 +142,19 @@ def setUp(self): def test_standard_name(self): standard_name = 'air_temperature' self.cf_var.standard_name = standard_name - expected = (standard_name, None, None, '') + expected = (standard_name, None, None, None) self.assertEqual(expected, self.cf_var.names) def test_long_name(self): long_name = 'air temperature' self.cf_var.long_name = long_name - expected = (None, long_name, None, '') + expected = (None, long_name, None, None) self.assertEqual(expected, self.cf_var.names) def test_var_name(self): var_name = 'atemp' self.cf_var.var_name = var_name - expected = (None, None, var_name, '') + expected = (None, None, var_name, None) self.assertEqual(expected, self.cf_var.names) def test_STASH(self): From d6b7db2e09e7696ea4e3ffaa43da28610017cc62 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 29 Oct 2019 15:37:23 +0000 Subject: [PATCH 08/14] Fix NameConstraint doc-string --- lib/iris/_constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index 7ad8722c81..d6aaab6769 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -513,7 +513,7 @@ def __init__(self, standard_name='none', long_name='none', iris.NameConstraint(long_name='air temp') iris.NameConstraint(standard_name='air_temperature', - STASH=lambda stash: stash.item == 203 + STASH=lambda stash: stash.item == 203) .. note:: From b52f7c361d3caea61838c7c6a7ba81ab6bb6fc3d Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 30 Oct 2019 13:55:57 +0000 Subject: [PATCH 09/14] Add new license header to new files --- lib/iris/tests/unit/constraints/__init__.py | 19 ++++--------------- .../unit/constraints/test_NameConstraint.py | 19 ++++--------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/lib/iris/tests/unit/constraints/__init__.py b/lib/iris/tests/unit/constraints/__init__.py index 146a4e5a8e..3a4fdb82bf 100644 --- a/lib/iris/tests/unit/constraints/__init__.py +++ b/lib/iris/tests/unit/constraints/__init__.py @@ -1,19 +1,8 @@ -# (C) British Crown Copyright 2019, Met Office +# Copyright Iris contributors # -# This file is part of Iris. -# -# Iris is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Iris is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Iris. If not, see . +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. """Unit tests for the :mod:`iris._constraints` module.""" from __future__ import (absolute_import, division, print_function) diff --git a/lib/iris/tests/unit/constraints/test_NameConstraint.py b/lib/iris/tests/unit/constraints/test_NameConstraint.py index c21be1b3b0..6699b960c0 100644 --- a/lib/iris/tests/unit/constraints/test_NameConstraint.py +++ b/lib/iris/tests/unit/constraints/test_NameConstraint.py @@ -1,19 +1,8 @@ -# (C) British Crown Copyright 2019, Met Office +# Copyright Iris contributors # -# This file is part of Iris. -# -# Iris is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Iris is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Iris. If not, see . +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. """Unit tests for the `iris._constraints.NameConstraint` class.""" from __future__ import (absolute_import, division, print_function) From 71e7929d57ed76cba53c130785f110c7eaa7b500 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 4 Nov 2019 15:38:40 +0000 Subject: [PATCH 10/14] Names property returns namedtuple --- lib/iris/_cube_coord_common.py | 27 ++++++++++++++++++- .../cube_coord_common/test_CFVariableMixin.py | 16 ++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/iris/_cube_coord_common.py b/lib/iris/_cube_coord_common.py index 4cd744af19..43ecc71b0b 100644 --- a/lib/iris/_cube_coord_common.py +++ b/lib/iris/_cube_coord_common.py @@ -4,6 +4,8 @@ # See COPYING and COPYING.LESSER in the root of the repository for full # licensing details. + +from collections import namedtuple import re import cf_units @@ -15,6 +17,29 @@ _TOKEN_PARSE = re.compile(r"""^[a-zA-Z0-9][\w\.\+\-@]*$""") +class Names(namedtuple('Names', + ['standard_name', 'long_name', 'var_name', 'STASH'])): + """ + Immutable container for name metadata. + + Args: + + * standard_name: + A string representing the CF Conventions and Metadata standard name, or + None. + * long_name: + A string representing the CF Conventions and Metadata long name, or + None + * var_name: + A string representing the associated NetCDF variable name, or None. + * STASH: + A string representing the `~iris.fileformats.pp.STASH` code, or None. + + """ + + __slots__ = () + + def get_valid_standard_name(name): # Standard names are optionally followed by a standard name # modifier, separated by one or more blank spaces @@ -195,7 +220,7 @@ def names(self): stash_name = self.attributes.get('STASH') if stash_name is not None: stash_name = str(stash_name) - return (standard_name, long_name, var_name, stash_name) + return Names(standard_name, long_name, var_name, stash_name) def rename(self, name): """ diff --git a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py index bf42015c64..7bb10be366 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py @@ -143,25 +143,33 @@ def test_standard_name(self): standard_name = 'air_temperature' self.cf_var.standard_name = standard_name expected = (standard_name, None, None, None) - self.assertEqual(expected, self.cf_var.names) + result = self.cf_var.names + self.assertEqual(expected, result) + self.assertEqual(result.standard_name, standard_name) def test_long_name(self): long_name = 'air temperature' self.cf_var.long_name = long_name expected = (None, long_name, None, None) - self.assertEqual(expected, self.cf_var.names) + result = self.cf_var.names + self.assertEqual(expected, result) + self.assertEqual(result.long_name, long_name) def test_var_name(self): var_name = 'atemp' self.cf_var.var_name = var_name expected = (None, None, var_name, None) - self.assertEqual(expected, self.cf_var.names) + result = self.cf_var.names + self.assertEqual(expected, result) + self.assertEqual(result.var_name, var_name) def test_STASH(self): stash = 'm01s16i203' self.cf_var.attributes = dict(STASH=stash) expected = (None, None, None, stash) - self.assertEqual(expected, self.cf_var.names) + result = self.cf_var.names + self.assertEqual(expected, result) + self.assertEqual(result.STASH, stash) class Test_standard_name__setter(tests.IrisTest): From 7073811e2a1755e1fa1ed67ae6c89f4e4f29bd03 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 12 Nov 2019 12:09:21 +0000 Subject: [PATCH 11/14] fix manual typo from rebase and blackify --- lib/iris/__init__.py | 2 +- lib/iris/_constraints.py | 30 ++++--- lib/iris/_cube_coord_common.py | 7 +- lib/iris/tests/test_constraints.py | 85 +++++++++++-------- lib/iris/tests/unit/constraints/__init__.py | 4 +- .../unit/constraints/test_NameConstraint.py | 69 ++++++++------- .../cube_coord_common/test_CFVariableMixin.py | 8 +- 7 files changed, 118 insertions(+), 87 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 969fe54e9b..6fe576ead7 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -117,7 +117,7 @@ def callback(cube, field, filename): "save", "Constraint", "AttributeConstraint", - "NameConstraint" + "NameConstraint", "sample_data_path", "site_configuration", "Future", diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index d6aaab6769..18d0d202e8 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -130,8 +130,10 @@ def _coordless_match(self, cube): """ match = True if self._name: - match = (self._name in cube.names or - self._name == CFVariableMixin._DEFAULT_NAME) + match = ( + self._name in cube.names + or self._name == CFVariableMixin._DEFAULT_NAME + ) if match and self._cube_func: match = self._cube_func(cube) return match @@ -505,8 +507,14 @@ class NameConstraint(Constraint): The name constraint will only succeed if all of the provided names match. """ - def __init__(self, standard_name='none', long_name='none', - var_name='none', STASH='none'): + + def __init__( + self, + standard_name="none", + long_name="none", + var_name="none", + STASH="none", + ): """ Example usage:: @@ -525,7 +533,7 @@ def __init__(self, standard_name='none', long_name='none', self.long_name = long_name self.var_name = var_name self.STASH = STASH - self._names = ('standard_name', 'long_name', 'var_name', 'STASH') + self._names = ("standard_name", "long_name", "var_name", "STASH") super().__init__(cube_func=self._cube_func) def _cube_func(self, cube): @@ -538,11 +546,11 @@ def matcher(target, value): match = True for name in self._names: - if name == 'STASH' and self.STASH != 'none': - match = matcher(cube.attributes.get('STASH'), self.STASH) + if name == "STASH" and self.STASH != "none": + match = matcher(cube.attributes.get("STASH"), self.STASH) else: expected = getattr(self, name) - if expected != 'none': + if expected != "none": actual = getattr(cube, name) match = matcher(actual, expected) # This is a short-cut match. @@ -555,6 +563,6 @@ def __repr__(self): names = [] for name in self._names: value = getattr(self, name) - if value != 'none': - names.append('{}={!r}'.format(name, value)) - return '{}({})'.format(self.__class__.__name__, ', '.join(names)) + if value != "none": + names.append("{}={!r}".format(name, value)) + return "{}({})".format(self.__class__.__name__, ", ".join(names)) diff --git a/lib/iris/_cube_coord_common.py b/lib/iris/_cube_coord_common.py index 43ecc71b0b..0a3dfd12ca 100644 --- a/lib/iris/_cube_coord_common.py +++ b/lib/iris/_cube_coord_common.py @@ -17,8 +17,9 @@ _TOKEN_PARSE = re.compile(r"""^[a-zA-Z0-9][\w\.\+\-@]*$""") -class Names(namedtuple('Names', - ['standard_name', 'long_name', 'var_name', 'STASH'])): +class Names( + namedtuple("Names", ["standard_name", "long_name", "var_name", "STASH"]) +): """ Immutable container for name metadata. @@ -217,7 +218,7 @@ def names(self): standard_name = self.standard_name long_name = self.long_name var_name = self.var_name - stash_name = self.attributes.get('STASH') + stash_name = self.attributes.get("STASH") if stash_name is not None: stash_name = str(stash_name) return Names(standard_name, long_name, var_name, stash_name) diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index b96a585878..9b5f810cda 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -302,10 +302,10 @@ def setUp(self): TestMixin.setUp(self) self.cube = iris.load_cube(self.theta_path) # Expected names... - self.standard_name = 'air_potential_temperature' - self.long_name = 'air potential temperature' - self.var_name = 'apt' - self.stash = 'm01s00i004' + self.standard_name = "air_potential_temperature" + self.long_name = "air potential temperature" + self.var_name = "apt" + self.stash = "m01s00i004" # Configure missing names... self.cube.long_name = self.long_name self.cube.var_name = self.var_name @@ -332,7 +332,7 @@ def test_stash(self): constraint = iris.Constraint(self.stash) result = self.cube.extract(constraint) self.assertIsNotNone(result) - self.assertEqual(str(result.attributes['STASH']), self.stash) + self.assertEqual(str(result.attributes["STASH"]), self.stash) def test_unknown(self): # Clear the cube metadata. @@ -341,7 +341,7 @@ def test_unknown(self): self.cube.var_name = None self.cube.attributes = None # Extract the unknown cube. - constraint = iris.Constraint('unknown') + constraint = iris.Constraint("unknown") result = self.cube.extract(constraint) self.assertIsNotNone(result) @@ -352,17 +352,17 @@ def setUp(self): TestMixin.setUp(self) self.cube = iris.load_cube(self.theta_path) # Expected names... - self.standard_name = 'air_potential_temperature' - self.long_name = 'air potential temperature' - self.var_name = 'apt' - self.stash = 'm01s00i004' + self.standard_name = "air_potential_temperature" + self.long_name = "air potential temperature" + self.var_name = "apt" + self.stash = "m01s00i004" # Configure missing names... self.cube.long_name = self.long_name self.cube.var_name = self.var_name def test_standard_name(self): # No match. - constraint = NameConstraint(standard_name='wibble') + constraint = NameConstraint(standard_name="wibble") result = self.cube.extract(constraint) self.assertIsNone(result) @@ -372,14 +372,14 @@ def test_standard_name(self): self.assertIsNotNone(result) # Match - callable. - kwargs = dict(standard_name=lambda item: item.startswith('air_pot')) + kwargs = dict(standard_name=lambda item: item.startswith("air_pot")) constraint = NameConstraint(**kwargs) result = self.cube.extract(constraint) self.assertIsNotNone(result) def test_long_name(self): # No match. - constraint = NameConstraint(long_name='wibble') + constraint = NameConstraint(long_name="wibble") result = self.cube.extract(constraint) self.assertIsNone(result) @@ -389,14 +389,14 @@ def test_long_name(self): self.assertIsNotNone(result) # Match - callable. - kwargs = dict(long_name=lambda item: item.startswith('air pot')) + kwargs = dict(long_name=lambda item: item.startswith("air pot")) constraint = NameConstraint(**kwargs) result = self.cube.extract(constraint) self.assertIsNotNone(result) def test_var_name(self): # No match. - constraint = NameConstraint(var_name='wibble') + constraint = NameConstraint(var_name="wibble") result = self.cube.extract(constraint) self.assertIsNone(result) @@ -406,14 +406,14 @@ def test_var_name(self): self.assertIsNotNone(result) # Match - callable. - kwargs = dict(var_name=lambda item: item.startswith('ap')) + kwargs = dict(var_name=lambda item: item.startswith("ap")) constraint = NameConstraint(**kwargs) result = self.cube.extract(constraint) self.assertIsNotNone(result) def test_stash(self): # No match. - constraint = NameConstraint(STASH='m01s00i444') + constraint = NameConstraint(STASH="m01s00i444") result = self.cube.extract(constraint) self.assertIsNone(result) @@ -430,46 +430,57 @@ def test_stash(self): def test_compound(self): # Match. - constraint = NameConstraint(standard_name=self.standard_name, - long_name=self.long_name) + constraint = NameConstraint( + standard_name=self.standard_name, long_name=self.long_name + ) result = self.cube.extract(constraint) self.assertIsNotNone(result) # No match - var_name. - constraint = NameConstraint(standard_name=self.standard_name, - long_name=self.long_name, - var_name='wibble') + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name="wibble", + ) result = self.cube.extract(constraint) self.assertIsNone(result) # Match. - constraint = NameConstraint(standard_name=self.standard_name, - long_name=self.long_name, - var_name=self.var_name) + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + ) result = self.cube.extract(constraint) self.assertIsNotNone(result) # No match - STASH. - constraint = NameConstraint(standard_name=self.standard_name, - long_name=self.long_name, - var_name=self.var_name, - STASH='m01s00i444') + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH="m01s00i444", + ) result = self.cube.extract(constraint) self.assertIsNone(result) # Match. - constraint = NameConstraint(standard_name=self.standard_name, - long_name=self.long_name, - var_name=self.var_name, - STASH=self.stash) + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH=self.stash, + ) result = self.cube.extract(constraint) self.assertIsNotNone(result) # No match - standard_name. - constraint = NameConstraint(standard_name='wibble', - long_name=self.long_name, - var_name=self.var_name, - STASH=self.stash) + constraint = NameConstraint( + standard_name="wibble", + long_name=self.long_name, + var_name=self.var_name, + STASH=self.stash, + ) result = self.cube.extract(constraint) self.assertIsNone(result) diff --git a/lib/iris/tests/unit/constraints/__init__.py b/lib/iris/tests/unit/constraints/__init__.py index 3a4fdb82bf..f988e74937 100644 --- a/lib/iris/tests/unit/constraints/__init__.py +++ b/lib/iris/tests/unit/constraints/__init__.py @@ -5,5 +5,5 @@ # licensing details. """Unit tests for the :mod:`iris._constraints` module.""" -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa +from __future__ import absolute_import, division, print_function +from six.moves import filter, input, map, range, zip # noqa diff --git a/lib/iris/tests/unit/constraints/test_NameConstraint.py b/lib/iris/tests/unit/constraints/test_NameConstraint.py index 6699b960c0..cefb396ec8 100644 --- a/lib/iris/tests/unit/constraints/test_NameConstraint.py +++ b/lib/iris/tests/unit/constraints/test_NameConstraint.py @@ -5,8 +5,8 @@ # licensing details. """Unit tests for the `iris._constraints.NameConstraint` class.""" -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa +from __future__ import absolute_import, division, print_function +from six.moves import filter, input, map, range, zip # noqa # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -19,7 +19,7 @@ class Test___init__(tests.IrisTest): def setUp(self): - self.default = 'none' + self.default = "none" def test_default(self): constraint = NameConstraint() @@ -75,10 +75,12 @@ def setUp(self): self.long_name = sentinel.long_name self.var_name = sentinel.var_name self.STASH = sentinel.STASH - self.cube = Mock(standard_name=self.standard_name, - long_name=self.long_name, - var_name=self.var_name, - attributes=dict(STASH=self.STASH)) + self.cube = Mock( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + attributes=dict(STASH=self.STASH), + ) def test_standard_name(self): # Match. @@ -88,10 +90,10 @@ def test_standard_name(self): constraint = NameConstraint(self.standard_name) self.assertTrue(constraint._cube_func(self.cube)) # No match. - constraint = NameConstraint(standard_name='wibble') + constraint = NameConstraint(standard_name="wibble") self.assertFalse(constraint._cube_func(self.cube)) # No match. - constraint = NameConstraint('wibble') + constraint = NameConstraint("wibble") self.assertFalse(constraint._cube_func(self.cube)) def test_long_name(self): @@ -113,8 +115,9 @@ def test_var_name(self): constraint = NameConstraint(var_name=self.var_name) self.assertTrue(constraint._cube_func(self.cube)) # Match. - constraint = NameConstraint(self.standard_name, self.long_name, - self.var_name) + constraint = NameConstraint( + self.standard_name, self.long_name, self.var_name + ) self.assertTrue(constraint._cube_func(self.cube)) # No match. constraint = NameConstraint(var_name=None) @@ -128,8 +131,9 @@ def test_STASH(self): constraint = NameConstraint(STASH=self.STASH) self.assertTrue(constraint._cube_func(self.cube)) # Match. - constraint = NameConstraint(self.standard_name, self.long_name, - self.var_name, self.STASH) + constraint = NameConstraint( + self.standard_name, self.long_name, self.var_name, self.STASH + ) self.assertTrue(constraint._cube_func(self.cube)) # No match. constraint = NameConstraint(STASH=None) @@ -145,15 +149,15 @@ def setUp(self): self.long_name = sentinel.long_name self.var_name = sentinel.var_name self.STASH = sentinel.STASH - self.msg = 'NameConstraint({})' - self.f_standard_name = 'standard_name={!r}'.format(self.standard_name) - self.f_long_name = 'long_name={!r}'.format(self.long_name) - self.f_var_name = 'var_name={!r}'.format(self.var_name) - self.f_STASH = 'STASH={!r}'.format(self.STASH) + self.msg = "NameConstraint({})" + self.f_standard_name = "standard_name={!r}".format(self.standard_name) + self.f_long_name = "long_name={!r}".format(self.long_name) + self.f_var_name = "var_name={!r}".format(self.var_name) + self.f_STASH = "STASH={!r}".format(self.STASH) def test(self): constraint = NameConstraint() - expected = self.msg.format('') + expected = self.msg.format("") self.assertEqual(repr(constraint), expected) def test_standard_name(self): @@ -166,7 +170,7 @@ def test_long_name(self): expected = self.msg.format(self.f_long_name) self.assertEqual(repr(constraint), expected) constraint = NameConstraint(self.standard_name, self.long_name) - args = '{}, {}'.format(self.f_standard_name, self.f_long_name) + args = "{}, {}".format(self.f_standard_name, self.f_long_name) expected = self.msg.format(args) self.assertEqual(repr(constraint), expected) @@ -174,10 +178,12 @@ def test_var_name(self): constraint = NameConstraint(var_name=self.var_name) expected = self.msg.format(self.f_var_name) self.assertEqual(repr(constraint), expected) - constraint = NameConstraint(self.standard_name, self.long_name, - self.var_name) - args = '{}, {}, {}'.format(self.f_standard_name, self.f_long_name, - self.f_var_name) + constraint = NameConstraint( + self.standard_name, self.long_name, self.var_name + ) + args = "{}, {}, {}".format( + self.f_standard_name, self.f_long_name, self.f_var_name + ) expected = self.msg.format(args) self.assertEqual(repr(constraint), expected) @@ -185,13 +191,18 @@ def test_STASH(self): constraint = NameConstraint(STASH=self.STASH) expected = self.msg.format(self.f_STASH) self.assertEqual(repr(constraint), expected) - constraint = NameConstraint(self.standard_name, self.long_name, - self.var_name, self.STASH) - args = '{}, {}, {}, {}'.format(self.f_standard_name, self.f_long_name, - self.f_var_name, self.f_STASH) + constraint = NameConstraint( + self.standard_name, self.long_name, self.var_name, self.STASH + ) + args = "{}, {}, {}, {}".format( + self.f_standard_name, + self.f_long_name, + self.f_var_name, + self.f_STASH, + ) expected = self.msg.format(args) self.assertEqual(repr(constraint), expected) -if __name__ == '__main__': +if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py index 7bb10be366..74bf805ece 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py @@ -140,7 +140,7 @@ def setUp(self): self.cf_var.attributes = dict() def test_standard_name(self): - standard_name = 'air_temperature' + standard_name = "air_temperature" self.cf_var.standard_name = standard_name expected = (standard_name, None, None, None) result = self.cf_var.names @@ -148,7 +148,7 @@ def test_standard_name(self): self.assertEqual(result.standard_name, standard_name) def test_long_name(self): - long_name = 'air temperature' + long_name = "air temperature" self.cf_var.long_name = long_name expected = (None, long_name, None, None) result = self.cf_var.names @@ -156,7 +156,7 @@ def test_long_name(self): self.assertEqual(result.long_name, long_name) def test_var_name(self): - var_name = 'atemp' + var_name = "atemp" self.cf_var.var_name = var_name expected = (None, None, var_name, None) result = self.cf_var.names @@ -164,7 +164,7 @@ def test_var_name(self): self.assertEqual(result.var_name, var_name) def test_STASH(self): - stash = 'm01s16i203' + stash = "m01s16i203" self.cf_var.attributes = dict(STASH=stash) expected = (None, None, None, stash) result = self.cf_var.names From 1e10bda04601182f5a0416f355fe9a138b9bce0d Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 20 Nov 2019 09:40:31 +0000 Subject: [PATCH 12/14] review actions --- lib/iris/_constraints.py | 89 ++++++--- lib/iris/tests/test_constraints.py | 177 +++++++++++++----- .../unit/constraints/test_NameConstraint.py | 56 ++++-- .../cube_coord_common/test_CFVariableMixin.py | 5 + 4 files changed, 235 insertions(+), 92 deletions(-) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index 18d0d202e8..bcaed4dc3a 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -13,7 +13,6 @@ import numpy as np -from iris._cube_coord_common import CFVariableMixin import iris.coords import iris.exceptions @@ -130,10 +129,7 @@ def _coordless_match(self, cube): """ match = True if self._name: - match = ( - self._name in cube.names - or self._name == CFVariableMixin._DEFAULT_NAME - ) + match = self._name in cube.names or self._name == cube.name() if match and self._cube_func: match = self._cube_func(cube) return match @@ -498,15 +494,7 @@ def __repr__(self): class NameConstraint(Constraint): - """ - Provides a simple Cube name based :class:`Constraint`, which matches - against each of the names provided, which may be either standard name, - long name, NetCDF variable name and/or the STASH from the attributes - dictionary. - - The name constraint will only succeed if all of the provided names match. - - """ + """Provides a simple Cube name based :class:`Constraint`.""" def __init__( self, @@ -516,18 +504,49 @@ def __init__( STASH="none", ): """ + Provides a simple Cube name based :class:`Constraint`, which matches + against each of the names provided, which may be either standard name, + long name, NetCDF variable name and/or the STASH from the attributes + dictionary. + + The name constraint will only succeed if *all* of the provided names + match. + + Kwargs: + * standard_name: + A string or callable representing the standard name to match + against. + * long_name: + A string or callable representing the long name to match against. + * var_name: + A string or callable representing the NetCDF variable name to match + against. + * STASH: + A string or callable representing the UM STASH code to match + against. + + .. note:: + The default value of each of the keyword arguments is the string + "none", rather than the singleton None, as None may be a legitimate + value to be matched against e.g., to constrain against all cubes + where the standard_name is not set, then use standard_name=None. + + ... note:: + The None value will not be passed through to a callable. Instead + use the "=None" pattern. + + Returns: + * Boolean + Example usage:: - iris.NameConstraint(long_name='air temp') + iris.NameConstraint(long_name='air temp', var_name=None) + + iris.NameConstraint(long_name=lambda name: 'temp' in name) iris.NameConstraint(standard_name='air_temperature', STASH=lambda stash: stash.item == 203) - .. note:: - - Name constraint names are case sensitive i.e., use the - ``STASH`` keyword argument and not ``stash``. - """ self.standard_name = standard_name self.long_name = long_name @@ -539,23 +558,33 @@ def __init__( def _cube_func(self, cube): def matcher(target, value): if callable(value): - result = value(target) + result = False + if target is not None: + # + # Don't pass None through into the callable. Users should + # use the "name=None" pattern instead. Otherwise, users + # will need to explicitly handle the None case, which is + # unnecessary and pretty darn ugly e.g., + # + # lambda name: name is not None and name.startswith('ick') + # + result = value(target) else: result = value == target return result match = True for name in self._names: - if name == "STASH" and self.STASH != "none": - match = matcher(cube.attributes.get("STASH"), self.STASH) - else: - expected = getattr(self, name) - if expected != "none": + expected = getattr(self, name) + if expected != "none": + if name == "STASH": + actual = cube.attributes.get(name) + else: actual = getattr(cube, name) - match = matcher(actual, expected) - # This is a short-cut match. - if match is False: - break + match = matcher(actual, expected) + # Make this is a short-circuit match. + if match is False: + break return match diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index 9b5f810cda..07c4bf58bb 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -299,142 +299,209 @@ def load_match(self, files, constraints): @tests.skip_data class TestCubeExtract__names(TestMixin, tests.IrisTest): def setUp(self): + fname = iris.sample_data_path("atlantic_profiles.nc") + self.cubes = iris.load(fname) TestMixin.setUp(self) - self.cube = iris.load_cube(self.theta_path) + cube = iris.load_cube(self.theta_path) # Expected names... self.standard_name = "air_potential_temperature" self.long_name = "air potential temperature" self.var_name = "apt" self.stash = "m01s00i004" # Configure missing names... - self.cube.long_name = self.long_name - self.cube.var_name = self.var_name + cube.long_name = self.long_name + cube.var_name = self.var_name + # Add this cube to the mix... + self.cubes.append(cube) + self.index = len(self.cubes) - 1 def test_standard_name(self): constraint = iris.Constraint(self.standard_name) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) self.assertEqual(result.standard_name, self.standard_name) def test_long_name(self): constraint = iris.Constraint(self.long_name) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) self.assertEqual(result.long_name, self.long_name) def test_var_name(self): constraint = iris.Constraint(self.var_name) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) self.assertEqual(result.var_name, self.var_name) def test_stash(self): constraint = iris.Constraint(self.stash) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) self.assertEqual(str(result.attributes["STASH"]), self.stash) def test_unknown(self): + cube = self.cubes[self.index] # Clear the cube metadata. - self.cube.standard_name = None - self.cube.long_name = None - self.cube.var_name = None - self.cube.attributes = None + cube.standard_name = None + cube.long_name = None + cube.var_name = None + cube.attributes = None # Extract the unknown cube. constraint = iris.Constraint("unknown") - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.name(), "unknown") @tests.skip_data class TestCubeExtract__name_constraint(TestMixin, tests.IrisTest): def setUp(self): + fname = iris.sample_data_path("atlantic_profiles.nc") + self.cubes = iris.load(fname) TestMixin.setUp(self) - self.cube = iris.load_cube(self.theta_path) + cube = iris.load_cube(self.theta_path) # Expected names... self.standard_name = "air_potential_temperature" self.long_name = "air potential temperature" self.var_name = "apt" self.stash = "m01s00i004" # Configure missing names... - self.cube.long_name = self.long_name - self.cube.var_name = self.var_name + cube.long_name = self.long_name + cube.var_name = self.var_name + # Add this cube to the mix... + self.cubes.append(cube) + self.index = len(self.cubes) - 1 def test_standard_name(self): # No match. constraint = NameConstraint(standard_name="wibble") - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) # Match. constraint = NameConstraint(standard_name=self.standard_name) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) # Match - callable. kwargs = dict(standard_name=lambda item: item.startswith("air_pot")) constraint = NameConstraint(**kwargs) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + + def test_standard_name__None(self): + cube = self.cubes[self.index] + cube.standard_name = None + constraint = NameConstraint( + standard_name=None, long_name=self.long_name + ) + result = self.cubes.extract(constraint, strict=True) + self.assertIsNotNone(result) + self.assertIsNone(result.standard_name) + self.assertEqual(result.long_name, self.long_name) def test_long_name(self): # No match. constraint = NameConstraint(long_name="wibble") - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) # Match. constraint = NameConstraint(long_name=self.long_name) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.long_name, self.long_name) # Match - callable. - kwargs = dict(long_name=lambda item: item.startswith("air pot")) + kwargs = dict( + long_name=lambda item: item is not None + and item.startswith("air pot") + ) constraint = NameConstraint(**kwargs) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.long_name, self.long_name) + + def test_long_name__None(self): + cube = self.cubes[self.index] + cube.long_name = None + constraint = NameConstraint( + standard_name=self.standard_name, long_name=None + ) + result = self.cubes.extract(constraint, strict=True) + self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + self.assertIsNone(result.long_name) def test_var_name(self): # No match. constraint = NameConstraint(var_name="wibble") - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) # Match. constraint = NameConstraint(var_name=self.var_name) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.var_name, self.var_name) # Match - callable. kwargs = dict(var_name=lambda item: item.startswith("ap")) constraint = NameConstraint(**kwargs) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) + self.assertIsNotNone(result) + self.assertEqual(result.var_name, self.var_name) + + def test_var_name__None(self): + cube = self.cubes[self.index] + cube.var_name = None + constraint = NameConstraint( + standard_name=self.standard_name, var_name=None + ) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + self.assertIsNone(result.var_name) def test_stash(self): # No match. constraint = NameConstraint(STASH="m01s00i444") - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) # Match. constraint = NameConstraint(STASH=self.stash) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(str(result.attributes["STASH"]), self.stash) # Match - callable. kwargs = dict(STASH=lambda stash: stash.item == 4) constraint = NameConstraint(**kwargs) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + def test_stash__None(self): + cube = self.cubes[self.index] + del cube.attributes["STASH"] + constraint = NameConstraint( + standard_name=self.standard_name, STASH=None + ) + result = self.cubes.extract(constraint, strict=True) + self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + self.assertIsNone(result.attributes.get("STASH")) + def test_compound(self): # Match. constraint = NameConstraint( standard_name=self.standard_name, long_name=self.long_name ) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) # No match - var_name. constraint = NameConstraint( @@ -442,8 +509,8 @@ def test_compound(self): long_name=self.long_name, var_name="wibble", ) - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) # Match. constraint = NameConstraint( @@ -451,8 +518,11 @@ def test_compound(self): long_name=self.long_name, var_name=self.var_name, ) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + self.assertEqual(result.long_name, self.long_name) + self.assertEqual(result.var_name, self.var_name) # No match - STASH. constraint = NameConstraint( @@ -461,8 +531,8 @@ def test_compound(self): var_name=self.var_name, STASH="m01s00i444", ) - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) # Match. constraint = NameConstraint( @@ -471,8 +541,12 @@ def test_compound(self): var_name=self.var_name, STASH=self.stash, ) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + self.assertEqual(result.long_name, self.long_name) + self.assertEqual(result.var_name, self.var_name) + self.assertEqual(result.var_name, self.var_name) # No match - standard_name. constraint = NameConstraint( @@ -481,17 +555,28 @@ def test_compound(self): var_name=self.var_name, STASH=self.stash, ) - result = self.cube.extract(constraint) - self.assertIsNone(result) + result = self.cubes.extract(constraint) + self.assertFalse(result) def test_unknown(self): - self.cube.standard_name = None - self.cube.long_name = None - self.cube.var_name = None - self.cube.attributes = None + # No match. + constraint = NameConstraint(None, None, None, None) + result = self.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + cube = self.cubes[self.index] + cube.standard_name = None + cube.long_name = None + cube.var_name = None + cube.attributes = None constraint = NameConstraint(None, None, None, None) - result = self.cube.extract(constraint) + result = self.cubes.extract(constraint, strict=True) self.assertIsNotNone(result) + self.assertIsNone(result.standard_name) + self.assertIsNone(result.long_name) + self.assertIsNone(result.var_name) + self.assertIsNone(result.attributes.get("STASH")) @tests.skip_data diff --git a/lib/iris/tests/unit/constraints/test_NameConstraint.py b/lib/iris/tests/unit/constraints/test_NameConstraint.py index cefb396ec8..553f4ca7a6 100644 --- a/lib/iris/tests/unit/constraints/test_NameConstraint.py +++ b/lib/iris/tests/unit/constraints/test_NameConstraint.py @@ -32,7 +32,7 @@ def test_standard_name(self): standard_name = sentinel.standard_name constraint = NameConstraint(standard_name=standard_name) self.assertEqual(constraint.standard_name, standard_name) - constraint = NameConstraint(standard_name) + constraint = NameConstraint(standard_name=standard_name) self.assertEqual(constraint.standard_name, standard_name) def test_long_name(self): @@ -40,7 +40,7 @@ def test_long_name(self): constraint = NameConstraint(long_name=long_name) self.assertEqual(constraint.standard_name, self.default) self.assertEqual(constraint.long_name, long_name) - constraint = NameConstraint(None, long_name) + constraint = NameConstraint(standard_name=None, long_name=long_name) self.assertIsNone(constraint.standard_name) self.assertEqual(constraint.long_name, long_name) @@ -50,7 +50,9 @@ def test_var_name(self): self.assertEqual(constraint.standard_name, self.default) self.assertEqual(constraint.long_name, self.default) self.assertEqual(constraint.var_name, var_name) - constraint = NameConstraint(None, None, var_name) + constraint = NameConstraint( + standard_name=None, long_name=None, var_name=var_name + ) self.assertIsNone(constraint.standard_name) self.assertIsNone(constraint.long_name) self.assertEqual(constraint.var_name, var_name) @@ -62,7 +64,9 @@ def test_STASH(self): self.assertEqual(constraint.long_name, self.default) self.assertEqual(constraint.var_name, self.default) self.assertEqual(constraint.STASH, STASH) - constraint = NameConstraint(None, None, None, STASH) + constraint = NameConstraint( + standard_name=None, long_name=None, var_name=None, STASH=STASH + ) self.assertIsNone(constraint.standard_name) self.assertIsNone(constraint.long_name) self.assertIsNone(constraint.var_name) @@ -87,13 +91,13 @@ def test_standard_name(self): constraint = NameConstraint(standard_name=self.standard_name) self.assertTrue(constraint._cube_func(self.cube)) # Match. - constraint = NameConstraint(self.standard_name) + constraint = NameConstraint(standard_name=self.standard_name) self.assertTrue(constraint._cube_func(self.cube)) # No match. constraint = NameConstraint(standard_name="wibble") self.assertFalse(constraint._cube_func(self.cube)) # No match. - constraint = NameConstraint("wibble") + constraint = NameConstraint(standard_name="wibble") self.assertFalse(constraint._cube_func(self.cube)) def test_long_name(self): @@ -101,13 +105,17 @@ def test_long_name(self): constraint = NameConstraint(long_name=self.long_name) self.assertTrue(constraint._cube_func(self.cube)) # Match. - constraint = NameConstraint(self.standard_name, self.long_name) + constraint = NameConstraint( + standard_name=self.standard_name, long_name=self.long_name + ) self.assertTrue(constraint._cube_func(self.cube)) # No match. constraint = NameConstraint(long_name=None) self.assertFalse(constraint._cube_func(self.cube)) # No match. - constraint = NameConstraint(None, self.long_name) + constraint = NameConstraint( + standard_name=None, long_name=self.long_name + ) self.assertFalse(constraint._cube_func(self.cube)) def test_var_name(self): @@ -116,14 +124,18 @@ def test_var_name(self): self.assertTrue(constraint._cube_func(self.cube)) # Match. constraint = NameConstraint( - self.standard_name, self.long_name, self.var_name + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, ) self.assertTrue(constraint._cube_func(self.cube)) # No match. constraint = NameConstraint(var_name=None) self.assertFalse(constraint._cube_func(self.cube)) # No match. - constraint = NameConstraint(None, None, self.var_name) + constraint = NameConstraint( + standard_name=None, long_name=None, var_name=self.var_name + ) self.assertFalse(constraint._cube_func(self.cube)) def test_STASH(self): @@ -132,14 +144,19 @@ def test_STASH(self): self.assertTrue(constraint._cube_func(self.cube)) # Match. constraint = NameConstraint( - self.standard_name, self.long_name, self.var_name, self.STASH + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH=self.STASH, ) self.assertTrue(constraint._cube_func(self.cube)) # No match. constraint = NameConstraint(STASH=None) self.assertFalse(constraint._cube_func(self.cube)) # No match. - constraint = NameConstraint(None, None, None, self.STASH) + constraint = NameConstraint( + standard_name=None, long_name=None, var_name=None, STASH=self.STASH + ) self.assertFalse(constraint._cube_func(self.cube)) @@ -161,7 +178,7 @@ def test(self): self.assertEqual(repr(constraint), expected) def test_standard_name(self): - constraint = NameConstraint(self.standard_name) + constraint = NameConstraint(standard_name=self.standard_name) expected = self.msg.format(self.f_standard_name) self.assertEqual(repr(constraint), expected) @@ -169,7 +186,9 @@ def test_long_name(self): constraint = NameConstraint(long_name=self.long_name) expected = self.msg.format(self.f_long_name) self.assertEqual(repr(constraint), expected) - constraint = NameConstraint(self.standard_name, self.long_name) + constraint = NameConstraint( + standard_name=self.standard_name, long_name=self.long_name + ) args = "{}, {}".format(self.f_standard_name, self.f_long_name) expected = self.msg.format(args) self.assertEqual(repr(constraint), expected) @@ -179,7 +198,9 @@ def test_var_name(self): expected = self.msg.format(self.f_var_name) self.assertEqual(repr(constraint), expected) constraint = NameConstraint( - self.standard_name, self.long_name, self.var_name + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, ) args = "{}, {}, {}".format( self.f_standard_name, self.f_long_name, self.f_var_name @@ -192,7 +213,10 @@ def test_STASH(self): expected = self.msg.format(self.f_STASH) self.assertEqual(repr(constraint), expected) constraint = NameConstraint( - self.standard_name, self.long_name, self.var_name, self.STASH + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH=self.STASH, ) args = "{}, {}, {}, {}".format( self.f_standard_name, diff --git a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py index 74bf805ece..0f08d397cb 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py @@ -171,6 +171,11 @@ def test_STASH(self): self.assertEqual(expected, result) self.assertEqual(result.STASH, stash) + def test_None(self): + expected = (None, None, None, None) + result = self.cf_var.names + self.assertEqual(expected, result) + class Test_standard_name__setter(tests.IrisTest): def test_valid_standard_name(self): From c3ce0083186bf20a7ca07d8aa4b62c54dff87b1d Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 20 Nov 2019 10:01:03 +0000 Subject: [PATCH 13/14] add _coordless_match comment --- lib/iris/_constraints.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index bcaed4dc3a..a29922fefb 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -129,6 +129,8 @@ def _coordless_match(self, cube): """ match = True if self._name: + # Require to also check against cube.name() for the fallback + # "unknown" default case, when there is no name metadata available. match = self._name in cube.names or self._name == cube.name() if match and self._cube_func: match = self._cube_func(cube) From b535c770fe6795f29f7d050e6a193b5c6df1810e Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 20 Nov 2019 14:06:32 +0000 Subject: [PATCH 14/14] further review actions --- lib/iris/_constraints.py | 4 ---- lib/iris/tests/test_constraints.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index a29922fefb..ab213dfa7b 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -533,10 +533,6 @@ def __init__( value to be matched against e.g., to constrain against all cubes where the standard_name is not set, then use standard_name=None. - ... note:: - The None value will not be passed through to a callable. Instead - use the "=None" pattern. - Returns: * Boolean diff --git a/lib/iris/tests/test_constraints.py b/lib/iris/tests/test_constraints.py index 07c4bf58bb..1f0bf064ed 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -305,7 +305,7 @@ def setUp(self): cube = iris.load_cube(self.theta_path) # Expected names... self.standard_name = "air_potential_temperature" - self.long_name = "air potential temperature" + self.long_name = "AIR POTENTIAL TEMPERATURE" self.var_name = "apt" self.stash = "m01s00i004" # Configure missing names...