From 960fa5446825eb48d91061bf02e412497bc627f7 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 13 Oct 2025 22:30:24 +0100 Subject: [PATCH 1/9] Add unittest skips if ncdump not installed where req. by test --- cf/test/test_read_write.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cf/test/test_read_write.py b/cf/test/test_read_write.py index 82e3e919e8..fad3c05e15 100644 --- a/cf/test/test_read_write.py +++ b/cf/test/test_read_write.py @@ -642,6 +642,8 @@ def test_read_write_unlimited(self): self.assertTrue(domain_axes["domainaxis0"].nc_is_unlimited()) self.assertTrue(domain_axes["domainaxis2"].nc_is_unlimited()) + @unittest.skipUnless( + shutil.which("ncdump"), "ncdump not available - install nco") def test_read_CDL(self): subprocess.run( " ".join(["ncdump", self.filename, ">", tmpfile]), @@ -703,6 +705,8 @@ def test_read_CDL(self): with self.assertRaises(Exception): cf.read("test_read_write.py") + @unittest.skipUnless( + shutil.which("ncdump"), "ncdump not available - install nco") def test_read_cdl_string(self): """Test the cf.read 'cdl_string' keyword.""" f = cf.read("example_field_0.nc")[0] @@ -876,6 +880,8 @@ def test_read_url(self): f = cf.read(remote) self.assertEqual(len(f), 1) + @unittest.skipUnless( + shutil.which("ncdump"), "ncdump not available - install nco") def test_read_dataset_type(self): """Test the cf.read 'dataset_type' keyword.""" # netCDF dataset From 03aef609b0d20ab22e41ba96c91ecbb7676bcb08 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 13 Oct 2025 22:48:10 +0100 Subject: [PATCH 2/9] Add unittest skips on lack of pycodestyle or scipy availablility --- cf/test/test_Data.py | 3 +++ cf/test/test_Field.py | 3 +++ cf/test/test_read_write.py | 1 + cf/test/test_style.py | 3 +++ 4 files changed, 10 insertions(+) diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index 5918fb79b6..2f9d89449c 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -5,6 +5,7 @@ import io import itertools import os +import shutil import tempfile import unittest import warnings @@ -618,6 +619,8 @@ def test_Data_apply_masking(self): self.assertTrue((b == e.array).all()) self.assertTrue((b.mask == e.mask.array).all()) + @unittest.skipUnless( + shutil.which("scipy"), "scipy not available - install it") def test_Data_convolution_filter(self): """Test the `convolution_filter` Data method.""" # raise unittest.SkipTest("GSASL has no PLAIN support") diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 881ddbb3eb..f7773dc737 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -4,6 +4,7 @@ import itertools import os import re +import shutil import tempfile import unittest @@ -1961,6 +1962,8 @@ def test_Field_autocyclic(self): def test_Field_construct_key(self): self.f.construct_key("grid_longitude") + @unittest.skipUnless( + shutil.which("scipy"), "scipy not available - install it") def test_Field_convolution_filter(self): f = cf.read(self.filename1)[0] diff --git a/cf/test/test_read_write.py b/cf/test/test_read_write.py index fad3c05e15..aaa3d69443 100644 --- a/cf/test/test_read_write.py +++ b/cf/test/test_read_write.py @@ -4,6 +4,7 @@ import inspect import os import shutil +import shutil import subprocess import tempfile import unittest diff --git a/cf/test/test_style.py b/cf/test/test_style.py index 13ab4b45ec..c3feb430f3 100644 --- a/cf/test/test_style.py +++ b/cf/test/test_style.py @@ -1,6 +1,7 @@ import datetime import faulthandler import os +import shutil import unittest import pycodestyle @@ -31,6 +32,8 @@ def setUp(self): for path in non_cf_python_files ] + @unittest.skipUnless( + shutil.which("pycodestyle"), "pycodestyle not available - install it") def test_pep8_compliance(self): pep8_check = pycodestyle.StyleGuide() From 15ddd3f71c240d24ee219920c7e4f1da49e12e0b Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 13 Oct 2025 23:26:55 +0100 Subject: [PATCH 3/9] Add unittest skips on lack of matplotlib availablility --- cf/test/test_Field.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index f7773dc737..5bd7b8f1e4 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -1153,6 +1153,8 @@ def test_Field_insert_dimension(self): with self.assertRaises(ValueError): f.insert_dimension(1, "qwerty") + @unittest.skipUnless( + importlib.util.find_spec("matplotlib"), "matplotlib needs installing") def test_Field_indices(self): f = cf.read(self.filename)[0] From 8a9a8c2d0fdaedc01ec8d1059dbad6d5853239ed Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 14 Oct 2025 00:40:14 +0100 Subject: [PATCH 4/9] Process import attempts & consolidate messages for unittest skips --- cf/test/test_Data.py | 6 ++++-- cf/test/test_Field.py | 8 ++++---- cf/test/test_read_write.py | 6 +++--- cf/test/test_style.py | 7 ++++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index 2f9d89449c..6727032c2a 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -2,6 +2,7 @@ import contextlib import datetime import faulthandler +from importlib.util import find_spec import io import itertools import os @@ -14,7 +15,6 @@ import dask.array as da import numpy as np -from scipy.ndimage import convolve1d faulthandler.enable() # to debug seg faults and timeouts @@ -620,9 +620,11 @@ def test_Data_apply_masking(self): self.assertTrue((b.mask == e.mask.array).all()) @unittest.skipUnless( - shutil.which("scipy"), "scipy not available - install it") + find_spec("scipy"), "scipy required but not installed") def test_Data_convolution_filter(self): """Test the `convolution_filter` Data method.""" + from scipy.ndimage import convolve1d + # raise unittest.SkipTest("GSASL has no PLAIN support") d = cf.Data(self.ma, units="m", chunks=(2, 4, 5, 3)) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 5bd7b8f1e4..38a13d810b 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -1,16 +1,15 @@ import atexit import datetime import faulthandler +from importlib.util import find_spec import itertools import os import re -import shutil import tempfile import unittest import numpy import numpy as np -from scipy.ndimage import convolve1d faulthandler.enable() # to debug seg faults and timeouts @@ -1154,7 +1153,7 @@ def test_Field_insert_dimension(self): f.insert_dimension(1, "qwerty") @unittest.skipUnless( - importlib.util.find_spec("matplotlib"), "matplotlib needs installing") + find_spec("matplotlib"), "matplotlib required but not installed") def test_Field_indices(self): f = cf.read(self.filename)[0] @@ -1965,8 +1964,9 @@ def test_Field_construct_key(self): self.f.construct_key("grid_longitude") @unittest.skipUnless( - shutil.which("scipy"), "scipy not available - install it") + find_spec("scipy"), "scipy required but not installed") def test_Field_convolution_filter(self): + from scipy.ndimage import convolve1d f = cf.read(self.filename1)[0] window = [0.1, 0.15, 0.5, 0.15, 0.1] diff --git a/cf/test/test_read_write.py b/cf/test/test_read_write.py index aaa3d69443..2810c335ed 100644 --- a/cf/test/test_read_write.py +++ b/cf/test/test_read_write.py @@ -644,7 +644,7 @@ def test_read_write_unlimited(self): self.assertTrue(domain_axes["domainaxis2"].nc_is_unlimited()) @unittest.skipUnless( - shutil.which("ncdump"), "ncdump not available - install nco") + shutil.which("ncdump"), "ncdump required - install nco") def test_read_CDL(self): subprocess.run( " ".join(["ncdump", self.filename, ">", tmpfile]), @@ -707,7 +707,7 @@ def test_read_CDL(self): cf.read("test_read_write.py") @unittest.skipUnless( - shutil.which("ncdump"), "ncdump not available - install nco") + shutil.which("ncdump"), "ncdump required - install nco") def test_read_cdl_string(self): """Test the cf.read 'cdl_string' keyword.""" f = cf.read("example_field_0.nc")[0] @@ -882,7 +882,7 @@ def test_read_url(self): self.assertEqual(len(f), 1) @unittest.skipUnless( - shutil.which("ncdump"), "ncdump not available - install nco") + shutil.which("ncdump"), "ncdump required - install nco") def test_read_dataset_type(self): """Test the cf.read 'dataset_type' keyword.""" # netCDF dataset diff --git a/cf/test/test_style.py b/cf/test/test_style.py index c3feb430f3..079fb92bf2 100644 --- a/cf/test/test_style.py +++ b/cf/test/test_style.py @@ -1,11 +1,10 @@ import datetime import faulthandler +from importlib.util import find_spec import os import shutil import unittest -import pycodestyle - faulthandler.enable() # to debug seg faults and timeouts import cf @@ -33,8 +32,10 @@ def setUp(self): ] @unittest.skipUnless( - shutil.which("pycodestyle"), "pycodestyle not available - install it") + find_spec("pycodestyle"), "pycodestyle required but not installed") def test_pep8_compliance(self): + import pycodestyle + pep8_check = pycodestyle.StyleGuide() # Directories to skip in the recursive walk of the directory: From b5dfe4e47c167358624cd32f0eb05c4c3699ca8a Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 14 Oct 2025 23:59:04 +0100 Subject: [PATCH 5/9] Prevent RuntimeError raised by tests in test_RegridOperator --- cf/test/test_RegridOperator.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/cf/test/test_RegridOperator.py b/cf/test/test_RegridOperator.py index dae6be8ce8..d7d59d50c0 100644 --- a/cf/test/test_RegridOperator.py +++ b/cf/test/test_RegridOperator.py @@ -7,11 +7,31 @@ import cf +# ESMF renamed its Python module to `esmpy` at ESMF version 8.4.0. Allow +# either for now for backwards compatibility. +esmpy_imported = False +try: + import esmpy + + esmpy_imported = True +except ImportError: + try: + # Take the new name to use in preference to the old one. + import ESMF as esmpy + + esmpy_imported = True + except ImportError: + pass + + class RegridOperatorTest(unittest.TestCase): - src = cf.example_field(0) - dst = cf.example_field(1) - r = src.regrids(dst, "linear", return_operator=True) + def setUp(self): + src = cf.example_field(0) + dst = cf.example_field(1) + r = src.regrids(dst, "linear", return_operator=True) + + @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") def test_RegridOperator_attributes(self): self.assertEqual(self.r.coord_sys, "spherical") self.assertEqual(self.r.method, "linear") @@ -39,6 +59,7 @@ def test_RegridOperator_attributes(self): self.assertIsNone(self.r.dst_z) self.assertFalse(self.r.ln_z) + @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") def test_RegridOperator_copy(self): self.assertIsInstance(self.r.copy(), self.r.__class__) From 5feee6c78c25809426771d413be626f6181c3ee0 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 15 Oct 2025 00:26:43 +0100 Subject: [PATCH 6/9] Move logic requiring esmpy in test_regrid_{featureType, mesh} --- cf/test/test_regrid_featureType.py | 10 +++++----- cf/test/test_regrid_mesh.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cf/test/test_regrid_featureType.py b/cf/test/test_regrid_featureType.py index 198b44dd97..3a1967f3d5 100644 --- a/cf/test/test_regrid_featureType.py +++ b/cf/test/test_regrid_featureType.py @@ -36,11 +36,6 @@ atol = 2e-12 rtol = 0 -meshloc = { - "face": esmpy.MeshLoc.ELEMENT, - "node": esmpy.MeshLoc.NODE, -} - def esmpy_regrid(coord_sys, method, src, dst, **kwargs): """Helper function that regrids one dimension of Field data using @@ -53,6 +48,11 @@ def esmpy_regrid(coord_sys, method, src, dst, **kwargs): Regridded numpy masked array. """ + meshloc = { + "face": esmpy.MeshLoc.ELEMENT, + "node": esmpy.MeshLoc.NODE, + } + esmpy_regrid = cf.regrid.regrid( coord_sys, src, diff --git a/cf/test/test_regrid_mesh.py b/cf/test/test_regrid_mesh.py index 3095640135..39e29f26fe 100644 --- a/cf/test/test_regrid_mesh.py +++ b/cf/test/test_regrid_mesh.py @@ -39,11 +39,6 @@ atol = 2e-12 rtol = 0 -meshloc = { - "face": esmpy.MeshLoc.ELEMENT, - "node": esmpy.MeshLoc.NODE, -} - def esmpy_regrid(coord_sys, method, src, dst, **kwargs): """Helper function that regrids one dimension of Field data using @@ -56,6 +51,11 @@ def esmpy_regrid(coord_sys, method, src, dst, **kwargs): Regridded numpy masked array. """ + meshloc = { + "face": esmpy.MeshLoc.ELEMENT, + "node": esmpy.MeshLoc.NODE, + } + esmpy_regrid = cf.regrid.regrid( coord_sys, src, From a5eb69a631f68ea72b5dc3596d793cd05022497c Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 15 Oct 2025 17:33:14 +0100 Subject: [PATCH 7/9] Revert skips on now-compulsory module SciPy --- cf/test/test_Data.py | 5 +---- cf/test/test_Field.py | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index 6727032c2a..96b2de0dea 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -15,6 +15,7 @@ import dask.array as da import numpy as np +from scipy.ndimage import convolve1d faulthandler.enable() # to debug seg faults and timeouts @@ -619,12 +620,8 @@ def test_Data_apply_masking(self): self.assertTrue((b == e.array).all()) self.assertTrue((b.mask == e.mask.array).all()) - @unittest.skipUnless( - find_spec("scipy"), "scipy required but not installed") def test_Data_convolution_filter(self): """Test the `convolution_filter` Data method.""" - from scipy.ndimage import convolve1d - # raise unittest.SkipTest("GSASL has no PLAIN support") d = cf.Data(self.ma, units="m", chunks=(2, 4, 5, 3)) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 38a13d810b..92a72ff8a8 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -10,6 +10,7 @@ import numpy import numpy as np +from scipy.ndimage import convolve1d faulthandler.enable() # to debug seg faults and timeouts @@ -1963,10 +1964,7 @@ def test_Field_autocyclic(self): def test_Field_construct_key(self): self.f.construct_key("grid_longitude") - @unittest.skipUnless( - find_spec("scipy"), "scipy required but not installed") def test_Field_convolution_filter(self): - from scipy.ndimage import convolve1d f = cf.read(self.filename1)[0] window = [0.1, 0.15, 0.5, 0.15, 0.1] From 55b1ca35187466d91b94b33d9b5133383b38fdfe Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 15 Oct 2025 17:39:41 +0100 Subject: [PATCH 8/9] Remove now unused imports from tweaks --- cf/test/test_Data.py | 2 -- cf/test/test_style.py | 1 - 2 files changed, 3 deletions(-) diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index 96b2de0dea..5918fb79b6 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -2,11 +2,9 @@ import contextlib import datetime import faulthandler -from importlib.util import find_spec import io import itertools import os -import shutil import tempfile import unittest import warnings diff --git a/cf/test/test_style.py b/cf/test/test_style.py index 079fb92bf2..907cfd892b 100644 --- a/cf/test/test_style.py +++ b/cf/test/test_style.py @@ -2,7 +2,6 @@ import faulthandler from importlib.util import find_spec import os -import shutil import unittest faulthandler.enable() # to debug seg faults and timeouts From 7c576cdaaee5aeb4a415bea2db536a9935fb7840 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 15 Oct 2025 17:54:49 +0100 Subject: [PATCH 9/9] Fix setUp variable unittest scoping in test_RegridOperator --- cf/test/test_RegridOperator.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/cf/test/test_RegridOperator.py b/cf/test/test_RegridOperator.py index d7d59d50c0..d92bf9b554 100644 --- a/cf/test/test_RegridOperator.py +++ b/cf/test/test_RegridOperator.py @@ -1,5 +1,6 @@ import datetime import faulthandler +from importlib.util import find_spec import unittest faulthandler.enable() # to debug seg faults and timeouts @@ -10,18 +11,10 @@ # ESMF renamed its Python module to `esmpy` at ESMF version 8.4.0. Allow # either for now for backwards compatibility. esmpy_imported = False -try: - import esmpy - +# Note: here only need esmpy for cf under-the-hood code, not in test +# directly, so no need to actually import esmpy, just test it is there. +if find_spec("esmpy") or find_spec("ESMF"): esmpy_imported = True -except ImportError: - try: - # Take the new name to use in preference to the old one. - import ESMF as esmpy - - esmpy_imported = True - except ImportError: - pass class RegridOperatorTest(unittest.TestCase): @@ -29,7 +22,7 @@ class RegridOperatorTest(unittest.TestCase): def setUp(self): src = cf.example_field(0) dst = cf.example_field(1) - r = src.regrids(dst, "linear", return_operator=True) + self.r = src.regrids(dst, "linear", return_operator=True) @unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.") def test_RegridOperator_attributes(self):