Skip to content

Commit

Permalink
fix(BaseModel): don't suppress error if exe not found (#1901)
Browse files Browse the repository at this point in the history
* warn at init/load time if exe not found, raise error at model run time
  • Loading branch information
wpbonelli committed Aug 25, 2023
1 parent 5d21410 commit 82bc3f1
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 12 deletions.
17 changes: 11 additions & 6 deletions autotest/test_mbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def mf6_model_path(example_data_path):

@requires_exe("mf6")
@pytest.mark.parametrize("use_ext", [True, False])
def test_resolve_exe_named(function_tmpdir, use_ext):
def test_resolve_exe_by_name(function_tmpdir, use_ext):
if use_ext and system() != "Windows":
pytest.skip(".exe extensions are Windows-only")

Expand All @@ -31,7 +31,7 @@ def test_resolve_exe_named(function_tmpdir, use_ext):

@requires_exe("mf6")
@pytest.mark.parametrize("use_ext", [True, False])
def test_resolve_exe_full_path(function_tmpdir, use_ext):
def test_resolve_exe_by_abs_path(function_tmpdir, use_ext):
if use_ext and system() != "Windows":
pytest.skip(".exe extensions are Windows-only")

Expand All @@ -44,7 +44,8 @@ def test_resolve_exe_full_path(function_tmpdir, use_ext):

@requires_exe("mf6")
@pytest.mark.parametrize("use_ext", [True, False])
def test_resolve_exe_rel_path(function_tmpdir, use_ext):
@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")

Expand All @@ -66,9 +67,13 @@ def test_resolve_exe_rel_path(function_tmpdir, use_ext):
assert actual.lower() == expected
assert which(actual)

# should raise an error if exe DNE
with pytest.raises(FileNotFoundError):
resolve_exe("../bin/mf2005")
# check behavior if exe DNE
with (
pytest.warns(UserWarning)
if forgive
else pytest.raises(FileNotFoundError)
):
assert not resolve_exe("../bin/mf2005", forgive)


def test_run_model_when_namefile_not_in_model_ws(
Expand Down
50 changes: 50 additions & 0 deletions autotest/test_modflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,56 @@ def test_mt_modelgrid(function_tmpdir):
assert np.array_equal(swt.modelgrid.idomain, ml.modelgrid.idomain)


@requires_exe("mp7")
def test_exe_selection(example_data_path, function_tmpdir):
model_path = example_data_path / "freyberg"
namfile_path = model_path / "freyberg.nam"

# 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.load(namfile_path, model_ws=model_path).exe_name).name
== exe_name
)
assert (
Path(
Modflow.load(
namfile_path, exe_name=None, model_ws=model_path
).exe_name
).name
== 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.load(
namfile_path, exe_name=exe_name, model_ws=model_path
).exe_name
).name
== exe_name
)

# init/load should warn if exe DNE
exe_name = "not_an_exe"
with pytest.warns(UserWarning):
ml = Modflow(exe_name=exe_name)
with pytest.warns(UserWarning):
ml = Modflow.load(namfile_path, exe_name=exe_name, model_ws=model_path)

# run should error if exe DNE
ml = Modflow.load(namfile_path, exe_name=exe_name, model_ws=model_path)
ml.change_model_ws(function_tmpdir)
ml.write_input()
with pytest.raises(ValueError):
ml.run_model()


def test_free_format_flag(function_tmpdir):
Lx = 100.0
Ly = 100.0
Expand Down
25 changes: 19 additions & 6 deletions flopy/mbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,21 @@
iprn = -1


def resolve_exe(exe_name: Union[str, os.PathLike]) -> str:
def resolve_exe(
exe_name: Union[str, os.PathLike], forgive: bool = False
) -> str:
"""
Resolves the absolute path of the executable.
Resolves the absolute path of the executable, raising FileNotFoundError if the executable
cannot be found (set forgive to True to return None and warn instead of raising an error).
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.
forgive : bool
If True and executable cannot be found, return None and warn
rather than raising a FileNotFoundError. Defaults to False.
Returns
-------
Expand All @@ -75,6 +81,12 @@ def resolve_exe(exe_name: Union[str, os.PathLike]) -> str:
# try tilde-expanded abspath without .exe suffix
exe = which(Path(exe_name[:-4]).expanduser().absolute())
if exe is None:
if forgive:
warn(
f"The program {exe_name} does not exist or is not executable."
)
return None

raise FileNotFoundError(
f"The program {exe_name} does not exist or is not executable."
)
Expand Down Expand Up @@ -376,10 +388,11 @@ def __init__(
self._namefile = self.__name + "." + self.namefile_ext
self._packagelist = []
self.heading = ""
try:
self.exe_name = resolve_exe(exe_name)
except:
self.exe_name = "mf2005"
self.exe_name = (
"mf2005"
if exe_name is None
else resolve_exe(exe_name, forgive=True)
)
self._verbose = verbose
self.external_path = None
self.external_extension = "ref"
Expand Down

0 comments on commit 82bc3f1

Please sign in to comment.