Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(resolve_exe): support extensionless abs/rel paths on windows #1957

Merged
merged 1 commit into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
14 changes: 7 additions & 7 deletions autotest/test_modflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import numpy as np
import pytest
from autotest.conftest import get_example_data_path
from modflow_devtools.misc import has_pkg
from modflow_devtools.markers import excludes_platform, requires_exe
from modflow_devtools.misc import has_pkg

from flopy.discretization import StructuredGrid
from flopy.mf6 import MFSimulation
Expand Down Expand Up @@ -268,31 +268,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