Skip to content

Commit 5ff8d7f

Browse files
authored
fix(dfns): remove fkeys/ref; add DfnSpec.dump/s() (#305)
Add DfnSpec.dump() and DfnSpec.dumps() methods to serialize the full spec to a single hierarchical TOML blob. This standalone representation might be more convenient than a directory of files for some use cases. E.g., MF6 could emit the entire spec as a TOML blob (MODFLOW-ORG/modflow6#2777) for flopy to generate an MF6 module from, e.g. mf6 --spec | flopy.mf6.utils.generate_classes - Also remove the fkeys attribute/concept (field-level foreign keys encoding subpackage attachment) and Ref dataclass. Represent "floating" subpackage relationships (i.e., a subpackage may be attached to one of several possible parent components) instead via the existing Dfn.subcomponents field, which lists the string names of component types a given component can accept as subpackages. This is simpler and more consistent with how fixed-parent hierarchy is already handled via Dfn.children. Also normalize subcomponent abbreviations to lowercase to match component name conventions elsewhere.
1 parent 4e8ff99 commit 5ff8d7f

3 files changed

Lines changed: 30 additions & 24 deletions

File tree

modflow_devtools/dfns/__init__.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
)
3030
from modflow_devtools.dfns.schema.block import Block, Blocks, block_sort_key
3131
from modflow_devtools.dfns.schema.field import Field, Fields
32-
from modflow_devtools.dfns.schema.ref import Ref
3332
from modflow_devtools.dfns.schema.v1 import SCALAR_TYPES as V1_SCALAR_TYPES
3433
from modflow_devtools.dfns.schema.v1 import FieldV1
3534
from modflow_devtools.dfns.schema.v2 import FieldV2
@@ -61,7 +60,6 @@
6160
"FieldV2",
6261
"Fields",
6362
"LocalDfnRegistry",
64-
"Ref",
6563
"RemoteDfnRegistry",
6664
"block_sort_key",
6765
"get_dfn",
@@ -106,9 +104,6 @@ class Dfn:
106104
Whether this is a multi-package.
107105
ftype : str | None
108106
File type identifier.
109-
ref : Ref | None
110-
Metadata if this component is a subpackage (child's perspective).
111-
Populated from DFN comments like: # flopy subpackage <key> <abbr> <param> <val>
112107
blocks : Blocks | None
113108
Block definitions containing field specifications.
114109
children : Dfns | None
@@ -125,7 +120,6 @@ class Dfn:
125120
advanced: bool = False
126121
multi: bool = False
127122
ftype: str | None = None
128-
ref: Ref | None = None
129123
blocks: Blocks | None = None
130124
children: Dfns | None = None
131125
subcomponents: list[str] | None = None
@@ -258,6 +252,23 @@ def __contains__(self, name: object) -> bool:
258252
"""Check if a component exists by name."""
259253
return name in self._flat
260254

255+
def dump(self, f) -> None:
256+
"""Serialize the full spec to a TOML byte stream."""
257+
import tomli_w
258+
259+
doc = {"schema_version": str(self.schema_version)}
260+
for name, dfn in self._flat.items():
261+
doc[name] = _toml_safe(remap(asdict(dfn), visit=drop_none_or_empty))
262+
f.write(tomli_w.dumps(doc).encode())
263+
264+
def dumps(self) -> str:
265+
"""Serialize the full spec to a TOML string."""
266+
import io
267+
268+
buf = io.BytesIO()
269+
self.dump(buf)
270+
return buf.getvalue().decode()
271+
261272
@classmethod
262273
def load(
263274
cls,
@@ -563,13 +574,23 @@ def map(self, dfn: Dfn) -> Dfn:
563574
advanced=dfn.advanced,
564575
multi=dfn.multi,
565576
ftype=dfn.ftype or (dfn.name.split("-", 1)[1].upper() if "-" in dfn.name else None),
566-
ref=dfn.ref,
567577
blocks=MapV1To2.map_blocks(dfn),
568578
schema_version=v2,
569579
parent=dfn.parent,
570580
)
571581

572582

583+
def _toml_safe(obj):
584+
"""Recursively coerce non-TOML-native types to str."""
585+
if isinstance(obj, dict):
586+
return {k: _toml_safe(v) for k, v in obj.items()}
587+
if isinstance(obj, list):
588+
return [_toml_safe(v) for v in obj]
589+
if isinstance(obj, (str, int, float, bool)) or obj is None:
590+
return obj
591+
return str(obj)
592+
593+
573594
def map(
574595
dfn: Dfn,
575596
schema_version: str | Version = "2",
@@ -618,7 +639,6 @@ def load(f, format: str = "dfn", **kwargs) -> Dfn:
618639
"multi": data.pop("multi", False),
619640
"ftype": data.pop("ftype", None)
620641
or (dfn_name.split("-", 1)[1].upper() if dfn_name and "-" in dfn_name else None),
621-
"ref": data.pop("ref", None),
622642
}
623643

624644
if (expected_name := kwargs.pop("name", None)) is not None:

modflow_devtools/dfns/parse.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,16 @@ def parse_mf6_subpackages(meta: list[str]) -> list[str]:
101101
Returns
102102
-------
103103
list[str]
104-
List of uppercase component abbreviations (e.g., ['UTL-NCF']).
104+
List of lowercase component abbreviations (e.g., ['utl-ncf']).
105105
106106
See Also
107107
--------
108108
Dfn.subcomponents : Stores the result (schema-level constraint).
109-
Dfn.fkeys : Field-level foreign keys from flopy subpackage declarations.
110109
"""
111110
result = []
112111
for m in meta:
113112
if m.startswith("mf6-subpackage "):
114-
abbr = m.removeprefix("mf6-subpackage ").strip().upper()
113+
abbr = m.removeprefix("mf6-subpackage ").strip().lower()
115114
result.append(abbr)
116115
return result
117116

modflow_devtools/dfns/schema/ref.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)