From e992174f36e5b9e14b513dd3c5f2dc6a1524e38a Mon Sep 17 00:00:00 2001 From: jaddix Date: Mon, 11 May 2026 20:22:51 -0400 Subject: [PATCH 1/5] migrated tests to use pathlib.Path --- lib/iris/tests/__init__.py | 64 +++++++++---------- lib/iris/tests/_shared_utils.py | 45 ++++++------- .../tests/integration/netcdf/test_general.py | 3 +- .../tests/integration/test_climatology.py | 15 ++--- lib/iris/tests/stock/__init__.py | 6 +- lib/iris/tests/test_cdm.py | 4 +- lib/iris/tests/test_coding_standards.py | 24 +++---- lib/iris/tests/test_netcdf.py | 1 - lib/iris/tests/test_pp_module.py | 4 +- lib/iris/tests/test_uri_callback.py | 4 +- .../unit/fileformats/dot/test__dot_path.py | 20 +++--- .../tests/unit/io/test_expand_filespecs.py | 26 ++++---- lib/iris/tests/unit/test_sample_data_path.py | 12 ++-- .../unit/util/test_file_is_newer_than.py | 4 +- 14 files changed, 114 insertions(+), 118 deletions(-) diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 77b78701eb..aa0305b0a3 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -26,7 +26,6 @@ import json import math import os -import os.path from pathlib import Path import re import shutil @@ -88,7 +87,7 @@ STRATIFY_AVAILABLE = False #: Basepath for test results. -_RESULT_PATH = os.path.join(os.path.dirname(__file__), "results") +_RESULT_PATH = Path(__file__).parent / "results" MIN_PICKLE_PROTOCOL = 4 if "--data-files-used" in sys.argv: @@ -287,34 +286,35 @@ def get_data_path(relative_path): """ if not isinstance(relative_path, str): - relative_path = os.path.join(*relative_path) + relative_path = Path(*relative_path) test_data_dir = iris.config.TEST_DATA_DIR if test_data_dir is None: test_data_dir = "" - data_path = os.path.join(test_data_dir, relative_path) + data_path = Path(test_data_dir) / relative_path + data_path_str = str(data_path) if _EXPORT_DATAPATHS_FILE is not None: - _EXPORT_DATAPATHS_FILE.write(data_path + "\n") + _EXPORT_DATAPATHS_FILE.write(data_path_str + "\n") - if isinstance(data_path, str) and not os.path.exists(data_path): + if isinstance(data_path_str, str) and not data_path.exists(): # if the file is gzipped, ungzip it and return the path of the ungzipped # file. - gzipped_fname = data_path + ".gz" - if os.path.exists(gzipped_fname): - with gzip.open(gzipped_fname, "rb") as gz_fh: + gzipped_fname = Path(data_path_str + ".gz") + if gzipped_fname.exists(): + with gzip.open(str(gzipped_fname), "rb") as gz_fh: try: - with open(data_path, "wb") as fh: + with open(data_path_str, "wb") as fh: fh.writelines(gz_fh) except IOError: # Put ungzipped data file in a temporary path, since we # can't write to the original path (maybe it is owned by # the system.) - _, ext = os.path.splitext(data_path) + ext = data_path.suffix data_path = iris.util.create_temp_filename(suffix=ext) with open(data_path, "wb") as fh: fh.writelines(gz_fh) - return data_path + return data_path_str @staticmethod def get_result_path(relative_path): @@ -323,8 +323,8 @@ def get_result_path(relative_path): """ if not isinstance(relative_path, str): - relative_path = os.path.join(*relative_path) - return os.path.abspath(os.path.join(_RESULT_PATH, relative_path)) + relative_path = Path(*relative_path) + return str(Path(_RESULT_PATH).absolute() / relative_path) def result_path(self, basename=None, ext=""): """Return the full path to a test result, generated from the \ @@ -342,8 +342,8 @@ def result_path(self, basename=None, ext=""): ext = "." + ext # Generate the folder name from the calling file name. - path = os.path.abspath(inspect.getfile(self.__class__)) - path = os.path.splitext(path)[0] + path = Path(inspect.getfile(self, __class__)).absolute().parent + sub_path = path.name sub_path = path.rsplit("iris", 1)[1].split("tests", 1)[1][1:] # Generate the file name from the calling function name? @@ -355,11 +355,11 @@ def result_path(self, basename=None, ext=""): break filename = basename + ext - result = os.path.join( - self.get_result_path(""), - sub_path.replace("test_", ""), - self.__class__.__name__.replace("Test_", ""), - filename, + result = Path( + self.get_result_path("") + / sub_path.replace("test_", "") + / self.__class__.__name__.replace("Test_", "") + / filename ) return result @@ -758,13 +758,13 @@ def _unique_id(self): # ird -t => 'iris.tests.test_plot.TestContourf.test_tx' bits = self.id().split(".") if bits[0] == "__main__": - floc = sys.modules["__main__"].__file__ - path, file_name = os.path.split(os.path.abspath(floc)) - bits[0] = os.path.splitext(file_name)[0] - folder, location = os.path.split(path) + floc = Path(sys.modules["__main__"].__file__).absolute() + path, bits[0] = floc.parent, floc.stem + + folder, location = path.parent, path.name bits = [location] + bits while location not in ["iris", "gallery_tests"]: - folder, location = os.path.split(folder) + folder, location = folder.parent, folder.name bits = [location] + bits test_id = ".".join(bits) @@ -775,16 +775,16 @@ def _unique_id(self): return test_id + "." + str(assertion_id) def _check_reference_file(self, reference_path): - reference_exists = os.path.isfile(reference_path) + reference_exists = Path(reference_path).is_file() if not (reference_exists or os.environ.get("IRIS_TEST_CREATE_MISSING")): msg = "Missing test result: {}".format(reference_path) raise AssertionError(msg) return reference_exists def _ensure_folder(self, path): - dir_path = os.path.dirname(path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) + dir_path = Path(path).parent + if not dir_path.exists(): + os.makedirs(str(dir_path)) def check_graphic(self): """Check the hash of the current matplotlib figure matches the expected @@ -988,7 +988,7 @@ def cube_save_test( """ # Watch out for a missing reference text file - if not os.path.isfile(reference_txt_path): + if not Path(reference_txt_path).is_file(): if reference_cubes: temp_pp_path = iris.util.create_temp_filename(".pp") try: @@ -1059,7 +1059,7 @@ class MyDataTests(tests.IrisTest): no_data = ( not iris.config.TEST_DATA_DIR - or not os.path.isdir(iris.config.TEST_DATA_DIR) + or not Path(iris.config.TEST_DATA_DIR).is_dir() or os.environ.get("IRIS_TEST_NO_DATA") ) diff --git a/lib/iris/tests/_shared_utils.py b/lib/iris/tests/_shared_utils.py index 3d47b0c618..cdc19904ba 100644 --- a/lib/iris/tests/_shared_utils.py +++ b/lib/iris/tests/_shared_utils.py @@ -17,7 +17,6 @@ import json import math import os -import os.path from pathlib import Path import re import shutil @@ -79,7 +78,7 @@ STRATIFY_AVAILABLE = False #: Basepath for test results. -_RESULT_PATH = os.path.join(os.path.dirname(__file__), "results") +_RESULT_PATH = Path(__file__).parent / "results" MIN_PICKLE_PROTOCOL = 4 @@ -168,34 +167,36 @@ def get_data_path(relative_path): """ if not isinstance(relative_path, str): - relative_path = os.path.join(*relative_path) + relative_path = str(Path(*relative_path)) test_data_dir = iris.config.TEST_DATA_DIR if test_data_dir is None: test_data_dir = "" - data_path = os.path.join(test_data_dir, relative_path) + + data_path = Path(test_data_dir) / relative_path + data_path_str = str(data_path) if iris.tests._EXPORT_DATAPATHS_FILE is not None: - iris.tests._EXPORT_DATAPATHS_FILE.write(data_path + "\n") + iris.tests._EXPORT_DATAPATHS_FILE.write(data_path_str + "\n") - if isinstance(data_path, str) and not os.path.exists(data_path): + if isinstance(data_path_str, str) and not data_path.exists(): # if the file is gzipped, ungzip it and return the path of the ungzipped # file. - gzipped_fname = data_path + ".gz" - if os.path.exists(gzipped_fname): - with gzip.open(gzipped_fname, "rb") as gz_fh: + gzipped_fname = Path(data_path_str + ".gz") + if gzipped_fname.exists(): + with gzip.open(str(gzipped_fname), "rb") as gz_fh: try: - with open(data_path, "wb") as fh: + with open(data_path_str, "wb") as fh: fh.writelines(gz_fh) except IOError: # Put ungzipped data file in a temporary path, since we # can't write to the original path (maybe it is owned by # the system.) - _, ext = os.path.splitext(data_path) - data_path = iris.util.create_temp_filename(suffix=ext) - with open(data_path, "wb") as fh: + ext = data_path.suffix + data_path_str = iris.util.create_temp_filename(suffix=ext) + with open(data_path_str, "wb") as fh: fh.writelines(gz_fh) - return data_path + return data_path_str def get_result_path(relative_path): @@ -204,8 +205,8 @@ def get_result_path(relative_path): """ if not isinstance(relative_path, str): - relative_path = os.path.join(*relative_path) - return os.path.abspath(os.path.join(_RESULT_PATH, relative_path)) + relative_path = Path(*relative_path) + return str((_RESULT_PATH / relative_path).absolute()) def _check_for_request_fixture(request, func_name: str): @@ -722,16 +723,16 @@ def file_checksum(file_path): def _check_reference_file(reference_path): - reference_exists = os.path.isfile(reference_path) - if not (reference_exists or os.environ.get("IRIS_TEST_CREATE_MISSING")): + reference_exists = Path(reference_path).is_file() + if not (str(reference_exists) or os.environ.get("IRIS_TEST_CREATE_MISSING")): msg = "Missing test result: {}".format(reference_path) raise AssertionError(msg) return reference_exists def _ensure_folder(path): - dir_path = os.path.dirname(path) - if not os.path.exists(dir_path): + dir_path = Path(path).parent + if not dir_path.exists(): os.makedirs(dir_path) @@ -886,7 +887,7 @@ def _create_reference_txt(txt_path, pp_path): txt_file.writelines(str(pp_fields)) # Watch out for a missing reference text file - if not os.path.isfile(reference_txt_path): + if not Path(reference_txt_path).is_file: if reference_cubes: temp_pp_path = iris.util.create_temp_filename(".pp") try: @@ -932,7 +933,7 @@ class MyDataTests(tests.IrisTest): """ no_data = ( not iris.config.TEST_DATA_DIR - or not os.path.isdir(iris.config.TEST_DATA_DIR) + or not Path(iris.config.TEST_DATA_DIR).is_dir() or os.environ.get("IRIS_TEST_NO_DATA") ) diff --git a/lib/iris/tests/integration/netcdf/test_general.py b/lib/iris/tests/integration/netcdf/test_general.py index fa544a1d67..ed125bcb13 100644 --- a/lib/iris/tests/integration/netcdf/test_general.py +++ b/lib/iris/tests/integration/netcdf/test_general.py @@ -5,7 +5,6 @@ """Integration tests for loading and saving netcdf files.""" from itertools import repeat -import os.path from pathlib import Path import warnings @@ -124,7 +123,7 @@ def test_unknown_method(self, tmp_path_factory): cube = Cube([1, 2], long_name="odd_phenomenon") cube.add_cell_method(CellMethod(method="oddity", coords=("x",))) temp_dirpath = tmp_path_factory.mktemp("test") - temp_filepath = os.path.join(temp_dirpath, "tmp.nc") + temp_filepath = str(Path(temp_dirpath) / "tmp.nc") iris.save(cube, temp_filepath) with warnings.catch_warnings(record=True) as warning_records: iris.load(temp_filepath) diff --git a/lib/iris/tests/integration/test_climatology.py b/lib/iris/tests/integration/test_climatology.py index e9de5c4b39..c434b02dcf 100644 --- a/lib/iris/tests/integration/test_climatology.py +++ b/lib/iris/tests/integration/test_climatology.py @@ -4,8 +4,7 @@ # See LICENSE in the root of the repository for full licensing details. """Integration tests for loading and saving netcdf files.""" -from os.path import dirname -from os.path import sep as os_sep +from pathlib import Path import pytest @@ -15,14 +14,10 @@ class TestClimatology: - reference_cdl_path = os_sep.join( - [ - dirname(iris.tests.__file__), - ( - "results/integration/climatology/TestClimatology/" - "reference_simpledata.cdl" - ), - ] + reference_cdl_path = str( + Path(iris.tests.__file__).parent + / "results/integration/climatology/TestClimatology/" + / "reference_simpledata.cdl" ) @classmethod diff --git a/lib/iris/tests/stock/__init__.py b/lib/iris/tests/stock/__init__.py index 31fa7e653d..f83d168468 100644 --- a/lib/iris/tests/stock/__init__.py +++ b/lib/iris/tests/stock/__init__.py @@ -7,7 +7,7 @@ import iris.tests as tests # isort:skip from datetime import datetime -import os.path +from pathlib import Path from typing import NamedTuple from cartopy.crs import CRS @@ -572,7 +572,7 @@ def realistic_4d(): """ data_path = tests.get_data_path(("stock", "stock_arrays.npz")) - if not os.path.isfile(data_path): + if not Path(data_path).is_file(): raise IOError("Test data is not available at {}.".format(data_path)) r = np.load(data_path) # sort the arrays based on the order they were originally given. @@ -671,7 +671,7 @@ def realistic_4d_no_derived(): def realistic_4d_w_missing_data(): data_path = tests.get_data_path(("stock", "stock_mdi_arrays.npz")) - if not os.path.isfile(data_path): + if not Path(data_path).is_file(): raise IOError("Test data is not available at {}.".format(data_path)) data_archive = np.load(data_path) data = ma.masked_array(data_archive["arr_0"], mask=data_archive["arr_1"]) diff --git a/lib/iris/tests/test_cdm.py b/lib/iris/tests/test_cdm.py index 15901ff342..6319b7e805 100644 --- a/lib/iris/tests/test_cdm.py +++ b/lib/iris/tests/test_cdm.py @@ -5,7 +5,7 @@ """Test cube indexing, slicing, and extracting, and also the dot graphs.""" import collections -import os +from pathlib import Path import re import cf_units @@ -27,7 +27,7 @@ class IrisDotTest: def check_dot(self, cube, reference_filename): test_string = iris.fileformats.dot.cube_text(cube) reference_path = _shared_utils.get_result_path(reference_filename) - if os.path.isfile(reference_path): + if Path(reference_path).is_file(): with open(reference_path, "r") as reference_fh: reference = "".join(reference_fh.readlines()) _shared_utils._assert_str_same( diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 58f5ef05fe..2cb621c873 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -25,15 +25,14 @@ # Guess iris repo directory of Iris - realpath is used to mitigate against # Python finding the iris package via a symlink. -IRIS_DIR = os.path.realpath(os.path.dirname(iris.__file__)) -IRIS_INSTALL_DIR = os.path.dirname(os.path.dirname(IRIS_DIR)) -DOCS_DIR = os.path.join(IRIS_INSTALL_DIR, "docs", "iris") -DOCS_DIR = iris.config.get_option("Resources", "doc_dir", default=DOCS_DIR) +IRIS_DIR = Path(iris.__file__).parent.resolve() +IRIS_INSTALL_DIR = Path(IRIS_DIR).parent.parent +DOCS_DIR = Path(IRIS_INSTALL_DIR) / "docs" / "iris" +DOCS_DIR = iris.config.get_option("Resources", "doc_dir", default=str(DOCS_DIR)) exclusion = ["Makefile", "build"] -DOCS_DIRS = glob(os.path.join(DOCS_DIR, "*")) -DOCS_DIRS = [ - DOC_DIR for DOC_DIR in DOCS_DIRS if os.path.basename(DOC_DIR) not in exclusion -] +DOCS_DIRS = glob(str(Path(DOCS_DIR) / "*")) +DOCS_DIRS = [DOC_DIR for DOC_DIR in DOCS_DIRS if Path(DOC_DIR).name not in exclusion] + # Get a dirpath to the git repository : allow setting with an environment # variable, so Travis can test for headers in the repo, not the installation. IRIS_REPO_DIRPATH = os.environ.get("IRIS_REPO_DIR", IRIS_INSTALL_DIR) @@ -220,7 +219,7 @@ def last_change_by_fname(): """ # Check the ".git" folder exists at the repo dir. - if not os.path.isdir(os.path.join(IRIS_REPO_DIRPATH, ".git")): + if not (Path(IRIS_REPO_DIRPATH) / ".git").is_dir(): msg = "{} is not a git repository." raise ValueError(msg.format(IRIS_REPO_DIRPATH)) @@ -262,10 +261,13 @@ def test_license_headers(self): failed = False for fname, last_change in sorted(last_change_by_fname.items()): - full_fname = os.path.join(IRIS_REPO_DIRPATH, fname) + full_fname = Path(IRIS_REPO_DIRPATH) / fname + is_file = full_fname.is_file() + full_fname = str(full_fname) + if ( full_fname.endswith(".py") - and os.path.isfile(full_fname) + and is_file and not any(fnmatch(fname, pat) for pat in exclude_patterns) ): with open(full_fname) as fh: diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index dfa8556dc7..2605343c4d 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -5,7 +5,6 @@ """Test CF-NetCDF file loading and saving.""" import os -import os.path import shutil import stat diff --git a/lib/iris/tests/test_pp_module.py b/lib/iris/tests/test_pp_module.py index 2100e0b132..b50493545d 100644 --- a/lib/iris/tests/test_pp_module.py +++ b/lib/iris/tests/test_pp_module.py @@ -4,7 +4,7 @@ # See LICENSE in the root of the repository for full licensing details. from copy import deepcopy -import os +from pathlib import Path from types import GeneratorType import cftime @@ -68,7 +68,7 @@ def check_pp(self, pp_fields, reference_filename): with np.printoptions(legacy=NP_PRINTOPTIONS_LEGACY): test_string = str(pp_fields) reference_path = _shared_utils.get_result_path(reference_filename) - if os.path.isfile(reference_path): + if Path(reference_path).is_file(): with open(reference_path, "r") as reference_fh: reference = "".join(reference_fh.readlines()) _shared_utils._assert_str_same( diff --git a/lib/iris/tests/test_uri_callback.py b/lib/iris/tests/test_uri_callback.py index 24aed18044..72220945fa 100644 --- a/lib/iris/tests/test_uri_callback.py +++ b/lib/iris/tests/test_uri_callback.py @@ -3,7 +3,7 @@ # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -import os +from pathlib import Path import iris from iris.tests import _shared_utils @@ -13,7 +13,7 @@ class TestCallbacks: def test_pp_callback(self, request): def pp_callback(cube, field, filename): - cube.attributes["filename"] = os.path.basename(filename) + cube.attributes["filename"] = Path(filename).name cube.attributes["lbyr"] = field.lbyr fname = _shared_utils.get_data_path(("PP", "aPPglob1", "global.pp")) diff --git a/lib/iris/tests/unit/fileformats/dot/test__dot_path.py b/lib/iris/tests/unit/fileformats/dot/test__dot_path.py index c01e1516b0..8e8d167887 100644 --- a/lib/iris/tests/unit/fileformats/dot/test__dot_path.py +++ b/lib/iris/tests/unit/fileformats/dot/test__dot_path.py @@ -4,7 +4,7 @@ # See LICENSE in the root of the repository for full licensing details. """Unit tests for :func:`iris.fileformats.dot._dot_path`.""" -import os.path +from pathlib import Path import subprocess import pytest @@ -26,25 +26,25 @@ def _setup(self, mocker): def test_valid_absolute_path(self, mocker): # Override the configuration value for System.dot_path - real_path = os.path.abspath(__file__) - assert os.path.exists(real_path) - assert os.path.isabs(real_path) + real_path = Path(__file__).absolute() + assert real_path.exists() + assert real_path.is_absolute() mocker.patch("iris.config.get_option", return_value=real_path) result = _dot_path() assert result == real_path def test_invalid_absolute_path(self, mocker): # Override the configuration value for System.dot_path - dummy_path = "/not_a_real_path" * 10 - assert not os.path.exists(dummy_path) + dummy_path = Path("/not_a_real_path" * 10) + assert not dummy_path.exists() mocker.patch("iris.config.get_option", return_value=dummy_path) result = _dot_path() assert result is None def test_valid_relative_path(self, mocker): # Override the configuration value for System.dot_path - dummy_path = "not_a_real_path" * 10 - assert not os.path.exists(dummy_path) + dummy_path = Path("not_a_real_path" * 10) + assert not dummy_path.exists() mocker.patch("iris.config.get_option", return_value=dummy_path) # Pretend we have a valid installation of dot mocker.patch("subprocess.check_output") @@ -53,8 +53,8 @@ def test_valid_relative_path(self, mocker): def test_valid_relative_path_broken_install(self, mocker): # Override the configuration value for System.dot_path - dummy_path = "not_a_real_path" * 10 - assert not os.path.exists(dummy_path) + dummy_path = Path("not_a_real_path" * 10) + assert not dummy_path.exists() mocker.patch("iris.config.get_option", return_value=dummy_path) # Pretend we have a broken installation of dot error = subprocess.CalledProcessError(-5, "foo", "bar") diff --git a/lib/iris/tests/unit/io/test_expand_filespecs.py b/lib/iris/tests/unit/io/test_expand_filespecs.py index b288901c4e..0a33f87c15 100644 --- a/lib/iris/tests/unit/io/test_expand_filespecs.py +++ b/lib/iris/tests/unit/io/test_expand_filespecs.py @@ -24,13 +24,13 @@ def _setup(self, tmp_path): file_path.write_text("anything") def test_absolute_path(self): - result = iio.expand_filespecs([os.path.join(self.tmpdir, "*")]) - expected = [os.path.join(self.tmpdir, fname) for fname in self.fnames] + result = iio.expand_filespecs([str(Path(self.tmpdir) / "*")]) + expected = [str(Path(self.tmpdir) / fname) for fname in self.fnames] assert result == expected def test_double_slash(self): - product = iio.expand_filespecs(["//" + os.path.join(self.tmpdir, "*")]) - predicted = [os.path.join(self.tmpdir, fname) for fname in self.fnames] + product = iio.expand_filespecs([str("//" / Path(self.tmpdir) / "*")]) + predicted = [str(Path(self.tmpdir) / fname) for fname in self.fnames] assert product == predicted def test_relative_path(self): @@ -38,7 +38,7 @@ def test_relative_path(self): try: os.chdir(self.tmpdir) item_out = iio.expand_filespecs(["*"]) - item_in = [os.path.join(self.tmpdir, fname) for fname in self.fnames] + item_in = [str(Path(self.tmpdir) / fname) for fname in self.fnames] assert item_out == item_in finally: os.chdir(cwd) @@ -50,10 +50,10 @@ def test_return_order(self): # this can be used with PP files to ensure that there is # a surface reference). patterns = [ - os.path.join(self.tmpdir, "a.*"), - os.path.join(self.tmpdir, "b.*"), + str(Path(self.tmpdir) / "a.*"), + str(Path(self.tmpdir) / "b.*"), ] - expected = [os.path.join(self.tmpdir, fname) for fname in ["a.foo", "b.txt"]] + expected = [str(Path(self.tmpdir) / fname) for fname in ["a.foo", "b.txt"]] result = iio.expand_filespecs(patterns) assert result == expected result = iio.expand_filespecs(patterns[::-1]) @@ -62,7 +62,7 @@ def test_return_order(self): def test_no_files_found(self): msg = r"\/no_exist.txt\" didn\'t match any files" with pytest.raises(IOError, match=msg): - iio.expand_filespecs([os.path.join(self.tmpdir, "no_exist.txt")]) + iio.expand_filespecs([str(Path(self.tmpdir) / "no_exist.txt")]) def test_files_and_none(self): emsg = ( @@ -79,13 +79,13 @@ def test_files_and_none(self): with pytest.raises(IOError, match=re.escape(emsg)): iio.expand_filespecs( [ - os.path.join(self.tmpdir, "does_not_exist.txt"), - os.path.join(self.tmpdir, "*"), + str(Path(self.tmpdir) / "does_not_exist.txt"), + str(Path(self.tmpdir) / "*"), ] ) def test_false_bool_absolute(self): - msg = os.path.join(self.tmpdir, "no_exist.txt") + msg = str(Path(self.tmpdir) / "no_exist.txt") (result,) = iio.expand_filespecs([msg], False) assert result == msg @@ -101,7 +101,7 @@ def test_false_bool_relative(self): try: os.chdir(self.tmpdir) item_out = iio.expand_filespecs(["no_exist.txt"], False) - item_in = [os.path.join(self.tmpdir, "no_exist.txt")] + item_in = [str(Path(self.tmpdir) / "no_exist.txt")] assert item_out == item_in finally: os.chdir(cwd) diff --git a/lib/iris/tests/unit/test_sample_data_path.py b/lib/iris/tests/unit/test_sample_data_path.py index 77847060c9..110f251198 100644 --- a/lib/iris/tests/unit/test_sample_data_path.py +++ b/lib/iris/tests/unit/test_sample_data_path.py @@ -5,7 +5,7 @@ """Unit tests for :func:`iris.sample_data_path` class.""" import os -import os.path +from pathlib import Path import pytest @@ -30,7 +30,7 @@ def test_call(self, mocker): sample_file.touch() mocker.patch("iris_sample_data.path", self.sample_dir) - result = sample_data_path(os.path.basename(sample_file)) + result = sample_data_path(sample_file.name) assert result == str(sample_file) def test_file_not_found(self, mocker): @@ -41,16 +41,16 @@ def test_file_not_found(self, mocker): def test_file_absolute(self, mocker): mocker.patch("iris_sample_data.path", self.sample_dir) with pytest.raises(ValueError, match="Absolute path"): - sample_data_path(os.path.abspath("foo")) + sample_data_path(Path("foo").absolute()) def test_glob_ok(self, mocker): sample_path = self.sample_dir / "sample.txt" sample_path.touch() - sample_glob = "?" + os.path.basename(sample_path)[1:] + sample_glob = "?" + Path(sample_path).name[1:] mocker.patch("iris_sample_data.path", self.sample_dir) result = sample_data_path(sample_glob) - assert result == os.path.join(self.sample_dir, sample_glob) + assert result == str(Path(self.sample_dir) / sample_glob) def test_glob_not_found(self, mocker): mocker.patch("iris_sample_data.path", self.sample_dir) @@ -60,7 +60,7 @@ def test_glob_not_found(self, mocker): def test_glob_absolute(self, mocker): mocker.patch("iris_sample_data.path", self.sample_dir) with pytest.raises(ValueError, match="Absolute path"): - sample_data_path(os.path.abspath("foo.*")) + sample_data_path(Path("foo.*").absolute()) class TestIrisSampleDataMissing: diff --git a/lib/iris/tests/unit/util/test_file_is_newer_than.py b/lib/iris/tests/unit/util/test_file_is_newer_than.py index fa63c2aaaa..593337b82e 100644 --- a/lib/iris/tests/unit/util/test_file_is_newer_than.py +++ b/lib/iris/tests/unit/util/test_file_is_newer_than.py @@ -5,7 +5,7 @@ """Test function :func:`iris.util.test_file_is_newer`.""" import os -import os.path +from pathlib import Path import pytest @@ -17,7 +17,7 @@ class TestFileIsNewer: def _name2path(self, filename): """Add the temporary dirpath to a filename to make a full path.""" - return os.path.join(self.temp_dir, filename) + return str(Path(self.temp_dir) / filename) @pytest.fixture(autouse=True) def _setup(self, tmp_path): From 62331e034afd785130d4212ced886bf3ef360716 Mon Sep 17 00:00:00 2001 From: jaddix Date: Tue, 12 May 2026 21:34:15 -0400 Subject: [PATCH 2/5] migrated to pathlib.Path in lib --- lib/iris/__init__.py | 10 +++++----- lib/iris/config.py | 19 ++++++++++--------- lib/iris/fileformats/abf.py | 6 +++--- lib/iris/fileformats/cf.py | 7 +++++-- lib/iris/fileformats/dot.py | 7 ++++--- lib/iris/fileformats/netcdf/saver.py | 8 ++++---- lib/iris/fileformats/um/_fast_load.py | 4 ++-- lib/iris/io/__init__.py | 6 +++--- lib/iris/io/format_picker.py | 4 ++-- lib/iris/palette.py | 8 ++++---- 10 files changed, 42 insertions(+), 37 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 90f8c1f51a..e7c76e8512 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -50,7 +50,7 @@ Filenames can contain `~` or `~user` abbreviations, and/or Unix shell-style wildcards (e.g. `*` and `?`). See the - standard library function :func:`os.path.expanduser` and + standard library function :func:`pathlib.Path(path).expanduser()` and module :mod:`fnmatch` for more details. .. warning:: @@ -101,7 +101,7 @@ def callback(cube, field, filename): import contextlib import glob import importlib -import os.path +from pathlib import Path import threading from typing import Callable, Literal @@ -340,8 +340,8 @@ def sample_data_path(*path_to_join): appropriate for general file access. """ - target = os.path.join(*path_to_join) - if os.path.isabs(target): + target = Path(*path_to_join) + if target.is_absolute(): raise ValueError( "Absolute paths, such as {!r}, are not supported.\n" "NB. This function is only for locating files in the " @@ -349,7 +349,7 @@ def sample_data_path(*path_to_join): "appropriate for general file access.".format(target) ) if iris_sample_data is not None: - target = os.path.join(iris_sample_data.path, target) + target = str(Path(iris_sample_data.path) / target) else: raise ImportError( "Please install the 'iris-sample-data' package to access sample data." diff --git a/lib/iris/config.py b/lib/iris/config.py index e99beb351d..ed374dfaa3 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -33,7 +33,8 @@ import configparser import contextlib import logging -import os.path +import os +from pathlib import Path import warnings import iris.warnings @@ -135,7 +136,7 @@ def get_dir_option(section, option, default=None): path = default if config.has_option(section, option): c_path = config.get(section, option) - if os.path.isdir(c_path): + if Path(c_path).is_dir(): path = c_path else: msg = ( @@ -150,14 +151,14 @@ def get_dir_option(section, option, default=None): # Figure out the full path to the "iris" package. -ROOT_PATH = os.path.abspath(os.path.dirname(__file__)) +ROOT_PATH = Path(__file__).parent.absolute() # The full path to the configuration directory of the active Iris instance. -CONFIG_PATH = os.path.join(ROOT_PATH, "etc") +CONFIG_PATH = ROOT_PATH / "etc" # Load the optional "site.cfg" file if it exists. config = configparser.ConfigParser() -config.read([os.path.join(CONFIG_PATH, "site.cfg")]) +config.read([CONFIG_PATH / "site.cfg"]) ################## # Resource options @@ -167,7 +168,7 @@ def get_dir_option(section, option, default=None): TEST_DATA_DIR = get_dir_option( _RESOURCE_SECTION, "test_data_dir", - default=os.path.join(os.path.dirname(__file__), "test_data"), + default=str(Path(__file__) / "test_data"), ) # Override the data repository if the appropriate environment variable @@ -175,11 +176,11 @@ def get_dir_option(section, option, default=None): override = os.environ.get("OVERRIDE_TEST_DATA_REPOSITORY") if override: TEST_DATA_DIR = None - if os.path.isdir(os.path.expanduser(override)): - TEST_DATA_DIR = os.path.abspath(override) + if Path(override).expanduser().is_dir(): + TEST_DATA_DIR = str(Path(override).absolute()) PALETTE_PATH = get_dir_option( - _RESOURCE_SECTION, "palette_path", os.path.join(CONFIG_PATH, "palette") + _RESOURCE_SECTION, "palette_path", CONFIG_PATH / "palette" ) # Runtime options diff --git a/lib/iris/fileformats/abf.py b/lib/iris/fileformats/abf.py index c27da55a0f..231cee1acc 100644 --- a/lib/iris/fileformats/abf.py +++ b/lib/iris/fileformats/abf.py @@ -20,7 +20,7 @@ import calendar import datetime import glob -import os.path +from pathlib import Path import numpy as np import numpy.ma as ma @@ -82,7 +82,7 @@ def __init__(self, filename): field = ABFField("AVHRRBUVI01.1985feba.abl") """ - basename = os.path.basename(filename) + basename = Path(filename).name if len(basename) != 24: raise ValueError( "ABFField expects a filename of 24 characters: {}".format(basename) @@ -100,7 +100,7 @@ def __getattr__(self, key): def _read(self): """Read the field from the given filename.""" - basename = os.path.basename(self._filename) + basename = Path(self._filename).name self.version = int(basename[9:11]) self.year = int(basename[12:16]) self.month = basename[16:19] diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 308ce381ee..c2a22ce5e0 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -21,7 +21,7 @@ from abc import ABCMeta, abstractmethod from collections.abc import Iterable, MutableMapping -import os +from pathlib import Path import re from typing import ClassVar, Optional import warnings @@ -1365,7 +1365,10 @@ def __init__(self, file_source, warn=False, monotonic=False): self._own_file = False if isinstance(file_source, str): # Create from filepath : open it + own it (=close when we die). - self._filename = os.path.expanduser(file_source) + self._filename = Path(file_source).expanduser() + if file_source.startswith("https:"): + self._filename = file_source + self._dataset = _thread_safe_nc.DatasetWrapper(self._filename, mode="r") self._own_file = True else: diff --git a/lib/iris/fileformats/dot.py b/lib/iris/fileformats/dot.py index b1047bcffe..5aa6a54b0e 100644 --- a/lib/iris/fileformats/dot.py +++ b/lib/iris/fileformats/dot.py @@ -11,6 +11,7 @@ """ import os +from pathlib import Path import subprocess import iris @@ -40,8 +41,8 @@ def _dot_path(): path = _DOT_EXECUTABLE_PATH else: path = iris.config.get_option("System", "dot_path", default="dot") - if not os.path.exists(path): - if not os.path.isabs(path): + if not Path(path).exists(): + if not Path(path).is_absolute(): try: # Check PATH subprocess.check_output([path, "-V"], stderr=subprocess.STDOUT) @@ -49,7 +50,7 @@ def _dot_path(): path = None else: path = None - _DOT_EXECUTABLE_PATH = path + _DOT_EXECUTABLE_PATH = str(path) _DOT_CHECKED = True return path diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 31a685f8ee..696f7f0dda 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -22,7 +22,7 @@ import collections from itertools import repeat, zip_longest import os -import os.path +from pathlib import Path import re import string import typing @@ -418,13 +418,13 @@ def __init__(self, filename, netcdf_format, compute=True): else: # Given a filepath string/path : create a dataset from that try: - self.filepath = os.path.abspath(filename) + self.filepath = Path(filename) self._dataset = _thread_safe_nc.DatasetWrapper( self.filepath, mode="w", format=netcdf_format ) except RuntimeError: - dir_name = os.path.dirname(self.filepath) - if not os.path.isdir(dir_name): + dir_name = self.filepath.parent + if not dir_name.is_dir(): msg = "No such file or directory: {}".format(dir_name) raise IOError(msg) if not os.access(dir_name, os.R_OK | os.W_OK): diff --git a/lib/iris/fileformats/um/_fast_load.py b/lib/iris/fileformats/um/_fast_load.py index 12441acdcc..45b784ac75 100644 --- a/lib/iris/fileformats/um/_fast_load.py +++ b/lib/iris/fileformats/um/_fast_load.py @@ -21,7 +21,7 @@ """ from contextlib import contextmanager -import os.path +from pathlib import Path import threading import numpy as np @@ -120,7 +120,7 @@ def _select_raw_fields_loader(fname): from iris.fileformats.um import um_to_pp with open(fname, "rb") as fh: - spec = FORMAT_AGENT.get_spec(os.path.basename(fname), fh) + spec = FORMAT_AGENT.get_spec(Path(fname).name, fh) if spec.name.startswith(_FF_SPEC_NAME): loader = um_to_pp elif spec.name.startswith(_PP_SPEC_NAME): diff --git a/lib/iris/io/__init__.py b/lib/iris/io/__init__.py index 0a7cdd9abb..1a895ff1a3 100644 --- a/lib/iris/io/__init__.py +++ b/lib/iris/io/__init__.py @@ -14,7 +14,6 @@ import collections from collections import OrderedDict import glob -import os.path import pathlib import re @@ -168,7 +167,8 @@ def expand_filespecs(file_specs, files_expected=True): """ # Remove any hostname component - currently unused filenames = [ - os.path.abspath(os.path.expanduser(fn.removeprefix("//"))) for fn in file_specs + str(pathlib.Path(fn.removeprefix("//")).expanduser().absolute()) + for fn in file_specs ] if files_expected: @@ -214,7 +214,7 @@ def load_files(filenames, callback, constraints=None): handler_map = collections.defaultdict(list) for fn in all_file_paths: with open(fn, "rb") as fh: - handling_format_spec = FORMAT_AGENT.get_spec(os.path.basename(fn), fh) + handling_format_spec = FORMAT_AGENT.get_spec(pathlib.Path(fn).name, fh) handler_map[handling_format_spec].append(fn) # Call each iris format handler with the appropriate filenames diff --git a/lib/iris/io/format_picker.py b/lib/iris/io/format_picker.py index 3f93b5cfd6..cec5855c11 100644 --- a/lib/iris/io/format_picker.py +++ b/lib/iris/io/format_picker.py @@ -48,7 +48,7 @@ from collections.abc import Callable import functools -import os +from pathlib import Path import struct @@ -348,7 +348,7 @@ class FileExtension(FileElement): def get_element(self, basename, file_handle): # noqa D102 - return os.path.splitext(basename)[1] + return Path(basename).suffix class LeadingLine(FileElement): diff --git a/lib/iris/palette.py b/lib/iris/palette.py index 7f8046fbf9..6043e75a17 100644 --- a/lib/iris/palette.py +++ b/lib/iris/palette.py @@ -15,7 +15,7 @@ from functools import wraps import os -import os.path +from pathlib import Path import re import cf_units @@ -248,15 +248,15 @@ def _load_palette(): # Identify any target .txt color map palette files. filenames.extend( [ - os.path.join(root, filename) + str(Path(root) / filename) for filename in files - if os.path.splitext(filename)[1] == ".txt" + if Path(filename).suffix == ".txt" ] ) for filename in filenames: # Default color map name based on the file base-name (case-SENSITIVE). - cmap_name = os.path.splitext(os.path.basename(filename))[0] + cmap_name = Path(filename).stem cmap_scheme = None cmap_keywords = [] cmap_std_names = [] From d2bb70f0331aceb9a594ce051043e8e0fee60e8c Mon Sep 17 00:00:00 2001 From: jaddix Date: Tue, 12 May 2026 22:15:17 -0400 Subject: [PATCH 3/5] added entry to latest.rst --- docs/src/whatsnew/latest.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 3e6d53fa52..32819262e5 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -102,6 +102,8 @@ This document explains the changes made to Iris for this release except for mo_pack. (:issue:`6832`, :pull:`6976`) +#. `@SgtVarmint` _ migrated codebase from os.path to pathlib.Path where possible + (:issue:`4523`, :pull:`7087`) .. comment Whatsnew author names (@github name) in alphabetical order. Note that, core dev names are automatically included by the common_links.inc: From 2af71ea30807730096e9f218c9ccc3844c28bdc9 Mon Sep 17 00:00:00 2001 From: jaddix Date: Tue, 12 May 2026 22:20:55 -0400 Subject: [PATCH 4/5] code cleanup around os.path usage --- benchmarks/benchmarks/sperf/combine_regions.py | 2 -- docs/gallery_code/meteorology/plot_COP_maps.py | 4 ++-- docs/src/conf.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/benchmarks/benchmarks/sperf/combine_regions.py b/benchmarks/benchmarks/sperf/combine_regions.py index 591b7bb9be..7a1d6d9d44 100644 --- a/benchmarks/benchmarks/sperf/combine_regions.py +++ b/benchmarks/benchmarks/sperf/combine_regions.py @@ -4,8 +4,6 @@ # See LICENSE in the root of the repository for full licensing details. """Region combine benchmarks for the SPerf scheme of the UK Met Office's NG-VAT project.""" -import os.path - from dask import array as da import numpy as np diff --git a/docs/gallery_code/meteorology/plot_COP_maps.py b/docs/gallery_code/meteorology/plot_COP_maps.py index fca40dc373..79ebcad96d 100644 --- a/docs/gallery_code/meteorology/plot_COP_maps.py +++ b/docs/gallery_code/meteorology/plot_COP_maps.py @@ -26,7 +26,7 @@ """ # noqa: D205, D212, D400 -import os.path +from pathlib import Path import matplotlib.pyplot as plt import numpy as np @@ -40,7 +40,7 @@ def cop_metadata_callback(cube, field, filename): """Add an "Experiment" coordinate which comes from the filename.""" # Extract the experiment name (such as A1B or E1) from the filename (in # this case it is just the start of the file name, before the first "."). - fname = os.path.basename(filename) # filename without path. + fname = Path(filename).name # filename without path. experiment_label = fname.split(".")[0] # Create a coordinate with the experiment label in it... diff --git a/docs/src/conf.py b/docs/src/conf.py index 19fe71f5cc..03f5efe364 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -79,13 +79,13 @@ def autolog(message): # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. +# documentation root, use pathlib.Path().absolute() to make it absolute, like shown here. # custom sphinx extensions -sys.path.append(os.path.abspath("sphinxext")) +sys.path.append(str(Path("sphinxtext").absolute())) # add some sample files from the developers guide.. -sys.path.append(os.path.abspath(os.path.join("developers_guide"))) +sys.path.append(str(Path("developers_guide").absolute())) # why isn't the iris path added to it is discoverable too? We dont need to, # the sphinext to generate the api rst knows where the source is. If it From 958571ff3de258ca22ff49044073c22ea4bb5d45 Mon Sep 17 00:00:00 2001 From: jaddix Date: Tue, 12 May 2026 22:32:48 -0400 Subject: [PATCH 5/5] fixed typo in conf.py --- docs/src/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index 03f5efe364..d5f138d7b9 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -82,7 +82,7 @@ def autolog(message): # documentation root, use pathlib.Path().absolute() to make it absolute, like shown here. # custom sphinx extensions -sys.path.append(str(Path("sphinxtext").absolute())) +sys.path.append(str((Path("sphinxext").absolute()))) # add some sample files from the developers guide.. sys.path.append(str(Path("developers_guide").absolute()))