Skip to content

Commit

Permalink
fix(resolve_exe): support extensionless abs/rel paths on windows (#1957)
Browse files Browse the repository at this point in the history
  • Loading branch information
wpbonelli committed Sep 30, 2023
1 parent d2dbacb commit 3522dce
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 41 deletions.
33 changes: 16 additions & 17 deletions autotest/test_mbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
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"


@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}")
Expand All @@ -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)

Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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"
Expand Down
12 changes: 6 additions & 6 deletions autotest/test_modflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,31 +207,31 @@ 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 (
Path(
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
)

Expand Down
48 changes: 30 additions & 18 deletions flopy/mbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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."
Expand All @@ -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
Expand Down

0 comments on commit 3522dce

Please sign in to comment.