diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index b51bfba76a..8326bdccb4 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -2772,7 +2772,6 @@ def __init__( raise ValueError(msg) # Get the 'coord identity' metadata from the relevant node-coordinate. - # N.B. mesh.coord returns a dict node_coord = self.mesh.coord(include_nodes=True, axis=self.axis) # Call parent constructor to handle the common constructor args. super().__init__( @@ -2897,6 +2896,56 @@ def __eq__(self, other): return eq + def _string_summary(self, repr_style): + # Note: bypass the immediate parent here, which is Coord, because we + # have no interest in reporting coord_system or climatological, or in + # printing out our points/bounds. + # We also want to list our defining properties, i.e. mesh/location/axis + # *first*, before names/units etc, so different from other Coord types. + + # First construct a shortform text summary to identify the Mesh. + # IN 'str-mode', this attempts to use Mesh.name() if it is set, + # otherwise uses an object-id style (as also for 'repr-mode'). + # TODO: use a suitable method provided by Mesh, e.g. something like + # "Mesh.summary(shorten=True)", when it is available. + mesh_name = None + if not repr_style: + mesh_name = self.mesh.name() + if mesh_name in (None, "", "unknown"): + mesh_name = None + if mesh_name: + # Use a more human-readable form + mesh_string = f"Mesh({mesh_name!r})" + else: + # Mimic the generic object.__str__ style. + mesh_id = id(self.mesh) + mesh_string = f"" + result = ( + f"mesh={mesh_string}" + f", location={self.location!r}" + f", axis={self.axis!r}" + ) + # Add 'other' metadata that is drawn from the underlying node-coord. + # But put these *afterward*, unlike other similar classes. + for item in ("standard_name", "units", "long_name", "attributes"): + # NOTE: order of these matches Coord.summary, but omit var_name. + val = getattr(self, item, None) + if item == "attributes": + is_blank = len(val) == 0 # an empty dict is as good as none + else: + is_blank = val is None + if not is_blank: + result += f", {item}={val!r}" + + result = f"MeshCoord({result})" + return result + + def __str__(self): + return self._string_summary(repr_style=False) + + def __repr__(self): + return self._string_summary(repr_style=True) + def _construct_access_arrays(self): """ Build lazy points and bounds arrays, providing dynamic access via the diff --git a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py index 6c46c178c6..5a40bdcba0 100644 --- a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py +++ b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py @@ -42,8 +42,10 @@ def _create_test_mesh(): node_x = AuxCoord( 1100 + np.arange(_TEST_N_NODES), standard_name="longitude", + units="degrees_east", long_name="long-name", - var_name="var", + var_name="var-name", + attributes={"a": 1, "b": "c"}, ) node_y = AuxCoord( 1200 + np.arange(_TEST_N_NODES), standard_name="latitude" @@ -331,6 +333,101 @@ def test_fail_slice_part(self): meshcoord[:1] +class Test__str_repr(tests.IrisTest): + def setUp(self): + mesh = _create_test_mesh() + self.mesh = mesh + # Give mesh itself a name: makes a difference between str and repr. + self.mesh.rename("test_mesh") + self.meshcoord = _create_test_meshcoord(mesh=mesh) + + def _expected_elements_regexp( + self, + mesh_strstyle=True, + standard_name=True, + long_name=True, + attributes=True, + ): + regexp = r"^MeshCoord\(mesh=" + if mesh_strstyle: + regexp += r"Mesh\('test_mesh'\)" + else: + regexp += "" + regexp += ", location='face', axis='x'" + if standard_name: + regexp += ", standard_name='longitude'" + regexp += r", units=Unit\('degrees_east'\)" + if long_name: + regexp += ", long_name='long-name'" + if attributes: + regexp += r", attributes={'a': 1, 'b': 'c'}" + regexp += r"\)$" + return regexp + + def test_repr(self): + result = repr(self.meshcoord) + re_expected = self._expected_elements_regexp(mesh_strstyle=False) + self.assertRegex(result, re_expected) + + def test__str__(self): + result = str(self.meshcoord) + re_expected = self._expected_elements_regexp(mesh_strstyle=True) + self.assertRegex(result, re_expected) + + def test_alternative_location_and_axis(self): + meshcoord = _create_test_meshcoord( + mesh=self.mesh, location="edge", axis="y" + ) + result = str(meshcoord) + re_expected = r", location='edge', axis='y'" + self.assertRegex(result, re_expected) + + def test_str_no_long_name(self): + mesh = self.mesh + # Remove the long_name of the node coord in the mesh. + node_coord = mesh.coord(include_nodes=True, axis="x") + node_coord.long_name = None + # Make a new meshcoord, based on the modified mesh. + meshcoord = _create_test_meshcoord(mesh=self.mesh) + result = str(meshcoord) + re_expected = self._expected_elements_regexp(long_name=False) + self.assertRegex(result, re_expected) + + def test_str_no_standard_name(self): + mesh = self.mesh + # Remove the standard_name of the node coord in the mesh. + node_coord = mesh.coord(include_nodes=True, axis="x") + node_coord.standard_name = None + node_coord.axis = "x" # This is required : but it's a kludge !! + # Make a new meshcoord, based on the modified mesh. + meshcoord = _create_test_meshcoord(mesh=self.mesh) + result = str(meshcoord) + re_expected = self._expected_elements_regexp(standard_name=False) + self.assertRegex(result, re_expected) + + def test_str_no_attributes(self): + mesh = self.mesh + # No attributes on the node coord in the mesh. + node_coord = mesh.coord(include_nodes=True, axis="x") + node_coord.attributes = None + # Make a new meshcoord, based on the modified mesh. + meshcoord = _create_test_meshcoord(mesh=self.mesh) + result = str(meshcoord) + re_expected = self._expected_elements_regexp(attributes=False) + self.assertRegex(result, re_expected) + + def test_str_empty_attributes(self): + mesh = self.mesh + # Empty attributes dict on the node coord in the mesh. + node_coord = mesh.coord(include_nodes=True, axis="x") + node_coord.attributes.clear() + # Make a new meshcoord, based on the modified mesh. + meshcoord = _create_test_meshcoord(mesh=self.mesh) + result = str(meshcoord) + re_expected = self._expected_elements_regexp(attributes=False) + self.assertRegex(result, re_expected) + + class Test_cube_containment(tests.IrisTest): # Check that we can put a MeshCoord into a cube, and have it behave just # like a regular AuxCoord.