Skip to content

Commit

Permalink
refactor(dfn): use boltons remap, better comments/naming (MODFLOW-USG…
Browse files Browse the repository at this point in the history
  • Loading branch information
wpbonelli authored Jan 26, 2025
1 parent 50cba8b commit 362ecd5
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 55 deletions.
59 changes: 27 additions & 32 deletions modflow_devtools/dfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,23 @@ class Var(TypedDict):
description: Optional[str] = None


class Sub(TypedDict):
class Ref(TypedDict):
"""
A subpackage specification.
A foreign-key-like reference between a file input variable
in a referring input component and another input component.
A `Dfn` which declares itself a subpackage can be referred
to by other definitions, via a filepath variable acting as
a foreign key. The referring component's `__init__` method
is modified, the subpackage variable named `val` replacing
the `key` parameter, such that the referring component can
accept data for the subpackage directly instead of by file.
in a referring input component and another input component
referenced by it. Previously known as a "subpackage".
A `Dfn` with a nonempty `ref` can be referred to by other
component definitions, via a filepath variable which acts
as a foreign key. If such a variable is detected when any
component is loaded, the component's `__init__` method is
modified, such that the variable named `val`, residing in
the referenced component, replaces the variable with name
`key` in the referencing component, i.e., the foreign key
filepath variable, This forces a referencing component to
accept a subcomponent's data directly, as if it were just
a variable, rather than indirectly, with the subcomponent
loaded up from a file identified by the filepath variable.
"""

key: str
Expand Down Expand Up @@ -152,9 +156,8 @@ class Dfn(TypedDict):
name: str
advanced: bool = False
multi: bool = False
sub: Optional[Sub] = None
ref: Optional[Ref] = None
sln: Optional[Sln] = None
blocks: Optional[dict[str, Vars]] = None
fkeys: Optional[Dfns] = None

@staticmethod
Expand Down Expand Up @@ -237,22 +240,14 @@ def _load_v1(cls, f, name, **kwargs) -> "Dfn":
Temporary load routine for the v1 DFN format.
"""

# if we have any subpackage references
# we need to watch for foreign key vars
# (file input vars) and register matches
refs = kwargs.pop("refs", {})
fkeys = {}

# load dfn as flat multidict + str metadata
refs = kwargs.pop("refs", {})
flat, meta = Dfn._load_v1_flat(f, **kwargs)

def _load_variable(var: dict[str, Any]) -> Var:
"""
Convert an input variable from its original representation
in a definition file to a structured, Python-friendly form.
This involves trimming unneeded attributes and setting
some others.
Convert an input variable from its representation in a
legacy definition file to a structured form.
Notes
-----
Expand Down Expand Up @@ -410,7 +405,7 @@ def _fields() -> Vars:
f"{ref['abbr']} package documentation for more information."
),
default=None,
subpackage=ref,
ref=ref,
**var,
)

Expand Down Expand Up @@ -438,7 +433,7 @@ def _fields() -> Vars:
transient_block = transient_index["block"]
blocks[transient_block]["transient"] = True

# remove unneeded attributes
# remove unneeded variable attributes
def remove_attrs(path, key, value):
if key in ["block", "in_record", "tagged", "preserve_case"]:
return False
Expand Down Expand Up @@ -466,7 +461,7 @@ def _sln() -> Optional[Sln]:
return Sln(abbr=abbr, pattern=pattern)
return None

def _sub() -> Optional[Sub]:
def _sub() -> Optional[Ref]:
def _parent():
line = next(
iter(
Expand Down Expand Up @@ -509,7 +504,7 @@ def _rest():
parent = _parent()
rest = _rest()
if parent and rest:
return Sub(parent=parent, **rest)
return Ref(parent=parent, **rest)
return None

return cls(
Expand Down Expand Up @@ -562,14 +557,14 @@ def _load_all_v1(dfndir: PathLike) -> Dfns:
with common_path.open() as f:
common, _ = Dfn._load_v1_flat(f)

# load subpackages
# load references (subpackages)
refs = {}
for path in paths:
with path.open() as f:
dfn = Dfn.load(f, name=path.stem, common=common)
subpkg = dfn.get("sub", None)
if subpkg:
refs[subpkg["key"]] = subpkg
ref = dfn.get("ref", None)
if ref:
refs[ref["key"]] = ref

# load definitions
dfns: Dfns = {}
Expand Down
17 changes: 8 additions & 9 deletions modflow_devtools/dfn2toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from pathlib import Path

import tomli_w as tomli
from boltons.iterutils import remap

from modflow_devtools.dfn import Dfn
from modflow_devtools.misc import filter_recursive


def convert(indir: PathLike, outdir: PathLike):
Expand All @@ -16,14 +16,13 @@ def convert(indir: PathLike, outdir: PathLike):
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:
tomli.dump(
filter_recursive(
dfn,
lambda v: v is not None
and not (isinstance(v, dict) and not any(v)),
),
f,
)

def drop_none_or_empty(path, key, value):
if value is None or value == "" or value == [] or value == {}:
return False
return True

tomli.dump(remap(dfn, visit=drop_none_or_empty), f)


if __name__ == "__main__":
Expand Down
15 changes: 1 addition & 14 deletions modflow_devtools/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import traceback
from _warnings import warn
from ast import literal_eval
from collections.abc import Mapping
from contextlib import contextmanager
from functools import wraps
from importlib import metadata
Expand All @@ -13,7 +12,7 @@
from shutil import which
from subprocess import run
from timeit import timeit
from typing import Any, Callable, Optional
from typing import Optional
from urllib import request
from urllib.error import URLError

Expand Down Expand Up @@ -550,15 +549,3 @@ def set_env(*remove, **update):
finally:
env.update(update_after)
[env.pop(k) for k in remove_after]


def filter_recursive(d: Any, pred: Callable) -> Any:
"""
If the object is a mapping, recursively apply
the predicate, keeping passing entries. Return
other objects unchanged.
"""
if isinstance(d, Mapping):
return {k: filter_recursive(v, pred) for k, v in d.items() if pred(v)}
else:
return d

0 comments on commit 362ecd5

Please sign in to comment.