Skip to content

Commit 457ace2

Browse files
authored
refactor(LocalRegistry): support multiple model directory paths (#212)
Allow providing more than one path to local registries. The registry becomes the union of models found in all the paths.
1 parent d532950 commit 457ace2

1 file changed

Lines changed: 52 additions & 40 deletions

File tree

modflow_devtools/models.py

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import hashlib
66
import importlib.resources as pkg_resources
77
from abc import ABC, abstractmethod
8-
from collections.abc import Callable
8+
from collections.abc import Callable, Iterable
99
from functools import partial
1010
from os import PathLike
1111
from pathlib import Path
@@ -45,12 +45,6 @@ def _sha256(path: Path) -> str:
4545

4646

4747
class ModelRegistry(ABC):
48-
@property
49-
@abstractmethod
50-
def path(self) -> Path:
51-
"""The path to the registry's data."""
52-
...
53-
5448
@property
5549
@abstractmethod
5650
def files(self) -> dict:
@@ -65,7 +59,7 @@ def files(self) -> dict:
6559
@abstractmethod
6660
def models(self) -> dict:
6761
"""
68-
A map of model name to the model's input file set.
62+
A map of model name to the model's input files.
6963
"""
7064
...
7165

@@ -102,7 +96,11 @@ class LocalRegistry(ModelRegistry):
10296

10397
exclude: ClassVar = [".DS_Store", "compare"]
10498

105-
def __init__(self, path: str | PathLike, namefile_pattern: str = "mfsim.nam"):
99+
def __init__(
100+
self,
101+
path: str | PathLike | Iterable[str | PathLike],
102+
namefile_pattern: str = "mfsim.nam",
103+
):
106104
"""
107105
Create a registry from models under the given
108106
directory path.
@@ -112,12 +110,23 @@ def __init__(self, path: str | PathLike, namefile_pattern: str = "mfsim.nam"):
112110
are identified by the presence of a namefile
113111
matching `namefile_pattern`.
114112
"""
115-
self._path = Path(path).expanduser().resolve().absolute()
113+
# check if path is iterable of str\pathlike
114+
if isinstance(path, Iterable) and not isinstance(path, str):
115+
path = [Path(p).expanduser().resolve().absolute() for p in path] # type: ignore
116+
missing = [p for p in path if not p.is_dir()] # type: ignore
117+
if any(missing):
118+
raise NotADirectoryError(
119+
f"Directory paths not found: {', '.join(missing)}" # type: ignore
120+
)
121+
self._path = path
122+
else:
123+
path = Path(path).expanduser().resolve().absolute()
124+
if not path.is_dir():
125+
raise NotADirectoryError(f"Directory path not found: {path}")
126+
self._path = [path]
116127
self._files: dict[str, dict[str, str | None]] = {}
117128
self._models: dict[str, list[str]] = {}
118129
self._examples: dict[str, list[str]] = {}
119-
if not self._path.is_dir():
120-
raise NotADirectoryError(f"Path {self._path} is not a directory.")
121130
self.namefile_pattern = namefile_pattern
122131
self.index()
123132

@@ -131,25 +140,28 @@ def index(self):
131140
self._files = {}
132141
self._models = {}
133142
self._examples = {}
134-
model_paths = get_model_paths(self._path, namefile=self.namefile_pattern)
135-
for model_path in model_paths:
136-
model_path = model_path.expanduser().resolve().absolute()
137-
rel_path = model_path.relative_to(self._path)
138-
model_name = "/".join(rel_path.parts)
139-
self._models[model_name] = []
140-
if len(rel_path.parts) > 1:
141-
name = rel_path.parts[0]
142-
if name not in self._examples:
143-
self._examples[name] = []
144-
self._examples[name].append(model_name)
145-
for p in model_path.rglob("*"):
146-
if not p.is_file() or any(e in p.name for e in LocalRegistry.exclude):
147-
continue
148-
relpath = p.expanduser().absolute().relative_to(self._path)
149-
name = "/".join(relpath.parts)
150-
hash = _sha256(p)
151-
self._files[name] = {"hash": hash}
152-
self._models[model_name].append(name)
143+
for path in self._path:
144+
model_paths = get_model_paths(path, namefile=self.namefile_pattern)
145+
for model_path in model_paths:
146+
model_path = model_path.expanduser().resolve().absolute()
147+
rel_path = model_path.relative_to(path)
148+
model_name = "/".join(rel_path.parts)
149+
self._models[model_name] = []
150+
if len(rel_path.parts) > 1:
151+
name = rel_path.parts[0]
152+
if name not in self._examples:
153+
self._examples[name] = []
154+
self._examples[name].append(model_name)
155+
for p in model_path.rglob("*"):
156+
if not p.is_file() or any(
157+
e in p.name for e in LocalRegistry.exclude
158+
):
159+
continue
160+
relpath = p.expanduser().absolute().relative_to(path)
161+
name = "/".join(relpath.parts)
162+
hash = _sha256(p)
163+
self._files[name] = {"hash": hash, "path": p, "relpath": relpath}
164+
self._models[model_name].append(p)
153165

154166
def copy_to(
155167
self, workspace: str | PathLike, model_name: str, verbose: bool = False
@@ -159,7 +171,7 @@ def copy_to(
159171
The workspace will be created if it does not exist.
160172
"""
161173

162-
if not any(files := self.models.get(model_name, [])):
174+
if not any(file_paths := self.models.get(model_name, [])):
163175
return None
164176
# create the workspace if needed
165177
workspace = Path(workspace).expanduser().absolute()
@@ -169,18 +181,18 @@ def copy_to(
169181
# copy the files. some might be in nested folders,
170182
# but the first is guaranteed not to be, so use it
171183
# to determine relative path in the new workspace.
172-
base = Path(files[0]).parent
173-
for file in [Path(self._path) / f for f in files]:
184+
base = Path(file_paths[0]).parent
185+
for file_path in file_paths:
174186
if verbose:
175-
print(f"Copying {file} to workspace")
176-
path = workspace / file.relative_to(base)
177-
path.parent.mkdir(parents=True, exist_ok=True)
178-
copy(file, workspace / file.relative_to(base))
187+
print(f"Copying {file_path} to workspace")
188+
dest = workspace / file_path.relative_to(base)
189+
dest.parent.mkdir(parents=True, exist_ok=True)
190+
copy(file_path, dest)
179191
return workspace
180192

181193
@property
182-
def path(self) -> Path:
183-
return self._path
194+
def path(self) -> list[Path]:
195+
return self._path # type: ignore
184196

185197
@property
186198
def files(self) -> dict:

0 commit comments

Comments
 (0)