From 3522dced8a49dc93fb0140d9ac360a88f31b11bb Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sun, 24 Sep 2023 13:21:19 -0400 Subject: [PATCH] fix(resolve_exe): support extensionless abs/rel paths on windows (#1957) --- autotest/test_mbase.py | 33 ++++++++++++++------------- autotest/test_modflow.py | 12 +++++----- flopy/mbase.py | 48 +++++++++++++++++++++++++--------------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/autotest/test_mbase.py b/autotest/test_mbase.py index 757909f960..8a2eddcadc 100644 --- a/autotest/test_mbase.py +++ b/autotest/test_mbase.py @@ -11,6 +11,9 @@ from flopy.utils.flopy_io import relpath_safe +_system = system() + + @pytest.fixture def mf6_model_path(example_data_path): return example_data_path / "mf6" / "test006_gwf3" @@ -18,10 +21,7 @@ def mf6_model_path(example_data_path): @requires_exe("mf6") @pytest.mark.parametrize("use_ext", [True, False]) -def test_resolve_exe_by_name(function_tmpdir, use_ext): - if use_ext and system() != "Windows": - pytest.skip(".exe extensions are Windows-only") - +def test_resolve_exe_by_name(use_ext): ext = ".exe" if use_ext else "" expected = which("mf6").lower() actual = resolve_exe(f"mf6{ext}") @@ -31,13 +31,14 @@ def test_resolve_exe_by_name(function_tmpdir, use_ext): @requires_exe("mf6") @pytest.mark.parametrize("use_ext", [True, False]) -def test_resolve_exe_by_abs_path(function_tmpdir, use_ext): - if use_ext and system() != "Windows": - pytest.skip(".exe extensions are Windows-only") - - ext = ".exe" if use_ext else "" +def test_resolve_exe_by_abs_path(use_ext): + abs_path = which("mf6") + if _system == "Windows" and not use_ext: + abs_path = abs_path[:-4] + elif _system != "Windows" and use_ext: + abs_path = f"{abs_path}.exe" expected = which("mf6").lower() - actual = resolve_exe(which(f"mf6{ext}")) + actual = resolve_exe(abs_path) assert actual.lower() == expected assert which(actual) @@ -46,9 +47,6 @@ def test_resolve_exe_by_abs_path(function_tmpdir, use_ext): @pytest.mark.parametrize("use_ext", [True, False]) @pytest.mark.parametrize("forgive", [True, False]) def test_resolve_exe_by_rel_path(function_tmpdir, use_ext, forgive): - if use_ext and system() != "Windows": - pytest.skip(".exe extensions are Windows-only") - ext = ".exe" if use_ext else "" expected = which("mf6").lower() @@ -59,10 +57,11 @@ def test_resolve_exe_by_rel_path(function_tmpdir, use_ext, forgive): with set_dir(inner_dir): # copy exe to relative dir - copy(expected, bin_dir / "mf6") - assert (bin_dir / "mf6").is_file() + new_exe_path = bin_dir / Path(expected).name + copy(expected, new_exe_path) + assert new_exe_path.is_file() - expected = which(str(Path(bin_dir / "mf6").absolute())).lower() + expected = which(str(new_exe_path.absolute())).lower() actual = resolve_exe(f"../bin/mf6{ext}") assert actual.lower() == expected assert which(actual) @@ -77,7 +76,7 @@ def test_resolve_exe_by_rel_path(function_tmpdir, use_ext, forgive): def test_run_model_when_namefile_not_in_model_ws( - mf6_model_path, example_data_path, function_tmpdir + mf6_model_path, function_tmpdir ): # copy input files to temp workspace ws = function_tmpdir / "ws" diff --git a/autotest/test_modflow.py b/autotest/test_modflow.py index 4f165dd11d..8e1c8b114a 100644 --- a/autotest/test_modflow.py +++ b/autotest/test_modflow.py @@ -207,10 +207,10 @@ def test_exe_selection(example_data_path, function_tmpdir): # no selection defaults to mf2005 exe_name = "mf2005" - assert Path(Modflow().exe_name).name == exe_name - assert Path(Modflow(exe_name=None).exe_name).name == exe_name + assert Path(Modflow().exe_name).stem == exe_name + assert Path(Modflow(exe_name=None).exe_name).stem == exe_name assert ( - Path(Modflow.load(namfile_path, model_ws=model_path).exe_name).name + Path(Modflow.load(namfile_path, model_ws=model_path).exe_name).stem == exe_name ) assert ( @@ -218,20 +218,20 @@ def test_exe_selection(example_data_path, function_tmpdir): Modflow.load( namfile_path, exe_name=None, model_ws=model_path ).exe_name - ).name + ).stem == exe_name ) # user-specified (just for testing - there is no legitimate reason # to use mp7 with Modflow but Modpath7 derives from BaseModel too) exe_name = "mp7" - assert Path(Modflow(exe_name=exe_name).exe_name).name == exe_name + assert Path(Modflow(exe_name=exe_name).exe_name).stem == exe_name assert ( Path( Modflow.load( namfile_path, exe_name=exe_name, model_ws=model_path ).exe_name - ).name + ).stem == exe_name ) diff --git a/flopy/mbase.py b/flopy/mbase.py index 846bc6fca9..c09c9b1e02 100644 --- a/flopy/mbase.py +++ b/flopy/mbase.py @@ -26,8 +26,10 @@ from .utils import flopy_io from .version import __version__ +on_windows = sys.platform.startswith("win") + # Prepend flopy appdir bin directory to PATH to work with "get-modflow :flopy" -if sys.platform.startswith("win"): +if on_windows: flopy_bin = os.path.expandvars(r"%LOCALAPPDATA%\flopy\bin") else: flopy_bin = os.path.join(os.path.expanduser("~"), ".local/share/flopy/bin") @@ -62,25 +64,34 @@ def resolve_exe( str: absolute path to the executable """ - exe_name = str(exe_name) - exe = which(exe_name) - if exe is not None: - # in case which() returned a relative path, resolve it - exe = which(str(Path(exe).resolve())) - else: - if exe_name.lower().endswith(".exe"): - # try removing .exe suffix - exe = which(exe_name[:-4]) + def _resolve(exe_name): + exe = which(exe_name) if exe is not None: - # in case which() returned a relative path, resolve it + # if which() returned a relative path, resolve it exe = which(str(Path(exe).resolve())) else: - # try tilde-expanded abspath - exe = which(Path(exe_name).expanduser().absolute()) - if exe is None and exe_name.lower().endswith(".exe"): - # try tilde-expanded abspath without .exe suffix - exe = which(Path(exe_name[:-4]).expanduser().absolute()) - if exe is None: + if exe_name.lower().endswith(".exe"): + # try removing .exe suffix + exe = which(exe_name[:-4]) + if exe is not None: + # in case which() returned a relative path, resolve it + exe = which(str(Path(exe).resolve())) + else: + # try tilde-expanded abspath + exe = which(Path(exe_name).expanduser().absolute()) + if exe is None and exe_name.lower().endswith(".exe"): + # try tilde-expanded abspath without .exe suffix + exe = which(Path(exe_name[:-4]).expanduser().absolute()) + return exe + + name = str(exe_name) + exe_path = _resolve(name) + if exe_path is None and on_windows and Path(name).suffix == "": + # try adding .exe suffix on windows (for portability from other OS) + exe_path = _resolve(f"{name}.exe") + + # raise if we are unforgiving, otherwise return None + if exe_path is None: if forgive: warn( f"The program {exe_name} does not exist or is not executable." @@ -90,7 +101,8 @@ def resolve_exe( raise FileNotFoundError( f"The program {exe_name} does not exist or is not executable." ) - return exe + + return exe_path # external exceptions for users