From dfc682931833cf4a7548bfebd8c13c1669b60ba7 Mon Sep 17 00:00:00 2001 From: w-bonelli Date: Thu, 2 Mar 2023 21:21:20 -0500 Subject: [PATCH] refactor: move resolve_exe to utils.flopy_io, various fixes --- autotest/test_modflow.py | 13 ++++--- autotest/test_uzf.py | 14 +++---- flopy/export/shapefile_utils.py | 4 +- flopy/mbase.py | 68 ++++++++++++++++----------------- flopy/mf6/mfmodel.py | 11 +++--- flopy/mf6/modflow/mfgwf.py | 7 +++- flopy/mfusg/mfusg.py | 12 +++--- flopy/modflow/mf.py | 18 +++++---- flopy/pakbase.py | 9 ++++- flopy/seawat/swt.py | 13 ++++--- flopy/utils/binaryfile.py | 1 - flopy/utils/datafile.py | 2 +- flopy/utils/flopy_io.py | 2 + flopy/utils/formattedfile.py | 2 - flopy/utils/zonbud.py | 17 +++++---- 15 files changed, 104 insertions(+), 89 deletions(-) diff --git a/autotest/test_modflow.py b/autotest/test_modflow.py index b70350e2e8..d0bfec78bc 100644 --- a/autotest/test_modflow.py +++ b/autotest/test_modflow.py @@ -814,20 +814,23 @@ def test_bcs_check(function_tmpdir): assert np.array_equal(chk.summary_array["j"], np.array([0, 1, 1, 1, 1])) -def test_path_arguments(function_tmpdir, module_tmpdir): +def test_path_params_and_props(function_tmpdir, module_tmpdir): + # properties should be set to string abspaths regardless of + # pathlib.Path or str arguments + mf = Modflow( version="mf2005", model_ws=function_tmpdir, external_path=module_tmpdir ) - assert mf.model_ws == function_tmpdir - assert mf.external_path == module_tmpdir + assert mf.model_ws == str(function_tmpdir) + assert mf.external_path == str(module_tmpdir) mf = Modflow( version="mf2005", model_ws=str(function_tmpdir), external_path=str(module_tmpdir), ) - assert mf.model_ws == function_tmpdir - assert mf.external_path == module_tmpdir + assert mf.model_ws == str(function_tmpdir) + assert mf.external_path == str(module_tmpdir) def test_properties_check(function_tmpdir): diff --git a/autotest/test_uzf.py b/autotest/test_uzf.py index 5c83df6c36..9a8f3ca17a 100644 --- a/autotest/test_uzf.py +++ b/autotest/test_uzf.py @@ -643,8 +643,7 @@ def test_uzf_negative_iuzfopt(function_tmpdir): ml.write_input() success, buff = ml.run_model() - if not success: - raise AssertionError("UZF model with -1 iuzfopt failed to run") + assert success, "UZF model with -1 iuzfopt failed to run" ml2 = Modflow.load( "uzf_neg.nam", version="mfnwt", model_ws=function_tmpdir @@ -653,8 +652,9 @@ def test_uzf_negative_iuzfopt(function_tmpdir): pet = ml2.uzf.pet.array extpd = ml2.uzf.pet.array - if not np.max(pet) == np.min(pet) and np.max(pet) != 0.1: - raise AssertionError("Read error for iuzfopt less than 0") - - if not np.max(extpd) == np.min(extpd) and np.max(extpd) != 0.2: - raise AssertionError("Read error for iuzfopt less than 0") + assert ( + np.max(pet) == np.min(pet) and np.max(pet) != 0.1 + ), "Read error for iuzfopt less than 0" + assert ( + np.max(extpd) == np.min(extpd) and np.max(extpd) != 0.2 + ), "Read error for iuzfopt less than 0" diff --git a/flopy/export/shapefile_utils.py b/flopy/export/shapefile_utils.py index 1b5dc35a56..68bc182d05 100644 --- a/flopy/export/shapefile_utils.py +++ b/flopy/export/shapefile_utils.py @@ -534,7 +534,7 @@ def recarray2shp( shpname: Union[str, os.PathLike] = "recarray.shp", mg=None, epsg=None, - prj: Union[str, os.PathLike] = None, + prj: Optional[Union[str, os.PathLike]] = None, verbose=False, **kwargs, ): @@ -558,7 +558,7 @@ def recarray2shp( Path for the output shapefile epsg : int EPSG code. See https://www.epsg-registry.org/ or spatialreference.org - prj : str or PathLike, default None + prj : str or PathLike, optional, default None Existing projection file to be used with new shapefile. verbose : bool Whether to print verbose output diff --git a/flopy/mbase.py b/flopy/mbase.py index 0589d40784..69f462475b 100644 --- a/flopy/mbase.py +++ b/flopy/mbase.py @@ -41,6 +41,37 @@ iprn = -1 +def resolve_exe(exe_name: Union[str, os.PathLike]) -> str: + """ + Resolves the absolute path of the executable. + + Parameters + ---------- + exe_name : str or PathLike + The executable's name or path. If only the name is provided, + the executable must be on the system path. + + Returns + ------- + str: absolute path to the executable + """ + + exe_name = str(exe_name) + exe = which(exe_name) + if exe is None: + if exe_name.lower().endswith(".exe"): + # try removing .exe suffix + exe = which(exe_name[:-4]) + if exe is None: + # try tilde-expanded abspath + exe = which(Path(exe_name).expanduser().absolute()) + if exe is None: + raise FileNotFoundError( + f"The program {exe_name} does not exist or is not executable." + ) + return exe + + # external exceptions for users class PackageLoadException(Exception): """ @@ -336,7 +367,7 @@ def __init__( self._namefile = self.__name + "." + self.namefile_ext self._packagelist = [] self.heading = "" - self.exe_name = resolve_exe(exe_name) + self.exe_name = resolve_exe(exe_name) if exe_name else "mf2005" self._verbose = verbose self.external_path = None self.external_extension = "ref" @@ -1364,7 +1395,7 @@ def run_model( pause=False, report=False, normal_msg="normal termination", - ): + ) -> Tuple[bool, List[str]]: """ This method will run the model using subprocess.Popen. @@ -1383,7 +1414,6 @@ def run_model( Returns ------- - (success, buff) success : boolean buff : list of lines of stdout @@ -1656,37 +1686,6 @@ def to_shapefile( self.export(filename, package_names=package_names) -def resolve_exe(exe_name: Union[str, os.PathLike]) -> str: - """ - Resolves the absolute path of the executable. - - Parameters - ---------- - exe_name : str or PathLike - The executable's name or path. If only the name is provided, - the executable must be on the system path. - - Returns - ------- - str: absolute path to the executable - """ - - exe_name = str(exe_name) - exe = which(exe_name) - if exe is None: - if exe_name.lower().endswith(".exe"): - # try removing .exe suffix - exe = which(exe_name[:-4]) - if exe is None: - # try tilde-expanded abspath - exe = which(Path(exe_name).expanduser().absolute()) - if exe is None: - raise FileNotFoundError( - f"The program {exe_name} does not exist or is not executable." - ) - return exe - - def run_model( exe_name: Union[str, os.PathLike], namefile: Optional[str], @@ -1734,7 +1733,6 @@ def run_model( (Default is None) Returns ------- - (success, buff) success : boolean buff : list of lines of stdout (empty if report is False) diff --git a/flopy/mf6/mfmodel.py b/flopy/mf6/mfmodel.py index 3112469ade..b67b0e6a6d 100644 --- a/flopy/mf6/mfmodel.py +++ b/flopy/mf6/mfmodel.py @@ -1,6 +1,7 @@ import inspect import os import sys +from typing import Union import numpy as np @@ -691,9 +692,9 @@ def load_base( model_nam_file="modflowtest.nam", mtype="gwf", version="mf6", - exe_name="mf6", + exe_name: Union[str, os.PathLike] = "mf6", strict=True, - model_rel_path=".", + model_rel_path=os.curdir, load_only=None, ): """ @@ -713,10 +714,8 @@ def load_base( relative path to the model name file from model working folder version : str version of modflow - exe_name : str - model executable name - model_ws : str - model working folder relative to simulation working folder + exe_name : str or PathLike + model executable name or path strict : bool strict mode when loading files model_rel_path : str diff --git a/flopy/mf6/modflow/mfgwf.py b/flopy/mf6/modflow/mfgwf.py index 1dc84e4e57..2f76dfab53 100644 --- a/flopy/mf6/modflow/mfgwf.py +++ b/flopy/mf6/modflow/mfgwf.py @@ -1,6 +1,9 @@ # DO NOT MODIFY THIS FILE DIRECTLY. THIS FILE MUST BE CREATED BY # mf6/utils/createpackages.py # FILE created on January 27, 2023 18:36:16 UTC +import os +from typing import Union + from .. import mfmodel from ..data.mfdatautil import ArrayTemplateGenerator, ListTemplateGenerator @@ -119,9 +122,9 @@ def load( modelname="NewModel", model_nam_file="modflowtest.nam", version="mf6", - exe_name="mf6", + exe_name: Union[str, os.PathLike] = "mf6", strict=True, - model_rel_path=".", + model_rel_path=os.curdir, load_only=None, ): return mfmodel.MFModel.load_base( diff --git a/flopy/mfusg/mfusg.py b/flopy/mfusg/mfusg.py index ba26e67338..8142202c40 100644 --- a/flopy/mfusg/mfusg.py +++ b/flopy/mfusg/mfusg.py @@ -146,9 +146,9 @@ def __repr__(self): @classmethod def load( cls, - f, + f: str, version="mfusg", - exe_name="mfusg", + exe_name: Union[str, os.PathLike] = "mfusg", verbose=False, model_ws: Union[str, os.PathLike] = os.curdir, load_only=None, @@ -159,15 +159,15 @@ def load( Parameters ---------- - f : str or PathLike - Path to MODFLOW name file to load. + f : str + Name of MODFLOW name file to load. version : str, default "mfusg" MODFLOW version. Must be "mfusg". exe_name : str, default "mfusg" MODFLOW executable name. verbose : bool, default False Show messages that can be useful for debugging. - model_ws : str, default "." + model_ws : str or PathLike, default "." Model workspace path. Default is the current directory. load_only : list, str or None List of case insensitive packages to load, e.g. ["bas6", "lpf"]. @@ -175,7 +175,7 @@ def load( which attempts to load all files. An empty list [] will not load any additional packages than is necessary. At a minimum, "dis" or "disu" is always loaded. - forgive : bool, optional + forgive : bool, optional, default False Option to raise exceptions on package load failure, which can be useful for debugging. Default False. check : boolean, optional diff --git a/flopy/modflow/mf.py b/flopy/modflow/mf.py index 33d37dc621..c61baf1aba 100644 --- a/flopy/modflow/mf.py +++ b/flopy/modflow/mf.py @@ -71,8 +71,8 @@ class Modflow(BaseModel): version : str, default "mf2005" MODFLOW version. Choose one of: "mf2k", "mf2005" (default), "mfnwt", or "mfusg". - exe_name : str, default "mf2005" - The name of the executable to use. + exe_name : str or PathLike, default "mf2005" + The name or path of the executable to use. structured : bool, default True Specify if model grid is structured (default) or unstructured. listunit : int, default 2 @@ -162,7 +162,9 @@ def __init__( print(f"Note: external_path {external_path} already exists") else: os.makedirs(os.path.join(model_ws, external_path)) - self.external_path = str(external_path) + self.external_path = str(external_path) + else: + self.external_path = None self.verbose = verbose self.mfpar = ModflowPar() @@ -649,9 +651,9 @@ def load_results(self, **kwargs): @classmethod def load( cls, - f: Union[str, os.PathLike], + f: str, version="mf2005", - exe_name="mf2005", + exe_name: Union[str, os.PathLike] = "mf2005", verbose=False, model_ws: Union[str, os.PathLike] = os.curdir, load_only=None, @@ -663,14 +665,14 @@ def load( Parameters ---------- - f : str or PathLike + f : str Path to MODFLOW name file to load. version : str, default "mf2005" MODFLOW version. Choose one of: "mf2k", "mf2005" (default), or "mfnwt". Note that this can be modified on loading packages unique to different MODFLOW versions. - exe_name : str, default "mf2005" - MODFLOW executable name. + exe_name : str or PathLike, default "mf2005" + MODFLOW executable name or path. verbose : bool, default False Show messages that can be useful for debugging. model_ws : str or PathLike, default "." diff --git a/flopy/pakbase.py b/flopy/pakbase.py index 6cf6a90567..11aee0f5a6 100644 --- a/flopy/pakbase.py +++ b/flopy/pakbase.py @@ -7,6 +7,7 @@ import abc import os import webbrowser as wb +from typing import Union import numpy as np from numpy.lib.recfunctions import stack_arrays @@ -881,7 +882,13 @@ def write_file(self, f=None, check=False): return @staticmethod - def load(f, model, pak_type, ext_unit_dict=None, **kwargs): + def load( + f: Union[str, bytes, os.PathLike], + model, + pak_type, + ext_unit_dict=None, + **kwargs, + ): """ Default load method for standard boundary packages. diff --git a/flopy/seawat/swt.py b/flopy/seawat/swt.py index 7c2554e9aa..f895906a4f 100644 --- a/flopy/seawat/swt.py +++ b/flopy/seawat/swt.py @@ -1,4 +1,5 @@ import os +from typing import Union from ..discretization.modeltime import ModelTime from ..discretization.structuredgrid import StructuredGrid @@ -425,11 +426,11 @@ def write_name_file(self): @classmethod def load( cls, - f, + f: str, version="seawat", - exe_name="swtv4", + exe_name: Union[str, os.PathLike] = "swtv4", verbose=False, - model_ws=".", + model_ws: Union[str, os.PathLike] = os.curdir, load_only=None, ): """ @@ -438,15 +439,15 @@ def load( Parameters ---------- f : str - Path to SEAWAT name file to load. + Name of SEAWAT name file to load. version : str, default "seawat" Version of SEAWAT to use. Valid versions are "seawat" (default). exe_name : str, default "swtv4" The name of the executable to use. verbose : bool, default False Print additional information to the screen. - model_ws : str, default "." - Model workspace. Directory name to create model data sets. + model_ws : str or PathLike, default "." + Model workspace. Directory to create model data sets. Default is the present working directory. load_only : list of str, optional Packages to load (e.g. ["lpf", "adv"]). Default None diff --git a/flopy/utils/binaryfile.py b/flopy/utils/binaryfile.py index 1729930603..cb5b585afd 100644 --- a/flopy/utils/binaryfile.py +++ b/flopy/utils/binaryfile.py @@ -485,7 +485,6 @@ def _build_index(self): self.recordarray = np.array(self.recordarray, dtype=self.header_dtype) self.iposarray = np.array(self.iposarray) self.nlay = np.max(self.recordarray["ilay"]) - return def get_databytes(self, header): """ diff --git a/flopy/utils/datafile.py b/flopy/utils/datafile.py index 2bf748d130..73a7f61b66 100644 --- a/flopy/utils/datafile.py +++ b/flopy/utils/datafile.py @@ -634,4 +634,4 @@ def close(self): Close the file handle. """ - return + self.file.close() diff --git a/flopy/utils/flopy_io.py b/flopy/utils/flopy_io.py index 38b8f0cbd5..cfd3190d67 100644 --- a/flopy/utils/flopy_io.py +++ b/flopy/utils/flopy_io.py @@ -4,6 +4,8 @@ import os import platform import sys +from pathlib import Path +from shutil import which from typing import Union import numpy as np diff --git a/flopy/utils/formattedfile.py b/flopy/utils/formattedfile.py index a835841ca6..db53aa9cfa 100644 --- a/flopy/utils/formattedfile.py +++ b/flopy/utils/formattedfile.py @@ -111,7 +111,6 @@ class FormattedLayerFile(LayerFile): def __init__(self, filename, precision, verbose, kwargs): super().__init__(filename, precision, verbose, kwargs) - return def _build_index(self): """ @@ -309,7 +308,6 @@ def close(self): """ self.file.close() - return class FormattedHeadFile(FormattedLayerFile): diff --git a/flopy/utils/zonbud.py b/flopy/utils/zonbud.py index 2f8261de4d..053b8e3df3 100644 --- a/flopy/utils/zonbud.py +++ b/flopy/utils/zonbud.py @@ -1,6 +1,7 @@ import copy import os from itertools import groupby +from typing import Union import numpy as np @@ -2092,7 +2093,7 @@ def write_input(self, line_length=20): foo.write("END ZONEBUDGET\n") @staticmethod - def load(nam_file, model_ws="."): + def load(nam_file, model_ws: Union[str, os.PathLike] = os.curdir): """ Method to load a zonebudget model from namefile @@ -2100,7 +2101,7 @@ def load(nam_file, model_ws="."): ---------- nam_file : str zonebudget name file - model_ws : str + model_ws : str or PathLike, default "." model workspace path Returns @@ -2130,7 +2131,8 @@ def export(self, f, ml, **kwargs): Parameters ---------- - f : str or flopy.export.netcdf.NetCdf object + f : str, PathLike, or flopy.export.netcdf.NetCdf object + The file to export to ml : flopy.modflow.Modflow or flopy.mf6.ModflowGwf object **kwargs : logger : flopy.export.netcdf.Logger instance @@ -2144,7 +2146,8 @@ def export(self, f, ml, **kwargs): """ from ..export.utils import output_helper - if isinstance(f, str): + if isinstance(f, (str, os.PathLike)): + f = str(f) if not f.endswith(".nc"): raise AssertionError( "File extension must end with .nc to " @@ -2250,14 +2253,14 @@ def write_input(self, f=None, line_length=20): foo.write("\nEND GRIDDATA\n") @staticmethod - def load(f, model): + def load(f: Union[str, os.PathLike], model): """ Method to load a Zone file for zonebudget 6. Parameter --------- - f : str - zone file name + f : str or PathLike + zone file path model : ZoneBudget6 object zonebudget 6 model object