From 7085a55419e0707ca7739db4528f28763b886a02 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 11 Aug 2025 12:31:14 +0100 Subject: [PATCH 1/9] Convert test_Connectivity to PyTest. --- .../unit/mesh/components/test_Connectivity.py | 183 ++++++++---------- 1 file changed, 78 insertions(+), 105 deletions(-) diff --git a/lib/iris/tests/unit/mesh/components/test_Connectivity.py b/lib/iris/tests/unit/mesh/components/test_Connectivity.py index de8d7de3d7..6bff5e74fe 100644 --- a/lib/iris/tests/unit/mesh/components/test_Connectivity.py +++ b/lib/iris/tests/unit/mesh/components/test_Connectivity.py @@ -4,23 +4,22 @@ # See LICENSE in the root of the repository for full licensing details. """Unit tests for the :class:`iris.mesh.Connectivity` class.""" -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - from platform import python_version from xml.dom import minidom import numpy as np from numpy import ma from packaging import version +import pytest from iris._lazy_data import as_lazy_data, is_lazy_data from iris.mesh import Connectivity +from iris.tests import _shared_utils -class TestStandard(tests.IrisTest): - def setUp(self): +class TestStandard: + @pytest.fixture(autouse=True) + def _setup(self): # Crete an instance, with non-default arguments to allow testing of # correct property setting. self.kwargs = { @@ -35,24 +34,26 @@ def setUp(self): self.connectivity = Connectivity(**self.kwargs) def test_cf_role(self): - self.assertEqual(self.kwargs["cf_role"], self.connectivity.cf_role) + assert self.kwargs["cf_role"] == self.connectivity.cf_role def test_location(self): expected = self.kwargs["cf_role"].split("_")[0] - self.assertEqual(expected, self.connectivity.location) + assert expected == self.connectivity.location def test_connected(self): expected = self.kwargs["cf_role"].split("_")[1] - self.assertEqual(expected, self.connectivity.connected) + assert expected == self.connectivity.connected def test_start_index(self): - self.assertEqual(self.kwargs["start_index"], self.connectivity.start_index) + assert self.kwargs["start_index"] == self.connectivity.start_index def test_location_axis(self): - self.assertEqual(self.kwargs["location_axis"], self.connectivity.location_axis) + assert self.kwargs["location_axis"] == self.connectivity.location_axis def test_indices(self): - self.assertArrayEqual(self.kwargs["indices"], self.connectivity.indices) + _shared_utils.assert_array_equal( + self.kwargs["indices"], self.connectivity.indices + ) def test_read_only(self): attributes = ("indices", "cf_role", "start_index", "location_axis") @@ -61,37 +62,33 @@ def test_read_only(self): else: msg = "can't set attribute" for attribute in attributes: - self.assertRaisesRegex( - AttributeError, - msg, - setattr, - self.connectivity, - attribute, - 1, - ) + with pytest.raises(AttributeError, match=msg): + setattr(self.connectivity, attribute, 1) def test_transpose(self): expected_dim = 1 - self.kwargs["location_axis"] expected_indices = self.kwargs["indices"].transpose() new_connectivity = self.connectivity.transpose() - self.assertEqual(expected_dim, new_connectivity.location_axis) - self.assertArrayEqual(expected_indices, new_connectivity.indices) + assert expected_dim == new_connectivity.location_axis + _shared_utils.assert_array_equal(expected_indices, new_connectivity.indices) def test_lazy_indices(self): - self.assertTrue(is_lazy_data(self.connectivity.lazy_indices())) + assert is_lazy_data(self.connectivity.lazy_indices()) def test_core_indices(self): - self.assertArrayEqual(self.kwargs["indices"], self.connectivity.core_indices()) + _shared_utils.assert_array_equal( + self.kwargs["indices"], self.connectivity.core_indices() + ) def test_has_lazy_indices(self): - self.assertFalse(self.connectivity.has_lazy_indices()) + assert not self.connectivity.has_lazy_indices() def test_lazy_location_lengths(self): - self.assertTrue(is_lazy_data(self.connectivity.lazy_location_lengths())) + assert is_lazy_data(self.connectivity.lazy_location_lengths()) def test_location_lengths(self): expected = [4, 4, 4] - self.assertArrayEqual(expected, self.connectivity.location_lengths()) + _shared_utils.assert_array_equal(expected, self.connectivity.location_lengths()) def test___str__(self): expected = "\n".join( @@ -113,67 +110,70 @@ def test___str__(self): " location_axis: 1", ] ) - self.assertEqual(expected, self.connectivity.__str__()) + assert expected == self.connectivity.__str__() def test___repr__(self): expected = ( "" ) - self.assertEqual(expected, self.connectivity.__repr__()) + assert expected == self.connectivity.__repr__() def test_xml_element(self): doc = minidom.Document() connectivity_element = self.connectivity.xml_element(doc) - self.assertEqual(connectivity_element.tagName, "connectivity") + assert connectivity_element.tagName == "connectivity" for attribute in ("cf_role", "start_index", "location_axis"): - self.assertIn(attribute, connectivity_element.attributes) + assert attribute in connectivity_element.attributes def test___eq__(self): equivalent_kwargs = self.kwargs equivalent_kwargs["indices"] = self.kwargs["indices"].transpose() equivalent_kwargs["location_axis"] = 1 - self.kwargs["location_axis"] equivalent = Connectivity(**equivalent_kwargs) - self.assertFalse(np.array_equal(equivalent.indices, self.connectivity.indices)) - self.assertEqual(equivalent, self.connectivity) + assert not np.array_equal(equivalent.indices, self.connectivity.indices) + assert equivalent == self.connectivity def test_different(self): different_kwargs = self.kwargs different_kwargs["indices"] = self.kwargs["indices"].transpose() different = Connectivity(**different_kwargs) - self.assertNotEqual(different, self.connectivity) + assert different != self.connectivity def test_no_cube_dims(self): - self.assertRaises(NotImplementedError, self.connectivity.cube_dims, 1) + pytest.raises(NotImplementedError, self.connectivity.cube_dims, 1) def test_shape(self): - self.assertEqual(self.kwargs["indices"].shape, self.connectivity.shape) + assert self.kwargs["indices"].shape == self.connectivity.shape def test_ndim(self): - self.assertEqual(self.kwargs["indices"].ndim, self.connectivity.ndim) + assert self.kwargs["indices"].ndim == self.connectivity.ndim def test___getitem_(self): subset = self.connectivity[:, 0:1] - self.assertArrayEqual(self.kwargs["indices"][:, 0:1], subset.indices) + _shared_utils.assert_array_equal(self.kwargs["indices"][:, 0:1], subset.indices) def test_copy(self): new_indices = np.linspace(11, 16, 6, dtype=int).reshape((3, -1)) copy_connectivity = self.connectivity.copy(new_indices) - self.assertArrayEqual(new_indices, copy_connectivity.indices) + _shared_utils.assert_array_equal(new_indices, copy_connectivity.indices) def test_indices_by_location(self): expected = self.kwargs["indices"].transpose() - self.assertArrayEqual(expected, self.connectivity.indices_by_location()) + _shared_utils.assert_array_equal( + expected, self.connectivity.indices_by_location() + ) def test_indices_by_location_input(self): expected = as_lazy_data(self.kwargs["indices"].transpose()) by_location = self.connectivity.indices_by_location( self.connectivity.lazy_indices() ) - self.assertArrayEqual(expected, by_location) + _shared_utils.assert_array_equal(expected, by_location) -class TestAltIndices(tests.IrisTest): - def setUp(self): +class TestAltIndices: + @pytest.fixture(autouse=True) + def _setup(self): mask = ([0, 0, 0, 0, 1] * 2) + [0, 0, 0, 1, 1] data = np.linspace(1, 15, 15, dtype=int).reshape((-1, 5)) self.masked_indices = ma.array(data=data, mask=mask) @@ -181,7 +181,7 @@ def setUp(self): def common(self, indices): connectivity = Connectivity(indices=indices, cf_role="face_node_connectivity") - self.assertArrayEqual(indices, connectivity.indices) + _shared_utils.assert_array_equal(indices, connectivity.indices) def test_int32(self): indices = np.linspace(1, 9, 9, dtype=np.int32).reshape((-1, 3)) @@ -204,19 +204,18 @@ def test_has_lazy_indices(self): connectivity = Connectivity( indices=self.lazy_indices, cf_role="face_node_connectivity" ) - self.assertTrue(connectivity.has_lazy_indices()) + assert connectivity.has_lazy_indices() -class TestValidations(tests.IrisTest): +class TestValidations: def test_start_index(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((-1, 3)), "cf_role": "face_node_connectivity", "start_index": 2, } - self.assertRaisesRegex( - ValueError, "Invalid start_index .", Connectivity, **kwargs - ) + with pytest.raises(ValueError, match="Invalid start_index ."): + Connectivity(**kwargs) def test_location_axis(self): kwargs = { @@ -224,101 +223,84 @@ def test_location_axis(self): "cf_role": "face_node_connectivity", "location_axis": 2, } - self.assertRaisesRegex( - ValueError, "Invalid location_axis .", Connectivity, **kwargs - ) + with pytest.raises(ValueError, match="Invalid location_axis ."): + Connectivity(**kwargs) def test_cf_role(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((-1, 3)), "cf_role": "error", } - self.assertRaisesRegex(ValueError, "Invalid cf_role .", Connectivity, **kwargs) + with pytest.raises(ValueError, match="Invalid cf_role ."): + Connectivity(**kwargs) def test_indices_int(self): kwargs = { "indices": np.linspace(1, 9, 9).reshape((-1, 3)), "cf_role": "face_node_connectivity", } - self.assertRaisesRegex( - ValueError, - "dtype must be numpy integer subtype", - Connectivity, - **kwargs, - ) + with pytest.raises(ValueError, match="dtype must be numpy integer subtype"): + Connectivity(**kwargs) def test_indices_start_index(self): kwargs = { "indices": np.linspace(-9, -1, 9, dtype=int).reshape((-1, 3)), "cf_role": "face_node_connectivity", } - self.assertRaisesRegex(ValueError, " < start_index", Connectivity, **kwargs) + with pytest.raises(ValueError, match="< start_index"): + Connectivity(**kwargs) def test_indices_dims_low(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int), "cf_role": "face_node_connectivity", } - self.assertRaisesRegex( - ValueError, "Expected 2-dimensional shape,", Connectivity, **kwargs - ) + with pytest.raises(ValueError, match="Expected 2-dimensional shape,"): + Connectivity(**kwargs) def test_indices_dims_high(self): kwargs = { "indices": np.linspace(1, 12, 12, dtype=int).reshape((-1, 3, 2)), "cf_role": "face_node_connectivity", } - self.assertRaisesRegex( - ValueError, "Expected 2-dimensional shape,", Connectivity, **kwargs - ) + with pytest.raises(ValueError, match="Expected 2-dimensional shape,"): + Connectivity(**kwargs) def test_indices_locations_edge(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((-1, 3)), "cf_role": "edge_node_connectivity", } - self.assertRaisesRegex( - ValueError, - "Not all edges meet requirement: len=2", - Connectivity, - **kwargs, - ) + with pytest.raises(ValueError, match="Not all edges meet requirement: len=2"): + Connectivity(**kwargs) def test_indices_locations_face(self): kwargs = { "indices": np.linspace(1, 6, 6, dtype=int).reshape((-1, 2)), "cf_role": "face_node_connectivity", } - self.assertRaisesRegex( - ValueError, - "Not all faces meet requirement: len>=3", - Connectivity, - **kwargs, - ) + with pytest.raises(ValueError, match="Not all faces meet requirement: len>=3"): + Connectivity(**kwargs) def test_indices_locations_volume_face(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((-1, 3)), "cf_role": "volume_face_connectivity", } - self.assertRaisesRegex( - ValueError, - "Not all volumes meet requirement: len>=4", - Connectivity, - **kwargs, - ) + with pytest.raises( + ValueError, match="Not all volumes meet requirement: len>=4" + ): + Connectivity(**kwargs) def test_indices_locations_volume_edge(self): kwargs = { "indices": np.linspace(1, 12, 12, dtype=int).reshape((-1, 3)), "cf_role": "volume_edge_connectivity", } - self.assertRaisesRegex( - ValueError, - "Not all volumes meet requirement: len>=6", - Connectivity, - **kwargs, - ) + with pytest.raises( + ValueError, match="Not all volumes meet requirement: len>=6" + ): + Connectivity(**kwargs) def test_indices_locations_alt_dim(self): """The transposed equivalent of `test_indices_locations_volume_face`.""" @@ -327,12 +309,10 @@ def test_indices_locations_alt_dim(self): "cf_role": "volume_face_connectivity", "location_axis": 1, } - self.assertRaisesRegex( - ValueError, - "Not all volumes meet requirement: len>=4", - Connectivity, - **kwargs, - ) + with pytest.raises( + ValueError, match="Not all volumes meet requirement: len>=4" + ): + Connectivity(**kwargs) def test_indices_locations_masked(self): mask = ([0, 0, 0] * 2) + [0, 0, 1] @@ -344,12 +324,5 @@ def test_indices_locations_masked(self): # Validation of individual location sizes (denoted by masks) only # available through explicit call of Connectivity.validate_indices(). connectivity = Connectivity(**kwargs) - self.assertRaisesRegex( - ValueError, - "Not all faces meet requirement: len>=3", - connectivity.validate_indices, - ) - - -if __name__ == "__main__": - tests.main() + with pytest.raises(ValueError, match="Not all faces meet requirement: len>=3"): + connectivity.validate_indices() From f2f65b6c3ca8eebdf665668787a11cc104d20990 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 11 Aug 2025 15:48:19 +0100 Subject: [PATCH 2/9] Extra fix in test_Connectivity. --- lib/iris/tests/unit/mesh/components/test_Connectivity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/mesh/components/test_Connectivity.py b/lib/iris/tests/unit/mesh/components/test_Connectivity.py index 6bff5e74fe..8b1bfe2bb2 100644 --- a/lib/iris/tests/unit/mesh/components/test_Connectivity.py +++ b/lib/iris/tests/unit/mesh/components/test_Connectivity.py @@ -140,7 +140,8 @@ def test_different(self): assert different != self.connectivity def test_no_cube_dims(self): - pytest.raises(NotImplementedError, self.connectivity.cube_dims, 1) + with pytest.raises(NotImplementedError): + self.connectivity.cube_dims(1) def test_shape(self): assert self.kwargs["indices"].shape == self.connectivity.shape From 5c75316fc687a043ef71e6d0307672af42e46b60 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 11 Aug 2025 15:55:55 +0100 Subject: [PATCH 3/9] Convert test_MeshXY to PyTest. --- lib/iris/mesh/__init__.py | 4 +- lib/iris/tests/_shared_utils.py | 26 +- .../tests/unit/mesh/components/test_MeshXY.py | 569 ++++++++---------- 3 files changed, 269 insertions(+), 330 deletions(-) diff --git a/lib/iris/mesh/__init__.py b/lib/iris/mesh/__init__.py index 9a2c10b7ca..ff530a4abd 100644 --- a/lib/iris/mesh/__init__.py +++ b/lib/iris/mesh/__init__.py @@ -26,4 +26,6 @@ ] # Configure the logger as a root logger. -logger = get_logger(__name__, fmt="[%(cls)s.%(funcName)s]", level="NOTSET") +logger = get_logger( + __name__, fmt="[%(cls)s.%(funcName)s]", level="NOTSET", propagate=True +) diff --git a/lib/iris/tests/_shared_utils.py b/lib/iris/tests/_shared_utils.py index fc2998c5f8..25d39d7a74 100644 --- a/lib/iris/tests/_shared_utils.py +++ b/lib/iris/tests/_shared_utils.py @@ -598,19 +598,21 @@ def assert_logs(caplog, logger=None, level=None, msg_regex=None): just to check that there are no formatting errors. """ + caplog_count = len(caplog.records) with caplog.at_level(level, logger.name): - assert len(caplog.records) != 0 - # Check for any formatting errors by running all the formatters. - for record in caplog.records: - for handler in caplog.logger.handlers: - handler.format(record) - - # Check message, if requested. - if msg_regex: - assert len(caplog.records) == 1 - rec = caplog.records[0] - assert level == rec.levelname - assert re.match(msg_regex, rec.msg) + yield + + assert len(caplog.records) > caplog_count + # Check for any formatting errors by running all the formatters. + for record in caplog.records: + # for handler in caplog.logger.handlers: + caplog.handler.format(record) + + # Check message, if requested. + if msg_regex: + rec = caplog.records[-1] + assert level == rec.levelname + assert re.match(msg_regex, rec.msg) @contextlib.contextmanager diff --git a/lib/iris/tests/unit/mesh/components/test_MeshXY.py b/lib/iris/tests/unit/mesh/components/test_MeshXY.py index abb294f8d8..0062c9a674 100644 --- a/lib/iris/tests/unit/mesh/components/test_MeshXY.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshXY.py @@ -4,24 +4,24 @@ # See LICENSE in the root of the repository for full licensing details. """Unit tests for the :class:`iris.mesh.MeshXY` class.""" -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip +import re import numpy as np +import pytest from iris.common.metadata import MeshMetadata from iris.coords import AuxCoord from iris.exceptions import ConnectivityNotFoundError, CoordinateNotFoundError from iris.mesh import components from iris.mesh.components import logger +from iris.tests import _shared_utils -class TestMeshCommon(tests.IrisTest): - @classmethod - def setUpClass(cls): - # A collection of minimal coords and connectivities describing an - # equilateral triangle. +class TestMeshCommon: + @staticmethod + @pytest.fixture(scope="class", autouse=True) + def make_common_inputs(): + cls = TestMeshCommon cls.NODE_LON = AuxCoord( [0, 2, 1], standard_name="longitude", @@ -69,11 +69,12 @@ def setUpClass(cls): class TestProperties1D(TestMeshCommon): # Tests that can reuse a single instance for greater efficiency. - @classmethod - def setUpClass(cls): - super().setUpClass() + @staticmethod + @pytest.fixture(scope="class", autouse=True) + def _setup_class_1d(make_common_inputs): # MeshXY kwargs with topology_dimension=1 and all applicable # arguments populated - this tests correct property setting. + cls = TestProperties1D cls.kwargs = { "topology_dimension": 1, "node_coords_and_axes": ((cls.NODE_LON, "x"), (cls.NODE_LAT, "y")), @@ -88,10 +89,7 @@ def setUpClass(cls): cls.mesh = components.MeshXY(**cls.kwargs) def test__metadata_manager(self): - self.assertEqual( - self.mesh._metadata_manager.cls.__name__, - MeshMetadata.__name__, - ) + assert self.mesh._metadata_manager.cls.__name__ == MeshMetadata.__name__ def test___getstate__(self): expected = ( @@ -99,11 +97,11 @@ def test___getstate__(self): self.mesh._coord_manager, self.mesh._connectivity_manager, ) - self.assertEqual(expected, self.mesh.__getstate__()) + assert expected == self.mesh.__getstate__() def test___repr__(self): expected = "" - self.assertEqual(expected, repr(self.mesh)) + assert expected == repr(self.mesh) def test___str__(self): expected = [ @@ -128,20 +126,20 @@ def test___str__(self): " attributes:", " notes 'this is a test'", ] - self.assertEqual(expected, str(self.mesh).split("\n")) + assert expected == str(self.mesh).split("\n") def test___eq__(self): # The dimension names do not participate in equality. equivalent_kwargs = self.kwargs.copy() equivalent_kwargs["node_dimension"] = "something_else" equivalent = components.MeshXY(**equivalent_kwargs) - self.assertEqual(equivalent, self.mesh) + assert equivalent == self.mesh def test_different(self): different_kwargs = self.kwargs.copy() different_kwargs["long_name"] = "new_name" different = components.MeshXY(**different_kwargs) - self.assertNotEqual(different, self.mesh) + assert different != self.mesh different_kwargs = self.kwargs.copy() ncaa = self.kwargs["node_coords_and_axes"] @@ -149,35 +147,36 @@ def test_different(self): new_ncaa = (ncaa[0], (new_lat, "y")) different_kwargs["node_coords_and_axes"] = new_ncaa different = components.MeshXY(**different_kwargs) - self.assertNotEqual(different, self.mesh) + assert different != self.mesh different_kwargs = self.kwargs.copy() conns = self.kwargs["connectivities"] new_conn = conns[0].copy(conns[0].indices + 1) different_kwargs["connectivities"] = new_conn different = components.MeshXY(**different_kwargs) - self.assertNotEqual(different, self.mesh) + assert different != self.mesh def test_all_connectivities(self): expected = components.Mesh1DConnectivities(self.EDGE_NODE) - self.assertEqual(expected, self.mesh.all_connectivities) + assert expected == self.mesh.all_connectivities def test_all_coords(self): expected = components.Mesh1DCoords( self.NODE_LON, self.NODE_LAT, self.EDGE_LON, self.EDGE_LAT ) - self.assertEqual(expected, self.mesh.all_coords) + assert expected == self.mesh.all_coords def test_boundary_node(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = self.mesh.boundary_node_connectivity def test_cf_role(self): - self.assertEqual("mesh_topology", self.mesh.cf_role) + assert "mesh_topology" == self.mesh.cf_role # Read only. - self.assertRaises(AttributeError, setattr, self.mesh.cf_role, "foo", 1) + with pytest.raises(AttributeError): + self.mesh.cf_role = "foo" - def test_connectivities(self): + def test_connectivities(self, mocker): # General results. Method intended for inheritance. positive_kwargs = ( {"item": self.EDGE_NODE}, @@ -188,7 +187,7 @@ def test_connectivities(self): {"cf_role": "edge_node_connectivity"}, ) - fake_connectivity = tests.mock.Mock( + fake_connectivity = mocker.Mock( __class__=components.Connectivity, cf_role="fake" ) negative_kwargs = ( @@ -203,11 +202,11 @@ def test_connectivities(self): func = self.mesh.connectivities for kwargs in positive_kwargs: - self.assertEqual([self.EDGE_NODE], func(**kwargs)) + assert [self.EDGE_NODE] == func(**kwargs) for kwargs in negative_kwargs: - self.assertEqual([], func(**kwargs)) + assert [] == func(**kwargs) - def test_connectivities_elements(self): + def test_connectivities_elements(self, caplog): # topology_dimension-specific results. Method intended to be overridden. positive_kwargs = ( {"contains_node": True}, @@ -223,23 +222,26 @@ def test_connectivities_elements(self): func = self.mesh.connectivities for kwargs in positive_kwargs: - self.assertEqual([self.EDGE_NODE], func(**kwargs)) + assert [self.EDGE_NODE] == func(**kwargs) for kwargs in negative_kwargs: - self.assertEqual([], func(**kwargs)) + assert [] == func(**kwargs) log_regex = r".*filter for non-existent.*" - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): - self.assertEqual([], func(contains_face=True)) + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): + assert [] == func(contains_face=True) def test_coord(self): # See MeshXY.coords tests for thorough coverage of cases. func = self.mesh.coord exception = CoordinateNotFoundError - self.assertRaisesRegex(exception, ".*but found 2", func, location="node") - self.assertRaisesRegex(exception, ".*but found none", func, axis="t") - self.assertRaisesRegex( - ValueError, "Expected location.*got `foo`", func, location="foo" - ) + with pytest.raises(exception, match=".*but found 2"): + func(location="node") + with pytest.raises(exception, match=".*but found none"): + func(axis="t") + with pytest.raises(ValueError, match="Expected location.*got `foo`"): + func(location="foo") def test_coords(self): # General results. Method intended for inheritance. @@ -266,15 +268,14 @@ def test_coords(self): func = self.mesh.coords for kwargs in positive_kwargs: - self.assertIn(self.NODE_LON, func(**kwargs)) + assert self.NODE_LON in func(**kwargs) for kwargs in negative_kwargs: - self.assertNotIn(self.NODE_LON, func(**kwargs)) + assert self.NODE_LON not in func(**kwargs) - self.assertRaisesRegex( - ValueError, "Expected location.*got.*foo", self.mesh.coords, location="foo" - ) + with pytest.raises(ValueError, match="Expected location.*got.*foo"): + self.mesh.coords(location="foo") - def test_coords_elements(self): + def test_coords_elements(self, caplog): # topology_dimension-specific results. Method intended to be overridden. all_expected = { "node_x": self.NODE_LON, @@ -294,67 +295,67 @@ def test_coords_elements(self): func = self.mesh.coords for kwargs, expected in kwargs_expected: expected = [all_expected[k] for k in expected if k in all_expected] - self.assertEqual(expected, func(**kwargs)) + assert expected == func(**kwargs) log_regex = r".*filter non-existent.*" - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): - self.assertEqual([], func(location="face")) + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): + assert [] == func(location="face") def test_edge_dimension(self): - self.assertEqual(self.kwargs["edge_dimension"], self.mesh.edge_dimension) + assert self.kwargs["edge_dimension"] == self.mesh.edge_dimension def test_edge_coords(self): expected = components.MeshEdgeCoords(self.EDGE_LON, self.EDGE_LAT) - self.assertEqual(expected, self.mesh.edge_coords) + assert expected == self.mesh.edge_coords def test_edge_face(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = self.mesh.edge_face_connectivity def test_edge_node(self): - self.assertEqual(self.EDGE_NODE, self.mesh.edge_node_connectivity) + assert self.EDGE_NODE == self.mesh.edge_node_connectivity def test_face_coords(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = self.mesh.face_coords def test_face_dimension(self): - self.assertIsNone(self.mesh.face_dimension) + assert self.mesh.face_dimension is None def test_face_edge(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = self.mesh.face_edge_connectivity def test_face_face(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = self.mesh.face_face_connectivity def test_face_node(self): - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = self.mesh.face_node_connectivity def test_node_coords(self): expected = components.MeshNodeCoords(self.NODE_LON, self.NODE_LAT) - self.assertEqual(expected, self.mesh.node_coords) + assert expected == self.mesh.node_coords def test_node_dimension(self): - self.assertEqual(self.kwargs["node_dimension"], self.mesh.node_dimension) + assert self.kwargs["node_dimension"] == self.mesh.node_dimension def test_topology_dimension(self): - self.assertEqual( - self.kwargs["topology_dimension"], self.mesh.topology_dimension - ) + assert self.kwargs["topology_dimension"] == self.mesh.topology_dimension # Read only. - self.assertRaises( - AttributeError, setattr, self.mesh.topology_dimension, "foo", 1 - ) + with pytest.raises(AttributeError): + self.mesh.topology_dimension = "foo" class TestProperties2D(TestProperties1D): # Additional/specialised tests for topology_dimension=2. - @classmethod - def setUpClass(cls): - super().setUpClass() + @staticmethod + @pytest.fixture(scope="class", autouse=True) + def _setup_class_2d(_setup_class_1d): + cls = TestProperties2D cls.kwargs["topology_dimension"] = 2 cls.kwargs["connectivities"] = ( cls.FACE_NODE, @@ -373,7 +374,7 @@ def setUpClass(cls): def test___repr__(self): expected = "" - self.assertEqual(expected, repr(self.mesh)) + assert expected == repr(self.mesh) def test___str__(self): expected = [ @@ -420,7 +421,7 @@ def test___str__(self): " attributes:", " notes 'this is a test'", ] - self.assertEqual(expected, str(self.mesh).split("\n")) + assert expected == str(self.mesh).split("\n") # Test some different options of the str() operation here. def test___str__noedgecoords(self): @@ -468,7 +469,7 @@ def test___str__noedgecoords(self): " attributes:", " notes 'this is a test'", ] - self.assertEqual(expected, str(alt_mesh).split("\n")) + assert expected == str(alt_mesh).split("\n") def test_all_connectivities(self): expected = components.Mesh2DConnectivities( @@ -479,7 +480,7 @@ def test_all_connectivities(self): self.EDGE_FACE, self.BOUNDARY_NODE, ) - self.assertEqual(expected, self.mesh.all_connectivities) + assert expected == self.mesh.all_connectivities def test_all_coords(self): expected = components.Mesh2DCoords( @@ -490,25 +491,20 @@ def test_all_coords(self): self.FACE_LON, self.FACE_LAT, ) - self.assertEqual(expected, self.mesh.all_coords) + assert expected == self.mesh.all_coords def test_boundary_node(self): - self.assertEqual(self.BOUNDARY_NODE, self.mesh.boundary_node_connectivity) + assert self.BOUNDARY_NODE == self.mesh.boundary_node_connectivity def test_connectivity(self): # See MeshXY.connectivities tests for thorough coverage of cases. # Can only test MeshXY.connectivity for 2D since we need >1 connectivity. func = self.mesh.connectivity exception = ConnectivityNotFoundError - self.assertRaisesRegex(exception, ".*but found 3", func, contains_node=True) - self.assertRaisesRegex( - exception, - ".*but found none", - func, - contains_node=False, - contains_edge=False, - contains_face=False, - ) + with pytest.raises(exception, match=".*but found 3"): + func(contains_node=True) + with pytest.raises(exception, match=".*but found none"): + func(contains_node=False, contains_edge=False, contains_face=False) def test_connectivities_elements(self): kwargs_expected = ( @@ -562,9 +558,9 @@ def test_connectivities_elements(self): func = self.mesh.connectivities for kwargs, expected in kwargs_expected: result = func(**kwargs) - self.assertEqual(len(expected), len(result)) + assert len(expected) == len(result) for item in expected: - self.assertIn(item, result) + assert item in result def test_coords_elements(self): all_expected = { @@ -586,34 +582,35 @@ def test_coords_elements(self): func = self.mesh.coords for kwargs, expected in kwargs_expected: expected = [all_expected[k] for k in expected if k in all_expected] - self.assertEqual(expected, func(**kwargs)) + assert expected == func(**kwargs) def test_edge_face(self): - self.assertEqual(self.EDGE_FACE, self.mesh.edge_face_connectivity) + assert self.EDGE_FACE == self.mesh.edge_face_connectivity def test_face_coords(self): expected = components.MeshFaceCoords(self.FACE_LON, self.FACE_LAT) - self.assertEqual(expected, self.mesh.face_coords) + assert expected == self.mesh.face_coords def test_face_dimension(self): - self.assertEqual(self.kwargs["face_dimension"], self.mesh.face_dimension) + assert self.kwargs["face_dimension"] == self.mesh.face_dimension def test_face_edge(self): - self.assertEqual(self.FACE_EDGE, self.mesh.face_edge_connectivity) + assert self.FACE_EDGE == self.mesh.face_edge_connectivity def test_face_face(self): - self.assertEqual(self.FACE_FACE, self.mesh.face_face_connectivity) + assert self.FACE_FACE == self.mesh.face_face_connectivity def test_face_node(self): - self.assertEqual(self.FACE_NODE, self.mesh.face_node_connectivity) + assert self.FACE_NODE == self.mesh.face_node_connectivity class Test__str__various(TestMeshCommon): # Some extra testing for the str() operation : based on 1D meshes as simpler - def setUp(self): - # All the tests here want modified meshes, so use standard setUp to + @pytest.fixture(autouse=True) + def _setup(self, make_common_inputs): + # All the tests here want modified meshes, so use standard set up to # create afresh for each test, allowing them to modify it. - super().setUp() + # MeshXY kwargs with topology_dimension=1 and all applicable # arguments populated - this tests correct property setting. self.kwargs = { @@ -637,26 +634,26 @@ def setUp(self): def test___repr__basic(self): expected = "" - self.assertEqual(expected, repr(self.mesh)) + assert expected == repr(self.mesh) def test___repr__varname(self): self.mesh.long_name = None expected = "" - self.assertEqual(expected, repr(self.mesh)) + assert expected == repr(self.mesh) def test___repr__noname(self): self.mesh.long_name = None self.mesh.var_name = None expected = "" - self.assertRegex(repr(self.mesh), expected) + assert re.search(expected, repr(self.mesh)) def test___str__noattributes(self): self.mesh.attributes = None - self.assertNotIn("attributes", str(self.mesh)) + assert "attributes" not in str(self.mesh) def test___str__emptyattributes(self): self.mesh.attributes.clear() - self.assertNotIn("attributes", str(self.mesh)) + assert "attributes" not in str(self.mesh) def test__str__longstringattribute(self): self.mesh.attributes["long_string"] = ( @@ -668,7 +665,7 @@ def test__str__longstringattribute(self): expected = ( "'long_x_10_long_x_20_long_x_30_long_x_40_long_x_50_long_x_60_long_x_70..." ) - self.assertIn(expected + ":END", result + ":END") + assert expected + ":END" in result + ":END" def test___str__units_stdname(self): # These are usually missing, but they *can* be present. @@ -691,13 +688,14 @@ def test___str__units_stdname(self): " notes 'this is a test'", ] ) - self.assertTrue(result.endswith(expected)) + assert result.endswith(expected) class TestOperations1D(TestMeshCommon): # Tests that cannot reuse an existing MeshXY instance, instead need a new # one each time. - def setUp(self): + @pytest.fixture(autouse=True) + def _setup_1d(self, make_common_inputs): self.mesh = components.MeshXY( topology_dimension=1, node_coords_and_axes=((self.NODE_LON, "x"), (self.NODE_LAT, "y")), @@ -735,9 +733,9 @@ def test___setstate__(self): ) ) - self.assertEqual(false_metadata_manager, self.mesh._metadata_manager) - self.assertEqual(false_coord_manager, self.mesh._coord_manager) - self.assertEqual(false_connectivity_manager, self.mesh._connectivity_manager) + assert false_metadata_manager == self.mesh._metadata_manager + assert false_coord_manager == self.mesh._coord_manager + assert false_connectivity_manager == self.mesh._connectivity_manager def test_add_connectivities(self): # Cannot test ADD - 1D - nothing extra to add beyond minimum. @@ -747,41 +745,33 @@ def test_add_connectivities(self): # with one of different length. edge_node = self.new_connectivity(self.EDGE_NODE, new_len) self.mesh.add_connectivities(edge_node) - self.assertEqual( - components.Mesh1DConnectivities(edge_node), - self.mesh.all_connectivities, + assert ( + components.Mesh1DConnectivities(edge_node) + == self.mesh.all_connectivities ) def test_add_connectivities_duplicates(self): edge_node_one = self.EDGE_NODE edge_node_two = self.new_connectivity(self.EDGE_NODE) self.mesh.add_connectivities(edge_node_one, edge_node_two) - self.assertEqual( - edge_node_two, - self.mesh.edge_node_connectivity, - ) + assert edge_node_two == self.mesh.edge_node_connectivity - def test_add_connectivities_invalid(self): - self.assertRaisesRegex( - TypeError, - "Expected Connectivity.*", - self.mesh.add_connectivities, - "foo", - ) + def test_add_connectivities_invalid(self, caplog): + with pytest.raises(TypeError, match="Expected Connectivity.*"): + self.mesh.add_connectivities("foo") face_node = self.FACE_NODE log_regex = r"Not adding connectivity.*" - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): self.mesh.add_connectivities(face_node) def test_add_coords(self): # ADD coords. edge_kwargs = {"edge_x": self.EDGE_LON, "edge_y": self.EDGE_LAT} self.mesh.add_coords(**edge_kwargs) - self.assertEqual( - components.MeshEdgeCoords(**edge_kwargs), - self.mesh.edge_coords, - ) + assert components.MeshEdgeCoords(**edge_kwargs) == self.mesh.edge_coords for new_shape in (False, True): # REPLACE coords, first with ones of the same shape, then with ones @@ -795,31 +785,19 @@ def test_add_coords(self): "edge_y": self.new_coord(self.EDGE_LAT, new_shape), } self.mesh.add_coords(**node_kwargs, **edge_kwargs) - self.assertEqual( - components.MeshNodeCoords(**node_kwargs), - self.mesh.node_coords, - ) - self.assertEqual( - components.MeshEdgeCoords(**edge_kwargs), - self.mesh.edge_coords, - ) + assert components.MeshNodeCoords(**node_kwargs) == self.mesh.node_coords + assert components.MeshEdgeCoords(**edge_kwargs) == self.mesh.edge_coords def test_add_coords_face(self): - self.assertRaises( - TypeError, - self.mesh.add_coords, - face_x=self.FACE_LON, - face_y=self.FACE_LAT, - ) + with pytest.raises(TypeError): + self.mesh.add_coords(face_x=self.FACE_LON, face_y=self.FACE_LAT) def test_add_coords_invalid(self): func = self.mesh.add_coords - self.assertRaisesRegex( - TypeError, ".*requires to be an 'AuxCoord'.*", func, node_x="foo" - ) - self.assertRaisesRegex( - TypeError, ".*requires a x-axis like.*", func, node_x=self.NODE_LAT - ) + with pytest.raises(TypeError, match=".*requires to be an 'AuxCoord'.*"): + func(node_x="foo") + with pytest.raises(TypeError, match=".*requires a x-axis like.*"): + func(node_x=self.NODE_LAT) climatological = AuxCoord( [0], bounds=[-1, 1], @@ -827,23 +805,18 @@ def test_add_coords_invalid(self): climatological=True, units="Days since 1970", ) - self.assertRaisesRegex( - TypeError, - ".*cannot be a climatological.*", - func, - node_x=climatological, - ) + with pytest.raises(TypeError, match=".*cannot be a climatological.*"): + func(node_x=climatological) wrong_shape = self.NODE_LON.copy([0]) - self.assertRaisesRegex( - ValueError, ".*requires to have shape.*", func, node_x=wrong_shape - ) + with pytest.raises(ValueError, match=".*requires to have shape.*"): + func(node_x=wrong_shape) def test_add_coords_single(self): # ADD coord. edge_x = self.EDGE_LON expected = components.MeshEdgeCoords(edge_x=edge_x, edge_y=None) self.mesh.add_coords(edge_x=edge_x) - self.assertEqual(expected, self.mesh.edge_coords) + assert expected == self.mesh.edge_coords # REPLACE coords. node_x = self.new_coord(self.NODE_LON) @@ -853,8 +826,8 @@ def test_add_coords_single(self): ) expected_edges = components.MeshEdgeCoords(edge_x=edge_x, edge_y=None) self.mesh.add_coords(node_x=node_x, edge_x=edge_x) - self.assertEqual(expected_nodes, self.mesh.node_coords) - self.assertEqual(expected_edges, self.mesh.edge_coords) + assert expected_nodes == self.mesh.node_coords + assert expected_edges == self.mesh.edge_coords # Attempt to REPLACE coords with those of DIFFERENT SHAPE. node_x = self.new_coord(self.NODE_LON, new_shape=True) @@ -863,53 +836,51 @@ def test_add_coords_single(self): edge_kwarg = {"edge_x": edge_x} both_kwargs = dict(**node_kwarg, **edge_kwarg) for kwargs in (node_kwarg, edge_kwarg, both_kwargs): - self.assertRaisesRegex( - ValueError, - ".*requires to have shape.*", - self.mesh.add_coords, - **kwargs, - ) + with pytest.raises(ValueError, match=".*requires to have shape.*"): + self.mesh.add_coords(**kwargs) def test_add_coords_single_face(self): - self.assertRaises(TypeError, self.mesh.add_coords, face_x=self.FACE_LON) + with pytest.raises(TypeError): + self.mesh.add_coords(face_x=self.FACE_LON) - def test_dimension_names(self): + def test_dimension_names(self, caplog): # Test defaults. default = components.Mesh1DNames("Mesh1d_node", "Mesh1d_edge") - self.assertEqual(default, self.mesh.dimension_names()) + assert default == self.mesh.dimension_names() log_regex = r"Not setting face_dimension.*" - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): self.mesh.dimension_names("foo", "bar", "baz") - self.assertEqual( - components.Mesh1DNames("foo", "bar"), - self.mesh.dimension_names(), - ) + assert components.Mesh1DNames("foo", "bar") == self.mesh.dimension_names() self.mesh.dimension_names_reset(True, True, True) - self.assertEqual(default, self.mesh.dimension_names()) + assert default == self.mesh.dimension_names() # Single. self.mesh.dimension_names(edge="foo") - self.assertEqual("foo", self.mesh.edge_dimension) + assert "foo" == self.mesh.edge_dimension self.mesh.dimension_names_reset(edge=True) - self.assertEqual(default, self.mesh.dimension_names()) + assert default == self.mesh.dimension_names() def test_edge_dimension_set(self): self.mesh.edge_dimension = "foo" - self.assertEqual("foo", self.mesh.edge_dimension) + assert "foo" == self.mesh.edge_dimension - def test_face_dimension_set(self): + def test_face_dimension_set(self, caplog): log_regex = r"Not setting face_dimension.*" - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): self.mesh.face_dimension = "foo" - self.assertIsNone(self.mesh.face_dimension) + assert self.mesh.face_dimension is None def test_node_dimension_set(self): self.mesh.node_dimension = "foo" - self.assertEqual("foo", self.mesh.node_dimension) + assert "foo" == self.mesh.node_dimension - def test_remove_connectivities(self): + def test_remove_connectivities(self, caplog, mocker): """Test that remove() mimics the connectivities() method correctly, and prevents removal of mandatory connectivities. @@ -926,7 +897,7 @@ def test_remove_connectivities(self): {"contains_edge": True, "contains_node": True}, ) - fake_connectivity = tests.mock.Mock( + fake_connectivity = mocker.Mock( __class__=components.Connectivity, cf_role="fake" ) negative_kwargs = ( @@ -945,18 +916,20 @@ def test_remove_connectivities(self): log_regex = r"Ignoring request to remove.*" for kwargs in positive_kwargs: - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): self.mesh.remove_connectivities(**kwargs) - self.assertEqual(self.EDGE_NODE, self.mesh.edge_node_connectivity) + assert self.EDGE_NODE == self.mesh.edge_node_connectivity for kwargs in negative_kwargs: - with self.assertLogs(logger, level="DEBUG") as log: - # Check that the only debug log is the one we inserted. + caplog.clear() + with caplog.at_level("DEBUG", logger=logger.name): logger.debug("foo", extra=dict(cls=None)) self.mesh.remove_connectivities(**kwargs) - self.assertEqual(1, len(log.records)) - self.assertEqual(self.EDGE_NODE, self.mesh.edge_node_connectivity) + assert len(caplog.records) == 1 + assert self.EDGE_NODE == self.mesh.edge_node_connectivity - def test_remove_coords(self): + def test_remove_coords(self, caplog): # Test that remove() mimics the coords() method correctly, # and prevents removal of mandatory coords. positive_kwargs = ( @@ -980,60 +953,64 @@ def test_remove_coords(self): log_regex = r"Ignoring request to remove.*" for kwargs in positive_kwargs: - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): self.mesh.remove_coords(**kwargs) - self.assertEqual(self.NODE_LON, self.mesh.node_coords.node_x) + assert self.NODE_LON == self.mesh.node_coords.node_x for kwargs in negative_kwargs: - with self.assertLogs(logger, level="DEBUG") as log: + caplog.clear() + with caplog.at_level("DEBUG", logger=logger.name): # Check that the only debug log is the one we inserted. logger.debug("foo", extra=dict(cls=None)) self.mesh.remove_coords(**kwargs) - self.assertEqual(1, len(log.records)) - self.assertEqual(self.NODE_LON, self.mesh.node_coords.node_x) + assert len(caplog.records) == 1 + assert self.NODE_LON == self.mesh.node_coords.node_x # Test removal of optional connectivity. self.mesh.add_coords(edge_x=self.EDGE_LON) # Attempt to remove a non-existent coord. self.mesh.remove_coords(self.EDGE_LAT) # Confirm that EDGE_LON is still there. - self.assertEqual(self.EDGE_LON, self.mesh.edge_coords.edge_x) + assert self.EDGE_LON == self.mesh.edge_coords.edge_x # Remove EDGE_LON and confirm success. self.mesh.remove_coords(self.EDGE_LON) - self.assertEqual(None, self.mesh.edge_coords.edge_x) + assert None is self.mesh.edge_coords.edge_x - def test_to_MeshCoord(self): + def test_to_mesh_coord(self): location = "node" axis = "x" result = self.mesh.to_MeshCoord(location, axis) - self.assertIsInstance(result, components.MeshCoord) - self.assertEqual(location, result.location) - self.assertEqual(axis, result.axis) + assert isinstance(result, components.MeshCoord) + assert location == result.location + assert axis == result.axis - def test_to_MeshCoord_face(self): + def test_to_mesh_coord_face(self): location = "face" axis = "x" - self.assertRaises( - CoordinateNotFoundError, self.mesh.to_MeshCoord, location, axis - ) + with pytest.raises(CoordinateNotFoundError): + self.mesh.to_MeshCoord(location, axis) - def test_to_MeshCoords(self): + def test_to_mesh_coords(self): location = "node" result = self.mesh.to_MeshCoords(location) - self.assertEqual(len(self.mesh.AXES), len(result)) + assert len(self.mesh.AXES) == len(result) for ix, axis in enumerate(self.mesh.AXES): coord = result[ix] - self.assertIsInstance(coord, components.MeshCoord) - self.assertEqual(location, coord.location) - self.assertEqual(axis, coord.axis) + assert isinstance(coord, components.MeshCoord) + assert location == coord.location + assert axis == coord.axis - def test_to_MeshCoords_face(self): + def test_to_mesh_coords_face(self): location = "face" - self.assertRaises(CoordinateNotFoundError, self.mesh.to_MeshCoords, location) + with pytest.raises(CoordinateNotFoundError): + self.mesh.to_MeshCoords(location) class TestOperations2D(TestOperations1D): # Additional/specialised tests for topology_dimension=2. - def setUp(self): + @pytest.fixture(autouse=True) + def _setup_2d(self, _setup_1d): self.mesh = components.MeshXY( topology_dimension=2, node_coords_and_axes=((self.NODE_LON, "x"), (self.NODE_LAT, "y")), @@ -1053,7 +1030,7 @@ def test_add_connectivities(self): face_node=self.mesh.face_node_connectivity, **kwargs ) self.mesh.add_connectivities(*kwargs.values()) - self.assertEqual(expected, self.mesh.all_connectivities) + assert expected == self.mesh.all_connectivities # REPLACE connectivities. kwargs["face_node"] = self.FACE_NODE @@ -1062,9 +1039,9 @@ def test_add_connectivities(self): # different length. kwargs = {k: self.new_connectivity(v, new_len) for k, v in kwargs.items()} self.mesh.add_connectivities(*kwargs.values()) - self.assertEqual( - components.Mesh2DConnectivities(**kwargs), - self.mesh.all_connectivities, + assert ( + components.Mesh2DConnectivities(**kwargs) + == self.mesh.all_connectivities ) def test_add_connectivities_inconsistent(self): @@ -1073,37 +1050,28 @@ def test_add_connectivities_inconsistent(self): face_edge = self.new_connectivity(self.FACE_EDGE, new_len=True) edge_face = self.new_connectivity(self.EDGE_FACE, new_len=True) for args in ([face_edge], [edge_face], [face_edge, edge_face]): - self.assertRaisesRegex( - ValueError, - "inconsistent .* counts.", - self.mesh.add_connectivities, - *args, - ) + with pytest.raises(ValueError, match="inconsistent .* counts."): + self.mesh.add_connectivities(*args) # REPLACE Connectivities self.mesh.add_connectivities(self.FACE_EDGE, self.EDGE_FACE) for args in ([face_edge], [edge_face], [face_edge, edge_face]): - self.assertRaisesRegex( - ValueError, - "inconsistent .* counts.", - self.mesh.add_connectivities, - *args, - ) + with pytest.raises(ValueError, match="inconsistent .* counts."): + self.mesh.add_connectivities(*args) - def test_add_connectivities_invalid(self): - fake_cf_role = tests.mock.Mock(__class__=components.Connectivity, cf_role="foo") + def test_add_connectivities_invalid(self, caplog, mocker): + fake_cf_role = mocker.Mock(__class__=components.Connectivity, cf_role="foo") log_regex = r"Not adding connectivity.*" - with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): + with _shared_utils.assert_logs( + caplog, logger, level="DEBUG", msg_regex=log_regex + ): self.mesh.add_connectivities(fake_cf_role) def test_add_coords_face(self): # ADD coords. kwargs = {"face_x": self.FACE_LON, "face_y": self.FACE_LAT} self.mesh.add_coords(**kwargs) - self.assertEqual( - components.MeshFaceCoords(**kwargs), - self.mesh.face_coords, - ) + assert components.MeshFaceCoords(**kwargs) == self.mesh.face_coords for new_shape in (False, True): # REPLACE coords, first with ones of the same shape, then with ones @@ -1113,56 +1081,48 @@ def test_add_coords_face(self): "face_y": self.new_coord(self.FACE_LAT, new_shape), } self.mesh.add_coords(**kwargs) - self.assertEqual( - components.MeshFaceCoords(**kwargs), - self.mesh.face_coords, - ) + assert components.MeshFaceCoords(**kwargs) == self.mesh.face_coords def test_add_coords_single_face(self): # ADD coord. face_x = self.FACE_LON expected = components.MeshFaceCoords(face_x=face_x, face_y=None) self.mesh.add_coords(face_x=face_x) - self.assertEqual(expected, self.mesh.face_coords) + assert expected == self.mesh.face_coords # REPLACE coord. face_x = self.new_coord(self.FACE_LON) expected = components.MeshFaceCoords(face_x=face_x, face_y=None) self.mesh.add_coords(face_x=face_x) - self.assertEqual(expected, self.mesh.face_coords) + assert expected == self.mesh.face_coords # Attempt to REPLACE coord with that of DIFFERENT SHAPE. face_x = self.new_coord(self.FACE_LON, new_shape=True) - self.assertRaisesRegex( - ValueError, - ".*requires to have shape.*", - self.mesh.add_coords, - face_x=face_x, - ) + with pytest.raises(ValueError, match=".*requires to have shape.*"): + self.mesh.add_coords(face_x=face_x) def test_dimension_names(self): # Test defaults. default = components.Mesh2DNames("Mesh2d_node", "Mesh2d_edge", "Mesh2d_face") - self.assertEqual(default, self.mesh.dimension_names()) + assert default == self.mesh.dimension_names() self.mesh.dimension_names("foo", "bar", "baz") - self.assertEqual( - components.Mesh2DNames("foo", "bar", "baz"), - self.mesh.dimension_names(), + assert ( + components.Mesh2DNames("foo", "bar", "baz") == self.mesh.dimension_names() ) self.mesh.dimension_names_reset(True, True, True) - self.assertEqual(default, self.mesh.dimension_names()) + assert default == self.mesh.dimension_names() # Single. self.mesh.dimension_names(face="foo") - self.assertEqual("foo", self.mesh.face_dimension) + assert "foo" == self.mesh.face_dimension self.mesh.dimension_names_reset(face=True) - self.assertEqual(default, self.mesh.dimension_names()) + assert default == self.mesh.dimension_names() def test_face_dimension_set(self): self.mesh.face_dimension = "foo" - self.assertEqual("foo", self.mesh.face_dimension) + assert "foo" == self.mesh.face_dimension def test_remove_connectivities(self): """Do what 1D test could not - test removal of optional connectivity.""" @@ -1171,38 +1131,38 @@ def test_remove_connectivities(self): # Attempt to remove a non-existent connectivity. self.mesh.remove_connectivities(self.EDGE_NODE) # Confirm that FACE_FACE is still there. - self.assertEqual(self.FACE_FACE, self.mesh.face_face_connectivity) + assert self.FACE_FACE == self.mesh.face_face_connectivity # Remove FACE_FACE and confirm success. self.mesh.remove_connectivities(contains_face=True) - self.assertEqual(None, self.mesh.face_face_connectivity) + assert None is self.mesh.face_face_connectivity - def test_remove_coords(self): + def test_remove_coords(self, caplog): """Test the face argument.""" - super().test_remove_coords() + super().test_remove_coords(caplog) self.mesh.add_coords(face_x=self.FACE_LON) - self.assertEqual(self.FACE_LON, self.mesh.face_coords.face_x) + assert self.FACE_LON == self.mesh.face_coords.face_x self.mesh.remove_coords(location="face") - self.assertEqual(None, self.mesh.face_coords.face_x) + assert None is self.mesh.face_coords.face_x - def test_to_MeshCoord_face(self): + def test_to_mesh_coord_face(self): self.mesh.add_coords(face_x=self.FACE_LON) location = "face" axis = "x" result = self.mesh.to_MeshCoord(location, axis) - self.assertIsInstance(result, components.MeshCoord) - self.assertEqual(location, result.location) - self.assertEqual(axis, result.axis) + assert isinstance(result, components.MeshCoord) + assert location == result.location + assert axis == result.axis - def test_to_MeshCoords_face(self): + def test_to_mesh_coords_face(self): self.mesh.add_coords(face_x=self.FACE_LON, face_y=self.FACE_LAT) location = "face" result = self.mesh.to_MeshCoords(location) - self.assertEqual(len(self.mesh.AXES), len(result)) + assert len(self.mesh.AXES) == len(result) for ix, axis in enumerate(self.mesh.AXES): coord = result[ix] - self.assertIsInstance(coord, components.MeshCoord) - self.assertEqual(location, coord.location) - self.assertEqual(axis, coord.axis) + assert isinstance(coord, components.MeshCoord) + assert location == coord.location + assert axis == coord.axis class InitValidation(TestMeshCommon): @@ -1215,46 +1175,33 @@ def test_invalid_topology(self): ), "connectivities": self.EDGE_NODE, } - self.assertRaisesRegex( - ValueError, - "Expected 'topology_dimension'.*", - components.MeshXY, - **kwargs, - ) + with pytest.raises(ValueError, match="Expected 'topology_dimension'.*"): + components.MeshXY(**kwargs) def test_invalid_axes(self): kwargs = { "topology_dimension": 2, "connectivities": self.FACE_NODE, } - self.assertRaisesRegex( - ValueError, - "Invalid axis specified for node.*", - components.MeshXY, - node_coords_and_axes=( - (self.NODE_LON, "foo"), - (self.NODE_LAT, "y"), - ), - **kwargs, - ) + with pytest.raises(ValueError, match="Invalid axis specified for node.*"): + components.MeshXY( + node_coords_and_axes=((self.NODE_LON, "foo"), (self.NODE_LAT, "y")), + **kwargs, + ) kwargs["node_coords_and_axes"] = ( (self.NODE_LON, "x"), (self.NODE_LAT, "y"), ) - self.assertRaisesRegex( - ValueError, - "Invalid axis specified for edge.*", - components.MeshXY, - edge_coords_and_axes=((self.EDGE_LON, "foo"),), - **kwargs, - ) - self.assertRaisesRegex( - ValueError, - "Invalid axis specified for face.*", - components.MeshXY, - face_coords_and_axes=((self.FACE_LON, "foo"),), - **kwargs, - ) + with pytest.raises(ValueError, match="Invalid axis specified for edge.*"): + components.MeshXY( + edge_coords_and_axes=((self.EDGE_LON, "foo"),), + **kwargs, + ) + with pytest.raises(ValueError, match="Invalid axis specified for face.*"): + components.MeshXY( + face_coords_and_axes=((self.FACE_LON, "foo"),), + **kwargs, + ) # Several arg safety checks in __init__ currently unreachable given earlier checks. @@ -1268,12 +1215,8 @@ def test_minimum_connectivities(self): ), "connectivities": (self.FACE_NODE,), } - self.assertRaisesRegex( - ValueError, - ".*requires a edge_node_connectivity.*", - components.MeshXY, - **kwargs, - ) + with pytest.raises(ValueError, match=".*requires a edge_node_connectivity.*"): + components.MeshXY(**kwargs) def test_minimum_coords(self): # Further validations are tested in add_coord tests. @@ -1282,13 +1225,5 @@ def test_minimum_coords(self): "node_coords_and_axes": ((self.NODE_LON, "x"), (None, "y")), "connectivities": (self.FACE_NODE,), } - self.assertRaisesRegex( - ValueError, - ".*is a required coordinate.*", - components.MeshXY, - **kwargs, - ) - - -if __name__ == "__main__": - tests.main() + with pytest.raises(ValueError, match=".*is a required coordinate.*"): + components.MeshXY(**kwargs) From f0a446ba1e9ac4ff4426c3587fdcd676174550c3 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 11 Aug 2025 16:01:57 +0100 Subject: [PATCH 4/9] Convert test_MeshXY__from_coords to PyTest. --- .../components/test_MeshXY__from_coords.py | 124 ++++++++++-------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py b/lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py index 8114fbe92e..5b086cafd2 100644 --- a/lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py @@ -4,19 +4,18 @@ # See LICENSE in the root of the repository for full licensing details. """Unit tests for the :meth:`iris.mesh.MeshXY.from_coords`.""" -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - import numpy as np +import pytest from iris.coords import AuxCoord, DimCoord from iris.mesh import Connectivity, MeshXY, logger +from iris.tests import _shared_utils from iris.tests.stock import simple_2d_w_multidim_coords -class Test1Dim(tests.IrisTest): - def setUp(self): +class Test1Dim: + @pytest.fixture(autouse=True) + def _setup_1d(self): self.lon = DimCoord( points=[0.5, 1.5, 2.5], bounds=[[0, 1], [1, 2], [2, 3]], @@ -42,20 +41,28 @@ def create(self): def test_dimensionality(self): mesh = self.create() - self.assertEqual(1, mesh.topology_dimension) + assert 1 == mesh.topology_dimension - self.assertArrayEqual([0, 1, 1, 2, 2, 3], mesh.node_coords.node_x.points) - self.assertArrayEqual([0, 1, 2, 3, 1, 2], mesh.node_coords.node_y.points) - self.assertArrayEqual([0.5, 1.5, 2.5], mesh.edge_coords.edge_x.points) - self.assertArrayEqual([0.5, 2.5, 1.5], mesh.edge_coords.edge_y.points) - self.assertIsNone(getattr(mesh, "face_coords", None)) + _shared_utils.assert_array_equal( + [0, 1, 1, 2, 2, 3], mesh.node_coords.node_x.points + ) + _shared_utils.assert_array_equal( + [0, 1, 2, 3, 1, 2], mesh.node_coords.node_y.points + ) + _shared_utils.assert_array_equal( + [0.5, 1.5, 2.5], mesh.edge_coords.edge_x.points + ) + _shared_utils.assert_array_equal( + [0.5, 2.5, 1.5], mesh.edge_coords.edge_y.points + ) + assert getattr(mesh, "face_coords", None) is None for conn_name in Connectivity.UGRID_CF_ROLES: conn = getattr(mesh, conn_name, None) if conn_name == "edge_node_connectivity": - self.assertArrayEqual([[0, 1], [2, 3], [4, 5]], conn.indices) + _shared_utils.assert_array_equal([[0, 1], [2, 3], [4, 5]], conn.indices) else: - self.assertIsNone(conn) + assert conn is None def test_node_metadata(self): mesh = self.create() @@ -67,8 +74,8 @@ def test_node_metadata(self): for attr in ("standard_name", "long_name", "units", "attributes"): expected = getattr(expected_coord, attr) actual = getattr(actual_coord, attr) - self.assertEqual(expected, actual) - self.assertIsNone(actual_coord.var_name) + assert expected == actual + assert actual_coord.var_name is None def test_centre_metadata(self): mesh = self.create() @@ -80,8 +87,8 @@ def test_centre_metadata(self): for attr in ("standard_name", "long_name", "units", "attributes"): expected = getattr(expected_coord, attr) actual = getattr(actual_coord, attr) - self.assertEqual(expected, actual) - self.assertIsNone(actual_coord.var_name) + assert expected == actual + assert actual_coord.var_name is None def test_mesh_metadata(self): # Inappropriate to guess these values from the input coords. @@ -91,9 +98,9 @@ def test_mesh_metadata(self): "long_name", "var_name", ): - self.assertIsNone(getattr(mesh, attr)) - self.assertTrue(mesh.units.is_unknown()) - self.assertDictEqual({}, mesh.attributes) + assert getattr(mesh, attr) is None + assert mesh.units.is_unknown() + assert {} == mesh.attributes def test_lazy(self): self.lon = AuxCoord.from_coord(self.lon) @@ -103,21 +110,21 @@ def test_lazy(self): mesh = self.create() for coord in list(mesh.all_coords): if coord is not None: - self.assertTrue(coord.has_lazy_points()) + assert coord.has_lazy_points() for conn in list(mesh.all_connectivities): if conn is not None: - self.assertTrue(conn.has_lazy_indices()) + assert conn.has_lazy_indices() def test_coord_shape_mismatch(self): lat_orig = self.lat.copy(self.lat.points, self.lat.bounds) self.lat = lat_orig.copy( points=lat_orig.points, bounds=np.tile(lat_orig.bounds, 2) ) - with self.assertRaisesRegex(ValueError, "bounds shapes are not identical"): + with pytest.raises(ValueError, match="bounds shapes are not identical"): _ = self.create() self.lat = lat_orig.copy(points=lat_orig.points[-1], bounds=lat_orig.bounds[-1]) - with self.assertRaisesRegex(ValueError, "points shapes are not identical"): + with pytest.raises(ValueError, match="points shapes are not identical"): _ = self.create() def test_reorder(self): @@ -125,26 +132,27 @@ def test_reorder(self): self.lat, self.lon = self.lon, self.lat mesh = self.create() # Confirm that the coords have been swapped back to the 'correct' order. - self.assertEqual("longitude", mesh.node_coords.node_x.standard_name) - self.assertEqual("latitude", mesh.node_coords.node_y.standard_name) + assert "longitude" == mesh.node_coords.node_x.standard_name + assert "latitude" == mesh.node_coords.node_y.standard_name - def test_non_xy(self): + def test_non_xy(self, caplog): for coord in self.lon, self.lat: coord.standard_name = None lon_name, lat_name = [coord.long_name for coord in (self.lon, self.lat)] # Swap the coords. self.lat, self.lon = self.lon, self.lat - with self.assertLogs(logger, "INFO", "Unable to find 'X' and 'Y'"): + with _shared_utils.assert_logs( + caplog, logger, "INFO", "Unable to find 'X' and 'Y'" + ): mesh = self.create() # Confirm that the coords have not been swapped back. - self.assertEqual(lat_name, mesh.node_coords.node_x.long_name) - self.assertEqual(lon_name, mesh.node_coords.node_y.long_name) + assert lat_name == mesh.node_coords.node_x.long_name + assert lon_name == mesh.node_coords.node_y.long_name class Test2Dim(Test1Dim): - def setUp(self): - super().setUp() - + @pytest.fixture(autouse=True) + def _setup_2d(self, _setup_1d): self.lon.bounds = [[0, 0.5, 1], [1, 1.5, 2], [2, 2.5, 3]] self.lon.long_name = "triangle longitudes" self.lat.bounds = [[0, 1, 0], [2, 3, 2], [1, 2, 1]] @@ -152,25 +160,31 @@ def setUp(self): def test_dimensionality(self): mesh = self.create() - self.assertEqual(2, mesh.topology_dimension) + assert 2 == mesh.topology_dimension - self.assertArrayEqual( + _shared_utils.assert_array_equal( [0, 0.5, 1, 1, 1.5, 2, 2, 2.5, 3], mesh.node_coords.node_x.points ) - self.assertArrayEqual( + _shared_utils.assert_array_equal( [0, 1, 0, 2, 3, 2, 1, 2, 1], mesh.node_coords.node_y.points ) - self.assertIsNone(mesh.edge_coords.edge_x) - self.assertIsNone(mesh.edge_coords.edge_y) - self.assertArrayEqual([0.5, 1.5, 2.5], mesh.face_coords.face_x.points) - self.assertArrayEqual([0.5, 2.5, 1.5], mesh.face_coords.face_y.points) + assert mesh.edge_coords.edge_x is None + assert mesh.edge_coords.edge_y is None + _shared_utils.assert_array_equal( + [0.5, 1.5, 2.5], mesh.face_coords.face_x.points + ) + _shared_utils.assert_array_equal( + [0.5, 2.5, 1.5], mesh.face_coords.face_y.points + ) for conn_name in Connectivity.UGRID_CF_ROLES: conn = getattr(mesh, conn_name, None) if conn_name == "face_node_connectivity": - self.assertArrayEqual([[0, 1, 2], [3, 4, 5], [6, 7, 8]], conn.indices) + _shared_utils.assert_array_equal( + [[0, 1, 2], [3, 4, 5], [6, 7, 8]], conn.indices + ) else: - self.assertIsNone(conn) + assert conn is None def test_centre_metadata(self): mesh = self.create() @@ -182,8 +196,8 @@ def test_centre_metadata(self): for attr in ("standard_name", "long_name", "units", "attributes"): expected = getattr(expected_coord, attr) actual = getattr(actual_coord, attr) - self.assertEqual(expected, actual) - self.assertIsNone(actual_coord.var_name) + assert expected == actual + assert actual_coord.var_name is None def test_mixed_shapes(self): self.lon = AuxCoord.from_coord(self.lon) @@ -194,34 +208,36 @@ def test_mixed_shapes(self): self.lat.bounds = np.ma.masked_equal(lat_bounds, 999) mesh = self.create() - self.assertArrayEqual(mesh.face_node_connectivity.location_lengths(), [4, 4, 3]) - self.assertEqual(mesh.node_coords.node_x.points[-1], 0.0) - self.assertEqual(mesh.node_coords.node_y.points[-1], 0.0) + _shared_utils.assert_array_equal( + mesh.face_node_connectivity.location_lengths(), [4, 4, 3] + ) + assert mesh.node_coords.node_x.points[-1] == 0.0 + assert mesh.node_coords.node_y.points[-1] == 0.0 -class TestInvalidBounds(tests.IrisTest): +class TestInvalidBounds: """Invalid bounds not supported.""" def test_no_bounds(self): lon = AuxCoord(points=[0.5, 1.5, 2.5]) lat = AuxCoord(points=[0, 1, 2]) - with self.assertRaisesRegex(ValueError, "bounds missing from"): + with pytest.raises(ValueError, match="bounds missing from"): _ = MeshXY.from_coords(lon, lat) def test_1_bound(self): lon = AuxCoord(points=[0.5, 1.5, 2.5], bounds=[[0], [1], [2]]) lat = AuxCoord(points=[0, 1, 2], bounds=[[0.5], [1.5], [2.5]]) - with self.assertRaisesRegex( - ValueError, r"Expected coordinate bounds.shape \(n, >=2\)" + with pytest.raises( + ValueError, match=r"Expected coordinate bounds.shape \(n, >=2\)" ): _ = MeshXY.from_coords(lon, lat) -class TestInvalidPoints(tests.IrisTest): +class TestInvalidPoints: """Only 1D coords supported.""" def test_2d_coord(self): cube = simple_2d_w_multidim_coords()[:3, :3] coord_1, coord_2 = cube.coords() - with self.assertRaisesRegex(ValueError, "Expected coordinate ndim == 1"): + with pytest.raises(ValueError, match="Expected coordinate ndim == 1"): _ = MeshXY.from_coords(coord_1, coord_2) From 7947e473411096e1c78651ff767fca7563154dcf Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 11 Aug 2025 17:57:40 +0100 Subject: [PATCH 5/9] Convert test_recombine_submeshes to PyTest. --- .../mesh/utils/test_recombine_submeshes.py | 125 +++++++++--------- 1 file changed, 61 insertions(+), 64 deletions(-) diff --git a/lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py b/lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py index 8e692140b6..519f823a87 100644 --- a/lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py +++ b/lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py @@ -4,16 +4,14 @@ # See LICENSE in the root of the repository for full licensing details. """Unit tests for :func:`iris.mesh.utils.recombine_submeshes`.""" -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - import dask.array as da import numpy as np +import pytest from iris.coords import AuxCoord from iris.cube import CubeList from iris.mesh.utils import recombine_submeshes +from iris.tests import _shared_utils from iris.tests.stock.mesh import sample_mesh, sample_mesh_cube @@ -78,25 +76,26 @@ def common_test_setup(self, shape_3d=(0, 2), data_chunks=None): self.expected_result = expected -class TestRecombine__data(tests.IrisTest): - def setUp(self): +class TestRecombine__data: + @pytest.fixture(autouse=True) + def _setup(self): common_test_setup(self) def test_basic(self): # Just confirm that all source data is lazy (by default) for cube in self.region_cubes + [self.mesh_cube]: - self.assertTrue(cube.has_lazy_data()) + assert cube.has_lazy_data() result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertTrue(result.has_lazy_data()) - self.assertMaskedArrayEqual(result.data, self.expected_result) + assert result.has_lazy_data() + _shared_utils.assert_masked_array_equal(result.data, self.expected_result) def test_chunking(self): # Make non-standard testcube with higher dimensions + specific chunking common_test_setup(self, shape_3d=(10, 3), data_chunks=(3, 2, -1)) - self.assertEqual(self.mesh_cube.lazy_data().chunksize, (3, 2, 20)) + assert self.mesh_cube.lazy_data().chunksize == (3, 2, 20) result = recombine_submeshes(self.mesh_cube, self.region_cubes) # Check that the result chunking matches the input. - self.assertEqual(result.lazy_data().chunksize, (3, 2, 20)) + assert result.lazy_data().chunksize == (3, 2, 20) def test_single_region(self): region = self.region_cubes[1] @@ -106,7 +105,7 @@ def test_single_region(self): expected = np.ma.masked_array(np.zeros(self.mesh_cube.shape), True) inds = region.coord("i_mesh_index").points expected[..., inds] = region.data - self.assertMaskedArrayEqual(result.data, expected) + _shared_utils.assert_masked_array_equal(result.data, expected) def test_region_overlaps(self): # generate two identical regions with different values. @@ -118,11 +117,11 @@ def test_region_overlaps(self): # check that result values all come from the second. result1 = recombine_submeshes(self.mesh_cube, [region1, region2]) result1 = result1[..., inds].data - self.assertArrayEqual(result1, 202.0) + _shared_utils.assert_array_equal(result1, 202.0) # swap the region order, and it should resolve the other way. result2 = recombine_submeshes(self.mesh_cube, [region2, region1]) result2 = result2[..., inds].data - self.assertArrayEqual(result2, 101.0) + _shared_utils.assert_array_equal(result2, 101.0) def test_missing_points(self): # check results with and without a specific region included. @@ -130,50 +129,52 @@ def test_missing_points(self): inds = region2.coord("i_mesh_index").points # With all regions, no points in reg1 are masked result_all = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertTrue(np.all(~result_all[..., inds].data.mask)) + assert np.all(~result_all[..., inds].data.mask) # Without region1, all points in reg1 are masked regions_not2 = [cube for cube in self.region_cubes if cube is not region2] result_not2 = recombine_submeshes(self.mesh_cube, regions_not2) - self.assertTrue(np.all(result_not2[..., inds].data.mask)) + assert np.all(result_not2[..., inds].data.mask) def test_transposed(self): # Check function when mesh-dim is NOT the last dim. self.mesh_cube.transpose() - self.assertEqual(self.mesh_cube.mesh_dim(), 0) + assert self.mesh_cube.mesh_dim() == 0 for cube in self.region_cubes: cube.transpose() result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertTrue(result.has_lazy_data()) - self.assertEqual(result.mesh_dim(), 0) - self.assertMaskedArrayEqual(result.data.transpose(), self.expected_result) + assert result.has_lazy_data() + assert result.mesh_dim() == 0 + _shared_utils.assert_masked_array_equal( + result.data.transpose(), self.expected_result + ) def test_dtype(self): # Check that result dtype comes from submeshes, not mesh_cube. - self.assertEqual(self.mesh_cube.dtype, np.float64) - self.assertTrue(all(cube.dtype == np.float64 for cube in self.region_cubes)) + assert self.mesh_cube.dtype == np.float64 + assert all(cube.dtype == np.float64 for cube in self.region_cubes) result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertEqual(result.dtype, np.float64) + assert result.dtype == np.float64 region_cubes2 = [ cube.copy(data=cube.lazy_data().astype(np.int16)) for cube in self.region_cubes ] result2 = recombine_submeshes(self.mesh_cube, region_cubes2) - self.assertEqual(result2.dtype, np.int16) + assert result2.dtype == np.int16 def test_meshcube_real(self): # Real data in reference 'mesh_cube' makes no difference. self.mesh_cube.data result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertTrue(result.has_lazy_data()) - self.assertMaskedArrayEqual(result.data, self.expected_result) + assert result.has_lazy_data() + _shared_utils.assert_masked_array_equal(result.data, self.expected_result) def test_regions_real(self): # Real data in submesh cubes makes no difference. for cube in self.region_cubes: cube.data result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertTrue(result.has_lazy_data()) - self.assertMaskedArrayEqual(result.data, self.expected_result) + assert result.has_lazy_data() + _shared_utils.assert_masked_array_equal(result.data, self.expected_result) def test_allinput_real(self): # Real data in reference AND regions still makes no difference. @@ -181,8 +182,8 @@ def test_allinput_real(self): for cube in self.region_cubes: cube.data result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertTrue(result.has_lazy_data()) - self.assertMaskedArrayEqual(result.data, self.expected_result) + assert result.has_lazy_data() + _shared_utils.assert_masked_array_equal(result.data, self.expected_result) def test_meshcube_masking(self): # Masked points in the reference 'mesh_cube' should make no difference. @@ -196,7 +197,7 @@ def test_meshcube_masking(self): self.mesh_cube.data = self.mesh_cube.lazy_data() # remake as lazy # result should show no difference result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertMaskedArrayEqual(result.data, self.expected_result) + _shared_utils.assert_masked_array_equal(result.data, self.expected_result) def test_no_missing_results(self): # For a result with no missing points, result array is still masked @@ -216,11 +217,11 @@ def test_no_missing_results(self): # result is as "normal" expected, except at the usually-missing points. expected = self.expected_result expected[expected.mask] = 7.777 - self.assertArrayEqual(result, expected) + _shared_utils.assert_array_equal(result, expected) # the actual result array is still masked, though with no masked points - self.assertIsInstance(result, np.ma.MaskedArray) - self.assertIsInstance(result.mask, np.ndarray) - self.assertArrayEqual(result.mask, False) + assert isinstance(result, np.ma.MaskedArray) + assert isinstance(result.mask, np.ndarray) + _shared_utils.assert_array_equal(result.mask, False) def test_maskeddata(self): # Check that masked points within regions behave like ordinary values. @@ -236,7 +237,7 @@ def test_maskeddata(self): expected = self.expected_result expected[:, 0] = np.ma.masked expected[:, 6] = np.ma.masked - self.assertArrayEqual(result.mask, expected.mask) + _shared_utils.assert_array_equal(result.mask, expected.mask) def test_nandata(self): # Check that NaN points within regions behave like ordinary values. @@ -249,16 +250,17 @@ def test_nandata(self): expected = self.expected_result expected[:, 0] = np.nan expected[:, 6] = np.nan - self.assertArrayEqual(np.isnan(result), np.isnan(expected)) + _shared_utils.assert_array_equal(np.isnan(result), np.isnan(expected)) -class TestRecombine__api(tests.IrisTest): - def setUp(self): +class TestRecombine__api: + @pytest.fixture(autouse=True) + def _setup(self): common_test_setup(self) def test_fail_no_mesh(self): self.mesh_cube = self.mesh_cube[..., 0:] - with self.assertRaisesRegex(ValueError, 'mesh_cube.*has no ".mesh"'): + with pytest.raises(ValueError, match='mesh_cube.*has no ".mesh"'): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_single_region(self): @@ -266,23 +268,23 @@ def test_single_region(self): single_region = self.region_cubes[0] result1 = recombine_submeshes(self.mesh_cube, single_region) result2 = recombine_submeshes(self.mesh_cube, [single_region]) - self.assertEqual(result1, result2) + assert result1 == result2 def test_fail_no_regions(self): - with self.assertRaisesRegex(ValueError, "'submesh_cubes' must be non-empty"): + with pytest.raises(ValueError, match="'submesh_cubes' must be non-empty"): recombine_submeshes(self.mesh_cube, []) def test_fail_dims_mismatch_mesh_regions(self): self.mesh_cube = self.mesh_cube[0] - with self.assertRaisesRegex( - ValueError, "Submesh cube.*has 2 dimensions, but 'mesh_cube' has 1" + with pytest.raises( + ValueError, match="Submesh cube.*has 2 dimensions, but 'mesh_cube' has 1" ): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_dims_mismatch_region_regions(self): self.region_cubes[1] = self.region_cubes[1][1] - with self.assertRaisesRegex( - ValueError, "Submesh cube.*has 1 dimensions, but 'mesh_cube' has 2" + with pytest.raises( + ValueError, match="Submesh cube.*has 1 dimensions, but 'mesh_cube' has 2" ): recombine_submeshes(self.mesh_cube, self.region_cubes) @@ -296,7 +298,7 @@ def test_fail_metdata_mismatch_region_regions(self): "does not match that of the other region_cubes,.*" "long_name=mesh_phenom" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) # Also check units @@ -308,7 +310,7 @@ def test_fail_metdata_mismatch_region_regions(self): "does not match that of the other region_cubes,.*" "units=unknown" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) # Also check attributes @@ -320,7 +322,7 @@ def test_fail_metdata_mismatch_region_regions(self): "does not match that of the other region_cubes,.*" "units=unknown, cell_methods=" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_dtype_mismatch_region_regions(self): @@ -331,13 +333,13 @@ def test_fail_dtype_mismatch_region_regions(self): "which does not match that of the other region_cubes, " "which is float64" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_dimcoord_sub_no_mesh(self): self.mesh_cube.remove_coord("level") msg = "has a dim-coord \"level\" for dimension 0, but 'mesh_cube' has none." - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_dimcoord_mesh_no_sub(self): @@ -346,7 +348,7 @@ def test_fail_dimcoord_mesh_no_sub(self): "has no dim-coord for dimension 0, " "to match the 'mesh_cube' dimension \"level\"" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_dimcoord_mesh_sub_differ(self): @@ -356,7 +358,7 @@ def test_fail_dimcoord_mesh_sub_differ(self): 'has a dim-coord "level" for dimension 0, ' "which does not match that of 'mesh_cube', \"level\"" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_index_coordname(self): @@ -366,7 +368,7 @@ def test_index_coordname(self): result = recombine_submeshes( self.mesh_cube, self.region_cubes, index_coord_name="ii" ) - self.assertArrayEqual(result.data, self.expected_result) + _shared_utils.assert_array_equal(result.data, self.expected_result) def test_fail_bad_indexcoord_name(self): self.region_cubes[2].coord("i_mesh_index").rename("ii") @@ -374,7 +376,7 @@ def test_fail_bad_indexcoord_name(self): 'Submesh cube #3/4, "mesh_phenom" has no "i_mesh_index" coord ' r"on the mesh dimension \(dimension 1\)." ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_missing_indexcoord(self): @@ -383,14 +385,14 @@ def test_fail_missing_indexcoord(self): 'Submesh cube #2/4, "mesh_phenom" has no "i_mesh_index" coord ' r"on the mesh dimension \(dimension 1\)." ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_no_mesh_indexcoord(self): # It is ok for the mesh-cube to NOT have an index-coord. self.mesh_cube.remove_coord("i_mesh_index") result = recombine_submeshes(self.mesh_cube, self.region_cubes) - self.assertArrayEqual(result.data, self.expected_result) + _shared_utils.assert_array_equal(result.data, self.expected_result) def test_fail_indexcoord_mismatch_mesh_region(self): self.mesh_cube.coord("i_mesh_index").units = "m" @@ -400,7 +402,7 @@ def test_fail_indexcoord_mismatch_mesh_region(self): "the same name in 'mesh_cube'" ".*units=1.* != .*units=m" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) def test_fail_indexcoord_mismatch_region_region(self): @@ -413,10 +415,5 @@ def test_fail_indexcoord_mismatch_region_region(self): ".*units=1, attributes={'x': 3}, climatological.*" " != .*units=1, climatological" ) - with self.assertRaisesRegex(ValueError, msg): + with pytest.raises(ValueError, match=msg): recombine_submeshes(self.mesh_cube, self.region_cubes) - - -if __name__ == "__main__": - # Make it runnable in its own right. - tests.main() From efe4fcd59edefa9202ee4e1c5d72fa2133940ab9 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 12 Aug 2025 15:36:19 +0100 Subject: [PATCH 6/9] Remove commented code. --- lib/iris/tests/_shared_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/iris/tests/_shared_utils.py b/lib/iris/tests/_shared_utils.py index 25d39d7a74..9e767ebb46 100644 --- a/lib/iris/tests/_shared_utils.py +++ b/lib/iris/tests/_shared_utils.py @@ -605,7 +605,6 @@ def assert_logs(caplog, logger=None, level=None, msg_regex=None): assert len(caplog.records) > caplog_count # Check for any formatting errors by running all the formatters. for record in caplog.records: - # for handler in caplog.logger.handlers: caplog.handler.format(record) # Check message, if requested. From 73894b7d7f3a8e39f216a4905829572ea6fbb71a Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 12 Aug 2025 15:37:29 +0100 Subject: [PATCH 7/9] Correct prefix for TestInitValidation. --- lib/iris/tests/unit/mesh/components/test_MeshXY.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/mesh/components/test_MeshXY.py b/lib/iris/tests/unit/mesh/components/test_MeshXY.py index 0062c9a674..2a49250dd9 100644 --- a/lib/iris/tests/unit/mesh/components/test_MeshXY.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshXY.py @@ -1165,7 +1165,7 @@ def test_to_mesh_coords_face(self): assert axis == coord.axis -class InitValidation(TestMeshCommon): +class TestInitValidation(TestMeshCommon): def test_invalid_topology(self): kwargs = { "topology_dimension": 0, From 60c15494c0121f066ab6abb2f28ec92a50e90840 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 12 Aug 2025 15:41:58 +0100 Subject: [PATCH 8/9] Correct test method naming convention. --- lib/iris/tests/unit/mesh/components/test_MeshXY.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/iris/tests/unit/mesh/components/test_MeshXY.py b/lib/iris/tests/unit/mesh/components/test_MeshXY.py index 2a49250dd9..0d2b9ce088 100644 --- a/lib/iris/tests/unit/mesh/components/test_MeshXY.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshXY.py @@ -977,7 +977,7 @@ def test_remove_coords(self, caplog): self.mesh.remove_coords(self.EDGE_LON) assert None is self.mesh.edge_coords.edge_x - def test_to_mesh_coord(self): + def test_to_MeshCoord(self): location = "node" axis = "x" result = self.mesh.to_MeshCoord(location, axis) @@ -985,13 +985,13 @@ def test_to_mesh_coord(self): assert location == result.location assert axis == result.axis - def test_to_mesh_coord_face(self): + def test_to_MeshCoord_face(self): location = "face" axis = "x" with pytest.raises(CoordinateNotFoundError): self.mesh.to_MeshCoord(location, axis) - def test_to_mesh_coords(self): + def test_to_MeshCoords(self): location = "node" result = self.mesh.to_MeshCoords(location) assert len(self.mesh.AXES) == len(result) @@ -1001,7 +1001,7 @@ def test_to_mesh_coords(self): assert location == coord.location assert axis == coord.axis - def test_to_mesh_coords_face(self): + def test_to_MeshCoords_face(self): location = "face" with pytest.raises(CoordinateNotFoundError): self.mesh.to_MeshCoords(location) @@ -1144,7 +1144,7 @@ def test_remove_coords(self, caplog): self.mesh.remove_coords(location="face") assert None is self.mesh.face_coords.face_x - def test_to_mesh_coord_face(self): + def test_to_MeshCoord_face(self): self.mesh.add_coords(face_x=self.FACE_LON) location = "face" axis = "x" @@ -1153,7 +1153,7 @@ def test_to_mesh_coord_face(self): assert location == result.location assert axis == result.axis - def test_to_mesh_coords_face(self): + def test_to_MeshCoords_face(self): self.mesh.add_coords(face_x=self.FACE_LON, face_y=self.FACE_LAT) location = "face" result = self.mesh.to_MeshCoords(location) From d07705ccf3f1af81e282464bb34ad5498a330df3 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 12 Aug 2025 15:49:13 +0100 Subject: [PATCH 9/9] Convert test_MeshCoord to pytest. --- lib/iris/tests/unit/mesh/components/test_MeshCoord.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/iris/tests/unit/mesh/components/test_MeshCoord.py b/lib/iris/tests/unit/mesh/components/test_MeshCoord.py index 7d64fa132c..8890207a52 100644 --- a/lib/iris/tests/unit/mesh/components/test_MeshCoord.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshCoord.py @@ -6,7 +6,6 @@ from platform import python_version import re -import unittest.mock as mock import dask.array as da import numpy as np @@ -60,9 +59,9 @@ def test_derived_properties(self): # All relevant attributes are derived from the face coord. assert meshval == getattr(face_x_coord, key) - def test_fail_bad_mesh(self): + def test_fail_bad_mesh(self, mocker): with pytest.raises(TypeError, match="must be a.*Mesh"): - sample_meshcoord(mesh=mock.sentinel.odd) + sample_meshcoord(mesh=mocker.sentinel.odd) def test_valid_locations(self): for loc in MeshXY.ELEMENTS: @@ -83,7 +82,7 @@ class Test__readonly_properties: def _setup(self): self.meshcoord = sample_meshcoord() - def test_fixed_metadata(self): + def test_fixed_metadata(self, mocker): # Check that you cannot set any of these on an existing MeshCoord. meshcoord = self.meshcoord if version.parse(python_version()) >= version.parse("3.11"): @@ -92,7 +91,7 @@ def test_fixed_metadata(self): msg = "can't set attribute" for prop in ("mesh", "location", "axis"): with pytest.raises(AttributeError, match=msg): - setattr(meshcoord, prop, mock.sentinel.odd) + setattr(meshcoord, prop, mocker.sentinel.odd) def test_set_coord_system(self): # The property exists, =None, can set to None, can not set otherwise. @@ -1068,7 +1067,7 @@ def test_bounds(self, mocker): mocked.assert_called_once() @pytest.mark.parametrize( - "metadata_name, value", + ("metadata_name", "value"), [ ("long_name", "foo"), ("var_name", "foo"),