Skip to content

Commit 8887835

Browse files
authored
refactor(models): distinguish source repo via prefix (#202)
Add a prefix to model names identifying which repository the model is from (i.e., what "kind" of model it is): - example/... example models from https://github.com/MODFLOW-ORG/modflow6-examples - test/... test models from https://github.com/MODFLOW-ORG/modflow6-testmodels (only mf6) - large/... models from https://github.com/MODFLOW-ORG/modflow6-largetestmodels This will allow more easily selecting subsets of models by the repository they are from as we do in MF6 autotests. I considered adding an "mf6" prefix to all these (useful if we want to expose older models, e.g. from the mf5to6 folder in the test models repo). I also considered making the prefix identical to the repository name. Not sure which way is best, this will do for now. Anyone reading this with an opinion, please make it known. Also, remove the auto-generated model functions (doesn't play nice with "namespaced" i.e. "/"-delimited model names), tweak the registry introspection functions for consistency, and update some docstrings.
1 parent 1ff3c0f commit 8887835

7 files changed

Lines changed: 11266 additions & 11266 deletions

File tree

autotest/test_models.py

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,35 @@ def test_registry():
2121
assert any(registry), "Registry is empty"
2222

2323

24-
@pytest.mark.parametrize("model_name, files", MODELMAP.items())
24+
@pytest.mark.parametrize(
25+
"model_name, files", MODELMAP.items(), ids=list(MODELMAP.keys())
26+
)
2527
def test_models(model_name, files):
26-
model_names = models.list_models()
28+
model_names = list(models.get_models().keys())
2729
assert model_name in model_names, f"Model {model_name} not found in model map"
2830
assert files == models.MODELMAP[model_name], (
2931
f"Files for model {model_name} do not match"
3032
)
31-
assert hasattr(models, model_name), (
32-
f"Function {model_name} not found in models module"
33-
)
3433
if "mf6" in model_name:
3534
assert any(Path(f).name == "mfsim.nam" for f in files)
3635

3736

38-
@pytest.mark.parametrize("example_name, model_names", models.get_examples().items())
37+
@pytest.mark.parametrize(
38+
"example_name, model_names",
39+
models.get_examples().items(),
40+
ids=list(models.get_examples().keys()),
41+
)
3942
def test_get_examples(example_name, model_names):
4043
assert example_name in models.EXAMPLES
4144
for model_name in model_names:
4245
assert model_name in models.MODELMAP
4346

4447

45-
@pytest.mark.parametrize("model_name, files", list(islice(MODELMAP.items(), TAKE)))
48+
@pytest.mark.parametrize(
49+
"model_name, files",
50+
list(islice(MODELMAP.items(), TAKE)),
51+
ids=list(MODELMAP.keys())[:TAKE],
52+
)
4653
def test_copy_to(model_name, files, tmp_path):
4754
workspace = models.copy_to(tmp_path, model_name, verbose=True)
4855
assert workspace.exists(), f"Model {model_name} was not copied to {tmp_path}"
@@ -54,21 +61,3 @@ def test_copy_to(model_name, files, tmp_path):
5461
)
5562
if "mf6" in model_name:
5663
assert any(Path(f).name == "mfsim.nam" for f in files)
57-
58-
59-
@pytest.mark.parametrize("model_name, files", list(islice(MODELMAP.items(), TAKE)))
60-
def test_generated_functions_return_files(model_name, files):
61-
model_function = getattr(models, model_name)
62-
fetched_files = model_function()
63-
assert isinstance(fetched_files, list), (
64-
f"Function {model_name} did not return a list"
65-
)
66-
assert len(fetched_files) == len(files), (
67-
f"Function {model_name} did not return the correct number of files"
68-
)
69-
if "mf6" in model_name:
70-
assert any(Path(f).name == "mfsim.nam" for f in files)
71-
for fetched_file in fetched_files:
72-
assert Path(fetched_file).exists(), (
73-
f"Fetched file {fetched_file} does not exist"
74-
)

docs/md/models.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
# Models API
22

3-
The `modflow_devtools.models` module provides programmatic access to MODFLOW 6 example models via [Pooch](https://www.fatiando.org/pooch/latest/index.html).
4-
5-
When the module is imported, it checks for the existence of the registry in models files. If they are found, it loads the registry and dynamically generates functions for each model, attaching them to the module namespace.
6-
7-
Each function returns a list of files. Example usage:
3+
The `modflow_devtools.models` module provides programmatic access to MODFLOW 6 example models via [Pooch](https://www.fatiando.org/pooch/latest/index.html). Example usage:
84

95
```python
106
import modflow_devtools.models as models
7+
from flopy.mf6 import MFSimulation
118

12-
files = models.some_model()
9+
workspace = models.copy_to("some/path", "some_model")
10+
sim = MFSimulation.load(sim_ws=workspace)
1311
```
1412

1513
## Developers
@@ -24,7 +22,7 @@ The script can be executed with `python -m modflow_devtools.make_registry`. It a
2422
For example, to create a registry of models in the MF6 examples and test models repositories, assuming each is checked out next to this project:
2523

2624
```shell
27-
python -m modflow_devtools.make_registry ../modflow6-examples/examples --url https://github.com/MODFLOW-ORG/modflow6-examples/releases/download/current/mf6examples.zip
28-
python -m modflow_devtools.make_registry ../modflow6-testmodels --append --url https://github.com/MODFLOW-ORG/modflow6-testmodels/raw/master
29-
python -m modflow_devtools.make_registry ../modflow6-largetestmodels --append --url https://github.com/MODFLOW-ORG/modflow6-largetestmodels/raw/master
25+
python -m modflow_devtools.make_registry ../modflow6-examples/examples --url https://github.com/MODFLOW-ORG/modflow6-examples/releases/download/current/mf6examples.zip --prefix example
26+
python -m modflow_devtools.make_registry ../modflow6-testmodels/mf6 --append --url https://github.com/MODFLOW-ORG/modflow6-testmodels/raw/master/mf6 --prefix test
27+
python -m modflow_devtools.make_registry ../modflow6-largetestmodels --append --url https://github.com/MODFLOW-ORG/modflow6-largetestmodels/raw/master --prefix large
3028
```

modflow_devtools/make_registry.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def _sha256(path: Path) -> str:
3232
def write_registry(
3333
path: str | PathLike,
3434
url: str,
35+
prefix: str = "",
3536
append: bool = False,
3637
):
3738
path = Path(path).expanduser().absolute()
@@ -47,18 +48,16 @@ def write_registry(
4748

4849
model_paths = get_model_paths(path)
4950
for model_path in model_paths:
50-
# TODO: the renaming is only necessary because we're attaching auto-
51-
# generated functions to the models module. If we can live without,
52-
# and get by with a single function that takes model name as an arg,
53-
# then the model names could correspond directly to directory names.
5451
model_path = model_path.expanduser().absolute()
5552
rel_path = model_path.relative_to(path)
56-
model_name = "/".join(rel_path.parts)
53+
parts = [prefix, *list(rel_path.parts)] if prefix else list(rel_path.parts)
54+
model_name = "/".join(parts)
5755
modelmap[model_name] = []
5856
if is_zip:
59-
if rel_path.parts[0] not in examples:
60-
examples[rel_path.parts[0]] = []
61-
examples[rel_path.parts[0]].append(model_name)
57+
name = rel_path.parts[0]
58+
if name not in examples:
59+
examples[name] = []
60+
examples[name].append(model_name)
6261
for p in model_path.rglob("*"):
6362
if not p.is_file() or any(e in p.name for e in exclude):
6463
continue
@@ -95,21 +94,23 @@ def drop_none_or_empty(path, key, value):
9594

9695

9796
if __name__ == "__main__":
98-
parser = argparse.ArgumentParser(description="Make a registry of example models.")
97+
parser = argparse.ArgumentParser(description="Make a registry of models.")
9998
parser.add_argument("path")
10099
parser.add_argument(
101100
"--append",
102101
"-a",
103102
action="store_true",
104103
help="Append instead of overwriting.",
105104
)
105+
parser.add_argument(
106+
"--prefix", "-p", type=str, help="Prefix for models.", default=""
107+
)
106108
parser.add_argument(
107109
"--url",
108110
"-u",
109111
type=str,
110-
help="Base URL for example models.",
112+
help="Base URL for models.",
113+
default=BASE_URL,
111114
)
112115
args = parser.parse_args()
113-
path = Path(args.path)
114-
url = args.url if args.url else BASE_URL
115-
write_registry(path=path, url=url, append=args.append)
116+
write_registry(path=args.path, url=args.url, prefix=args.prefix, append=args.append)

modflow_devtools/models.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ def _fetch_zip(zip_name):
8181
with pkg_resources.open_binary(REGISTRY_ANCHOR, MODELMAP_FILE_NAME) as models_file:
8282
MODELMAP = tomli.load(models_file)
8383
for model_name, files in MODELMAP.items():
84-
fetch = _fetch(model_name, files)
85-
FETCHERS[model_name] = fetch
86-
globals()[model_name] = fetch
84+
FETCHERS[model_name] = _fetch(model_name, files)
8785
except: # noqa: E722
8886
warn(
8987
f"No model mapping file '{MODELMAP_FILE_NAME}' "
@@ -104,21 +102,35 @@ def _fetch_zip(zip_name):
104102

105103

106104
def get_examples() -> dict[str, list[str]]:
105+
"""Get a map of example names to example scenarios."""
107106
return EXAMPLES
108107

109108

110109
def get_registry() -> dict[str, str]:
110+
"""
111+
Get a map of file names to URLs. Note that this mapping
112+
contains no information on which files belong to which
113+
models. For that information, use `get_models()`.
114+
"""
111115
return REGISTRY
112116

113117

114-
def list_models() -> list[str]:
115-
return list(MODELMAP.keys())
118+
def get_models() -> dict[str, str]:
119+
"""Get a map of model names to input files."""
120+
return MODELMAP
116121

117122

118123
def copy_to(
119124
workspace: str | PathLike, model_name: str, verbose: bool = False
120125
) -> Path | None:
121-
if not any(files := FETCHERS[model_name]()):
126+
"""
127+
Copy the model's input files to the given workspace.
128+
The workspace will be created if it does not exist.
129+
"""
130+
131+
if (fetch := FETCHERS.get(model_name, None)) is None:
132+
raise ValueError(f"Model '{model_name}' not in registry")
133+
if not any(files := fetch()):
122134
return None
123135
# create the workspace if needed
124136
workspace = Path(workspace).expanduser().absolute()

0 commit comments

Comments
 (0)