Skip to content
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
64 changes: 63 additions & 1 deletion autotest/test_dfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def pytest_generate_tests(metafunc):
convert(DFN_DIR, TOML_DIR)
dfn_paths = list(DFN_DIR.glob("*.dfn"))
assert all(
(TOML_DIR / f"{dfn.stem}.toml").is_file()
(TOML_DIR / f"{dfn.stem.replace('-nam', '')}.toml").is_file()
for dfn in dfn_paths
if "common" not in dfn.stem
)
Expand Down Expand Up @@ -61,3 +61,65 @@ def test_load_v2(toml_name):
def test_load_all(version):
dfns = Dfn.load_all(VERSIONS[version], version=version)
assert any(dfns)


@requires_pkg("boltons")
def test_load_tree():
import tempfile

import tomli

with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
convert(DFN_DIR, tmp_path)

# Test file conversion and naming
assert (tmp_path / "sim.toml").exists()
assert (tmp_path / "gwf.toml").exists()
assert not (tmp_path / "sim-nam.toml").exists()

# Test parent relationships in files
with (tmp_path / "sim.toml").open("rb") as f:
sim_data = tomli.load(f)
assert sim_data["name"] == "sim"
assert "parent" not in sim_data

with (tmp_path / "gwf.toml").open("rb") as f:
gwf_data = tomli.load(f)
assert gwf_data["name"] == "gwf"
assert gwf_data["parent"] == "sim"

# Test hierarchy enforcement and completeness
dfns = Dfn.load_all(tmp_path, version=2)
roots = [name for name, dfn in dfns.items() if not dfn.get("parent")]
assert len(roots) == 1
assert roots[0] == "sim"

for dfn in dfns.values():
parent = dfn.get("parent")
if parent:
assert parent in dfns

# Test tree building and navigation
tree = Dfn.load_tree(tmp_path, version=2)
assert "sim" in tree
assert tree["sim"]["name"] == "sim"

for model_type in ["gwf", "gwt", "gwe"]:
if model_type in tree["sim"]:
assert tree["sim"][model_type]["name"] == model_type
assert tree["sim"][model_type]["parent"] == "sim"

if "gwf" in tree["sim"]:
gwf_packages = [
k
for k in tree["sim"]["gwf"].keys()
if k.startswith("gwf-") and isinstance(tree["sim"]["gwf"][k], dict)
]
assert len(gwf_packages) > 0

if "gwf-dis" in tree["sim"]["gwf"]:
dis = tree["sim"]["gwf"]["gwf-dis"]
assert dis["name"] == "gwf-dis"
assert dis["parent"] == "gwf"
assert "options" in dis or "dimensions" in dis
36 changes: 36 additions & 0 deletions modflow_devtools/dfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class Dfn(TypedDict):
name: str
advanced: bool = False
multi: bool = False
parent: str | None = None
ref: Ref | None = None
sln: Sln | None = None
fkeys: Dfns | None = None
Expand Down Expand Up @@ -639,6 +640,41 @@ def load_all(dfndir: PathLike, version: FormatVersion = 1) -> Dfns:
else:
raise ValueError(f"Unsupported version, expected one of {version.__args__}")

@staticmethod
def load_tree(dfndir: PathLike, version: FormatVersion = 2) -> dict:
"""Load all definitions and return as hierarchical tree."""
dfns = Dfn.load_all(dfndir, version)
return infer_tree(dfns)


def infer_tree(dfns: dict[str, Dfn]) -> dict:
"""Infer the component hierarchy from definitions.

Enforces single root requirement - must be exactly one component
with no parent, and it must be named 'sim'.
"""
roots = [name for name, dfn in dfns.items() if not dfn.get("parent")]

if len(roots) != 1:
raise ValueError(
f"Expected exactly one root component, found {len(roots)}: {roots}"
)

root_name = roots[0]
if root_name != "sim":
raise ValueError(f"Root component must be named 'sim', found '{root_name}'")

def add_children(node_name: str) -> dict:
node = dfns[node_name].copy()
children = [
name for name, dfn in dfns.items() if dfn.get("parent") == node_name
]
for child in children:
node[child] = add_children(child)
return node

return {root_name: add_children(root_name)}


def get_dfns(
owner: str, repo: str, ref: str, outdir: str | PathLike, verbose: bool = False
Expand Down
42 changes: 41 additions & 1 deletion modflow_devtools/dfn2toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,47 @@ def convert(indir: PathLike, outdir: PathLike):
outdir = Path(outdir).expanduser().absolute()
outdir.mkdir(exist_ok=True, parents=True)
for dfn in Dfn.load_all(indir).values():
with Path.open(outdir / f"{dfn['name']}.toml", "wb") as f:
dfn_name = dfn["name"]

# Determine new filename and parent relationship
if dfn_name == "sim-nam":
filename = "sim.toml"
dfn = dfn.copy()
dfn["name"] = "sim"
# No parent - this is root
elif dfn_name.endswith("-nam"):
# Model name files: gwf-nam -> gwf.toml, parent = "sim"
model_type = dfn_name[:-4] # Remove "-nam"
filename = f"{model_type}.toml"
dfn = dfn.copy()
dfn["name"] = model_type
dfn["parent"] = "sim"
elif dfn_name.startswith("exg-"):
# Exchanges: parent = "sim"
filename = f"{dfn_name}.toml"
dfn = dfn.copy()
dfn["parent"] = "sim"
elif dfn_name.startswith("sln-"):
# Solutions: parent = "sim"
filename = f"{dfn_name}.toml"
dfn = dfn.copy()
dfn["parent"] = "sim"
elif dfn_name.startswith("utl-"):
# Utilities: parent = "sim"
filename = f"{dfn_name}.toml"
dfn = dfn.copy()
dfn["parent"] = "sim"
elif "-" in dfn_name:
# Packages: gwf-dis -> parent = "gwf"
model_type = dfn_name.split("-")[0]
filename = f"{dfn_name}.toml"
dfn = dfn.copy()
dfn["parent"] = model_type
else:
# Default case
filename = f"{dfn_name}.toml"

with Path.open(outdir / filename, "wb") as f:

def drop_none_or_empty(path, key, value):
if value is None or value == "" or value == [] or value == {}:
Expand Down