diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index 2cb3b9b2596..bf50acc6146 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/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 00000000000..eeb40990e24 --- /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 00000000000..a0926311520 --- /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 00000000000..6773ac28b15 --- /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/__init__.py b/lib/iris/__init__.py index e6aed67177b..20eaffe6de3 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -124,14 +124,26 @@ def callback(cube, field, filename): __version__ = '2.4.0rc0' # Restrict the names imported when using "from iris import *" -__all__ = ['load', 'load_cube', 'load_cubes', 'load_raw', - 'save', 'Constraint', 'AttributeConstraint', 'sample_data_path', - 'site_configuration', 'Future', 'FUTURE', - 'IrisDeprecation'] +__all__ = [ + "load", + "load_cube", + "load_cubes", + "load_raw", + "save", + "Constraint", + "AttributeConstraint", + "NameConstraint", + "sample_data_path", + "site_configuration", + "Future", + "FUTURE", + "IrisDeprecation", +] 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 18b7fb1f54c..c11e517e015 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -52,7 +52,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. @@ -140,7 +141,9 @@ def _coordless_match(self, cube): """ match = True if self._name: - match = self._name == cube.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) return match @@ -454,7 +457,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 @@ -477,4 +480,104 @@ def _cube_func(self, cube): return match def __repr__(self): - return 'AttributeConstraint(%r)' % self._attributes + return "AttributeConstraint(%r)" % self._attributes + + +class NameConstraint(Constraint): + """Provides a simple Cube name based :class:`Constraint`.""" + + def __init__( + self, + standard_name="none", + long_name="none", + var_name="none", + 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. + + Returns: + * Boolean + + Example usage:: + + 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) + + """ + 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): + def matcher(target, value): + if callable(value): + 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: + expected = getattr(self, name) + if expected != "none": + if name == "STASH": + actual = cube.attributes.get(name) + else: + actual = getattr(cube, name) + match = matcher(actual, expected) + # Make this is a short-circuit match. + if match is False: + break + + return match + + 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)) diff --git a/lib/iris/_cube_coord_common.py b/lib/iris/_cube_coord_common.py index 6225b6f64c8..a04c5aa1836 100644 --- a/lib/iris/_cube_coord_common.py +++ b/lib/iris/_cube_coord_common.py @@ -19,6 +19,8 @@ from six.moves import (filter, input, map, range, zip) # noqa import six + +from collections import namedtuple import re import string @@ -31,6 +33,30 @@ _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 @@ -177,6 +203,22 @@ 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 = 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) + 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 fd74bf8e532..8ea8d668941 100644 --- a/lib/iris/tests/test_constraints.py +++ b/lib/iris/tests/test_constraints.py @@ -29,6 +29,7 @@ import datetime import iris +from iris import AttributeConstraint, NameConstraint import iris.tests.stock as stock @@ -282,6 +283,289 @@ def load_match(self, files, constraints): return cubes +@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) + 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... + 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.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.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.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.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. + 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.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) + 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... + 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.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + constraint = NameConstraint(standard_name=self.standard_name) + 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.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.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + constraint = NameConstraint(long_name=self.long_name) + 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 is not None + and item.startswith("air pot") + ) + constraint = NameConstraint(**kwargs) + 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.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + constraint = NameConstraint(var_name=self.var_name) + 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.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.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + constraint = NameConstraint(STASH=self.stash) + 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.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.cubes.extract(constraint, strict=True) + self.assertIsNotNone(result) + self.assertEqual(result.standard_name, self.standard_name) + + # No match - var_name. + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name="wibble", + ) + result = self.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + ) + 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( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH="m01s00i444", + ) + result = self.cubes.extract(constraint) + self.assertFalse(result) + + # Match. + constraint = NameConstraint( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH=self.stash, + ) + 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( + standard_name="wibble", + long_name=self.long_name, + var_name=self.var_name, + STASH=self.stash, + ) + result = self.cubes.extract(constraint) + self.assertFalse(result) + + def test_unknown(self): + # 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.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 class TestCubeExtract(TestMixin, tests.IrisTest): def setUp(self): @@ -289,27 +573,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')) - self.assertCML(cube, ('constrained_load', 'attribute_constraint.cml')) + 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'))) - self.assertCML(cube, ('constrained_load', 'attribute_constraint.cml')) + 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): @@ -329,7 +622,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/constraints/__init__.py b/lib/iris/tests/unit/constraints/__init__.py new file mode 100644 index 00000000000..f988e749378 --- /dev/null +++ b/lib/iris/tests/unit/constraints/__init__.py @@ -0,0 +1,9 @@ +# Copyright Iris contributors +# +# 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 +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 00000000000..553f4ca7a61 --- /dev/null +++ b/lib/iris/tests/unit/constraints/test_NameConstraint.py @@ -0,0 +1,232 @@ +# Copyright Iris contributors +# +# 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 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=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(standard_name=None, long_name=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( + 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) + + 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( + 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) + 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(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(standard_name="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( + 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( + standard_name=None, long_name=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( + 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( + standard_name=None, long_name=None, var_name=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( + 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( + standard_name=None, long_name=None, var_name=None, STASH=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(standard_name=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( + 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) + + 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( + 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 + ) + 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( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + STASH=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 9366bb848a3..dd2311f024d 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py @@ -145,6 +145,52 @@ 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, None) + 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) + 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) + 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) + result = self.cf_var.names + 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): cf_var = CFVariableMixin()