From 1fb4746b24fabb49bbc0da547ca39efa53d2d6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 28 Oct 2024 12:24:20 +0100 Subject: [PATCH] ENH: support pathlib.Path everywhere --- docs/source/conf.py | 5 +- pyproject.toml | 3 +- src/cmasher/app_usage.py | 14 ++--- src/cmasher/cli_tools.py | 5 +- src/cmasher/colormaps/prep_cmap_data.py | 42 +++++++-------- src/cmasher/utils.py | 69 +++++++++++++------------ tests/test_utils.py | 19 ++++--- 7 files changed, 81 insertions(+), 76 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4e81bd3..d900549 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,12 +11,11 @@ # 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. # -import os import sys -from codecs import open from importlib.metadata import version as md_version +from pathlib import Path -sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, str(Path().parents[1].resolve())) # -- Project information ----------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d622f01..4752f1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ ignore = [ "E226", "F401", "F403", + "PTH123", # builtin-open ] select = [ "E", @@ -108,8 +109,8 @@ select = [ "I", # isort "UP", # pyupgrade "NPY", # numpy specific rules + "PTH", # flake8-use-pathlib ] - [tool.ruff.lint.isort] combine-as-imports = true known-first-party = ["cmasher"] diff --git a/src/cmasher/app_usage.py b/src/cmasher/app_usage.py index b2101fa..37a92b1 100644 --- a/src/cmasher/app_usage.py +++ b/src/cmasher/app_usage.py @@ -8,8 +8,9 @@ # %% IMPORTS # Built-in imports +import os import re -from os import path +from pathlib import Path from textwrap import dedent, indent # Import packages @@ -21,7 +22,7 @@ # %% FUNCTION DEFINITIONS # Define function that generates a Tableau properties file with colormaps -def update_tableau_pref_file(dirname: str = ".") -> None: +def update_tableau_pref_file(dirname: str | os.PathLike[str] = ".") -> None: """ Update an existing Tableau 'Preferences.tps' file to include colormaps from *CMasher*. @@ -30,7 +31,7 @@ def update_tableau_pref_file(dirname: str = ".") -> None: Optional -------- - dirname : str. Default: '.' + dirname : str or os.PathLike[str] Default: '.' The relative or absolute path to the directory where the Tableau preferences file should be updated. If `dirname` contains an existing file called 'Preferences.tps', it @@ -89,10 +90,10 @@ def update_tableau_pref_file(dirname: str = ".") -> None: entries_dict[cmap.name] = entry_str # Obtain absolute path to preferences file in provided dirname - filename = path.abspath(path.join(dirname, "Preferences.tps")) + filename = Path(dirname).joinpath("Preferences.tps").resolve() # Check if this file already exists - if path.exists(filename): + if filename.exists(): # If so, read in the file contents with open(filename) as f: text = f.read() @@ -184,5 +185,4 @@ def update_tableau_pref_file(dirname: str = ".") -> None: ).format(entries_str)[1:] # Create this file - with open(filename, "w") as f: - f.write(pref_file) + filename.write_text(pref_file) diff --git a/src/cmasher/cli_tools.py b/src/cmasher/cli_tools.py index ef40d16..97a26fa 100644 --- a/src/cmasher/cli_tools.py +++ b/src/cmasher/cli_tools.py @@ -5,6 +5,7 @@ import re import sys from importlib import import_module +from pathlib import Path from textwrap import dedent # Package imports @@ -220,7 +221,7 @@ def cli_app_usage_tableau(): cmr.app_usage.update_tableau_pref_file(dirname=ARGS.dir) # Print on commandline that properties file was created/updated - print(f"Created/Updated Tableau preferences file in {os.path.abspath(ARGS.dir)!r}.") + print(f"Created/Updated Tableau preferences file in {ARGS.dir.resolve()!r}.") # This function handles the 'lang_usage r' subcommand @@ -619,7 +620,7 @@ def main(): help="Path to directory where the module must be saved.", action="store", default=cmr.create_cmap_mod.__kwdefaults__["save_dir"], - type=str, + type=Path, ) # Set defaults for mk_cmod_parser diff --git a/src/cmasher/colormaps/prep_cmap_data.py b/src/cmasher/colormaps/prep_cmap_data.py index 793d722..f59783f 100644 --- a/src/cmasher/colormaps/prep_cmap_data.py +++ b/src/cmasher/colormaps/prep_cmap_data.py @@ -5,6 +5,7 @@ import sys from itertools import zip_longest from os import path +from pathlib import Path from textwrap import dedent import matplotlib.pyplot as plt @@ -26,15 +27,13 @@ ) # %% GLOBALS -docs_dir = path.abspath(path.join(path.dirname(__file__), "../../docs/source/user")) +docs_dir = Path(__file__).parents[2].joinpath("docs", "source", "user").resolve() # %% FUNCTION DEFINITIONS def create_cmap_app_overview(): # Load sequential image data - image_seq = np.loadtxt( - path.join(path.dirname(__file__), "app_data.txt.gz"), dtype=int - ) + image_seq = np.loadtxt(Path(__file__).parent / "app_data.txt.gz", dtype=int) # Obtain resolution ratio image_ratio = image_seq.shape[0] / image_seq.shape[1] @@ -162,8 +161,8 @@ def create_cmap_app_overview(): ) # Obtain figure path - fig_path_100 = path.join(docs_dir, "images", "cmr_cmaps_app_100.png") - fig_path_250 = path.join(docs_dir, "../_static", "cmr_cmaps_app_250.png") + fig_path_100 = docs_dir / "images" / "cmr_cmaps_app_100.png" + fig_path_250 = docs_dir.parent / "_static" / "cmr_cmaps_app_250.png" # Save the figure plt.savefig(fig_path_100, dpi=100) @@ -176,17 +175,17 @@ def create_cmap_app_overview(): # %% MAIN SCRIPT if __name__ == "__main__": # Obtain path to .jscm-file - jscm_path = path.abspath(sys.argv[1]) + jscm_path = Path(sys.argv[1]).resolve() # If this path does not exist, try again with added 'PROJECTS' - if not path.exists(jscm_path): - jscm_path = path.abspath(path.join("PROJECTS", sys.argv[1])) + if not jscm_path.exists(): + jscm_path = Path("PROJECTS", sys.argv[1]) # Get colormap name - name = path.splitext(path.basename(jscm_path))[0] + name = jscm_path.stem # Make a directory for the colormap files - os.mkdir(name) + Path(name).mkdir() # Move the .jscm-file to it shutil.move(jscm_path, name) @@ -248,10 +247,10 @@ def create_cmap_app_overview(): # Make new colormap overview create_cmap_overview(savefig="cmap_overview.png", sort="lightness") create_cmap_overview( - savefig=path.join(docs_dir, "images", "cmap_overview.png"), sort="lightness" + savefig=docs_dir / "images" / "cmap_overview.png", sort="lightness" ) create_cmap_overview( - savefig=path.join(docs_dir, "images", "cmap_overview_perceptual.png"), + savefig=docs_dir / "images" / "cmap_overview_perceptual.png", sort="perceptual", show_info=True, ) @@ -259,7 +258,7 @@ def create_cmap_app_overview(): plt.colormaps(), plot_profile=True, sort="lightness", - savefig=path.join(docs_dir, "images", "mpl_cmaps.png"), + savefig=docs_dir / "images" / "mpl_cmaps.png", ) create_cmap_app_overview() @@ -296,7 +295,7 @@ def create_cmap_app_overview(): cmaps, sort="perceptual", use_types=(cmtype == "diverging"), - savefig=path.join(docs_dir, "images", f"{cmtype[:3]}_cmaps.png"), + savefig=docs_dir / "images" / f"{cmtype[:3]}_cmaps.png", title=f"{cmtype.capitalize()} Colormaps", show_info=True, ) @@ -306,16 +305,17 @@ def create_cmap_app_overview(): cmaps, use_types=False, title="Sequential MPL Colormaps", - savefig=path.join(docs_dir, "images", "seq_mpl_cmaps.png"), + savefig=docs_dir / "images" / "seq_mpl_cmaps.png", ) # Update Tableau preferences file - update_tableau_pref_file(path.join(docs_dir, "../_static")) + update_tableau_pref_file(docs_dir.parent / "_static") # Create docs entry for this colormap if possible try: # Create docs entry - with open(path.join(docs_dir, cmtype, f"{name}.rst"), "x") as f: + + with docs_dir.joinpath(cmtype, f"{name}.rst").open("x") as f: f.write(docs_entry[1:]) # If this file already exists, then skip except FileExistsError: @@ -323,8 +323,7 @@ def create_cmap_app_overview(): # If the file did not exist yet, add it to the corresponding overview else: # Read the corresponding docs overview page - with open(path.join(docs_dir, f"{cmtype}.rst")) as f: - docs_overview = f.read() + docs_overview = docs_dir.joinpath(f"{cmtype}.rst").read_text() # Set the string used to start the toctree with toctree_header = ".. toctree::\n :caption: Individual colormaps\n\n" @@ -348,8 +347,7 @@ def create_cmap_app_overview(): docs_overview = "".join([desc, toctree_header, toctree]) # Save this as the new docs_overview - with open(path.join(docs_dir, f"{cmtype}.rst"), "w") as f: - f.write(docs_overview) + docs_dir.joinpath(f"{cmtype}.rst").write_text(docs_overview) # Create viscm output figure viscm.gui.main( diff --git a/src/cmasher/utils.py b/src/cmasher/utils.py index 53fe17c..93c9d05 100644 --- a/src/cmasher/utils.py +++ b/src/cmasher/utils.py @@ -13,6 +13,7 @@ from glob import glob from importlib.util import find_spec from os import path +from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Optional, Union @@ -360,7 +361,7 @@ def create_cmap_mod( Optional -------- - save_dir: os.PathLike Default: '.' + save_dir: str or os.PathLike[str] Default: '.' The path to the directory where the module must be saved. By default, the current directory is used. @@ -386,7 +387,7 @@ def create_cmap_mod( """ # Get absolute value to provided save_dir - save_dir = path.abspath(save_dir) + save_dir = Path(save_dir).resolve() # Remove any 'cmr.' prefix from provided cmap name = cmap.removeprefix("cmr.") @@ -469,21 +470,21 @@ def create_cmap_mod( ) # Obtain the path to the module - cmap_path = path.join(save_dir, f"{_copy_name or name}.py") + cmap_path = save_dir / f"{_copy_name or name}.py" # Create Python module with open(cmap_path, "w") as f: f.write(cm_py_file[1:]) # Return cmap_path - return cmap_path + return str(cmap_path.resolve()) # This function creates an overview plot of all colormaps specified def create_cmap_overview( cmaps: list[CMAP] | dict[str, list[Colormap]] | None = None, *, - savefig: str | None = None, + savefig: str | os.PathLike[str] | None = None, use_types: bool = True, sort: str | Callable | None = "alphabetical", show_grayscale: bool = True, @@ -505,7 +506,7 @@ def create_cmap_overview( A list of all colormaps that must be included in the overview plot. If dict of lists, the keys define categories for the colormaps. If *None*, all colormaps defined in *CMasher* are used instead. - savefig : str or None. Default: None + savefig : str, os.PathLike or None. Default: None If not *None*, the path where the overview plot must be saved to. Else, the plot will simply be shown. use_types : bool. Default: True @@ -915,7 +916,8 @@ def sort_key(x): # If savefig is not None, save the figure if savefig is not None: - dpi = 100 if (path.splitext(savefig)[1] == ".svg") else 250 + savefig = Path(savefig) + dpi = 100 if (savefig.suffix == ".svg") else 250 plt.savefig(savefig, dpi=dpi, facecolor=face_color, edgecolor=edge_color) plt.close(fig) @@ -1161,7 +1163,9 @@ def get_sub_cmap(cmap: CMAP, start: float, stop: float, *, N: int | None = None) # Function to import all custom colormaps in a file or directory -def import_cmaps(cmap_path: str, *, _skip_registration: bool = False) -> None: +def import_cmaps( + cmap_path: str | os.PathLike[str], *, _skip_registration: bool = False +) -> None: """ Reads in custom colormaps from a provided file or directory `cmap_path`; transforms them into :obj:`~matplotlib.colors.ListedColormap` objects; and @@ -1174,7 +1178,7 @@ def import_cmaps(cmap_path: str, *, _skip_registration: bool = False) -> None: Parameters ---------- - cmap_path : str + cmap_path : str or os.PathLike[str] Relative or absolute path to a custom colormap file; or directory that contains custom colormap files. A colormap file can be a *NumPy* binary file ('.npy'); a *viscm* source file ('.jscm'); or any text file. @@ -1204,24 +1208,29 @@ def import_cmaps(cmap_path: str, *, _skip_registration: bool = False) -> None: """ # Obtain path to file or directory with colormaps - cmap_path = path.abspath(cmap_path) + cmap_path_input = cmap_path + cmap_path = Path(cmap_path).resolve() # Check if provided file or directory exists - if not path.exists(cmap_path): - raise OSError( - f"Input argument 'cmap_path' is a non-existing path ({cmap_path!r})!" + if not cmap_path.exists(): + raise FileNotFoundError( + "Input argument 'cmap_path' is a non-existing path " + f"({cmap_path_input!r})!" ) + cm_files: list[Path] + # Check if cmap_path is a file or directory and act accordingly - if path.isfile(cmap_path): + if cmap_path.is_file(): # If file, split cmap_path up into dir and file components - cmap_dir, cmap_file = path.split(cmap_path) + cmap_dir = cmap_path.parent + cmap_file = cmap_path # Check if its name starts with 'cm_' and raise error if not - if not cmap_file.startswith("cm_"): + if not cmap_file.stem.startswith("cm_"): raise OSError( "Input argument 'cmap_path' does not lead to a file " - f"with the 'cm_' prefix ({cmap_path!r})!" + f"with the 'cm_' prefix ({cmap_path_input!r})!" ) # Set cm_files to be the sole read-in file @@ -1229,13 +1238,11 @@ def import_cmaps(cmap_path: str, *, _skip_registration: bool = False) -> None: else: # If directory, obtain the names of all colormap files in cmap_path cmap_dir = cmap_path - cm_files = list(map(path.basename, glob(f"{cmap_dir}/cm_*"))) - cm_files.sort() + cm_files = sorted(cmap_dir.glob("cm_*")) def sort_key(name): # prioritize binary files over text files because binary loads faster - _, ext = path.splitext(name) - if ext == ".npy": + if (ext := name.suffix) == ".npy": return 0 if ext == ".txt": return 1 @@ -1244,14 +1251,15 @@ def sort_key(name): cm_files.sort(key=sort_key) del sort_key - if any(file.endswith(".jscm") for file in cm_files) and not _HAS_VISCM: + if any(file.suffix == ".jscm" for file in cm_files) and not _HAS_VISCM: raise ValueError("The 'viscm' package is required to read '.jscm' files!") # Read in all the defined colormaps, transform and register them seen: set[str] = set() for cm_file in cm_files: # Split basename and extension - base_str, ext_str = path.splitext(cm_file) + base_str = cm_file.stem + ext_str = cm_file.suffix if base_str in seen: continue else: @@ -1259,14 +1267,11 @@ def sort_key(name): cm_name = base_str[3:] - # Obtain absolute path to colormap data file - cm_file_path = path.join(cmap_dir, cm_file) - # Process colormap files try: # If file is a NumPy binary file if ext_str == ".npy": - rgb = np.load(cm_file_path) + rgb = np.load(cm_file) # If file is viscm source file elif ext_str == ".jscm": @@ -1274,7 +1279,7 @@ def sort_key(name): # Load colormap cmap = viscm.gui.Colormap(None, None, None) - cmap.load(cm_file_path) + cmap.load(cm_file) # Create editor and obtain RGB values v = viscm.viscm_editor( @@ -1287,9 +1292,7 @@ def sort_key(name): # If file is anything else else: - rgb = np.genfromtxt( - cm_file_path, dtype=None, comments="//", encoding=None - ) # type: ignore [call-overload] + rgb = np.genfromtxt(cm_file, dtype=None, comments="//", encoding=None) # type: ignore [call-overload] if not _skip_registration: # Register colormap @@ -1612,7 +1615,7 @@ def view_cmap( # Check if show_test is True if show_test: # If so, use a colormap test data file - data = np.load(path.join(path.dirname(__file__), "data/colormaptest.npy")) + data = np.load(Path(__file__).parent / "data" / "colormaptest.npy") else: # If not, just plot the colormap data = [np.linspace(0, 1, cmap.N)] @@ -1646,4 +1649,4 @@ def view_cmap( # %% IMPORT SCRIPT # Import all colormaps defined in './colormaps' -import_cmaps(path.join(path.dirname(__file__), "colormaps")) +import_cmaps(Path(__file__).parent / "colormaps") diff --git a/tests/test_utils.py b/tests/test_utils.py index ed386f6..7eda9c0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,7 @@ # %% IMPORTS # Built-in imports -import os import shutil from importlib.util import find_spec, module_from_spec, spec_from_file_location -from os import path from pathlib import Path # Package imports @@ -290,9 +288,10 @@ def test_dict(self): create_cmap_overview({"test1": [cmrcm.rainforest], "test2": ["cmr.rainforest"]}) # Test if the figure can be saved - def test_savefig(self): - create_cmap_overview(savefig="test.png") - assert path.exists("./test.png") + def test_savefig(self, tmp_path): + dst = tmp_path / "test.png" + create_cmap_overview(savefig=dst) + assert dst.is_file() # test if providing an invalid sort value raises an error def test_invalid_sort_value(self): @@ -521,8 +520,12 @@ def test_default(self): view_cmap("cmr.rainforest") # Test if the figure can be saved - def test_savefig(self): + def test_savefig(self, tmp_path): + dst = tmp_path / "test.png" view_cmap( - "cmr.rainforest", show_test=True, show_grayscale=True, savefig="test.png" + "cmr.rainforest", + show_test=True, + show_grayscale=True, + savefig=dst, ) - assert path.exists("./test.png") + assert dst.is_file()