From 2a8830f9f46d71fc7ce1ea45a5df8e074e2cf019 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Fri, 29 May 2026 03:40:11 -0700 Subject: [PATCH 1/4] upgrade devtools --- flopy4/mf6/codec/filters.py | 11 +- flopy4/mf6/codec/reader/grammar/__init__.py | 6 +- flopy4/mf6/codec/reader/grammar/filters.py | 30 +-- flopy4/mf6/codec/reader/transformer/typed.py | 40 ++-- flopy4/mf6/converter/egress/unstructure.py | 3 +- flopy4/mf6/spec.py | 28 ++- flopy4/mf6/utils/codegen/filters.py | 225 ++++++++++--------- flopy4/mf6/utils/codegen/generate_classes.py | 2 +- flopy4/mf6/utils/codegen/make.py | 149 ++++++------ flopy4/mf6/utils/codegen/overrides.py | 7 +- pixi.lock | 88 +++++--- test/test_mf6_codegen.py | 80 +++---- test/test_mf6_grammar_gen.py | 31 ++- test/test_mf6_reader.py | 45 ++-- 14 files changed, 394 insertions(+), 351 deletions(-) diff --git a/flopy4/mf6/codec/filters.py b/flopy4/mf6/codec/filters.py index 7db96bd6..8eefd829 100644 --- a/flopy4/mf6/codec/filters.py +++ b/flopy4/mf6/codec/filters.py @@ -3,15 +3,12 @@ from typing import Any import xarray as xr -from modflow_devtools.dfns.schema.field import Field -from modflow_devtools.dfns.schema.v2 import FieldType +from modflow_devtools.dfn.schema import FieldType def field_type(value: Any) -> FieldType: """Get a value's type according to the MF6 specification.""" - if isinstance(value, Field): - return value.type if isinstance(value, bool): return "keyword" if isinstance(value, int): @@ -20,7 +17,11 @@ def field_type(value: Any) -> FieldType: return "double" if isinstance(value, str): return "string" - if isinstance(value, (dict, tuple)): + if isinstance(value, tuple): + return "record" + if isinstance(value, dict): + if type_ := value.get("type", None): + return type_ return "record" if isinstance(value, xr.DataArray): if value.dtype == "object": diff --git a/flopy4/mf6/codec/reader/grammar/__init__.py b/flopy4/mf6/codec/reader/grammar/__init__.py index 17e6aab5..8280d5f5 100644 --- a/flopy4/mf6/codec/reader/grammar/__init__.py +++ b/flopy4/mf6/codec/reader/grammar/__init__.py @@ -58,10 +58,10 @@ def make_grammar(dfn: Dfn, outdir: PathLike): outdir = Path(outdir).expanduser().resolve().absolute() env = _get_env() template = env.get_template("component.lark.jinja") - target_path = outdir / f"{dfn.name}.lark" - blocks, fields = _get_template_data(dfn.blocks) + target_path = outdir / f"{dfn['name']}.lark" + blocks, fields = _get_template_data(dfn["blocks"]) with open(target_path, "w") as f: - name = dfn.name + name = dfn["name"] f.write(template.render(name=name, blocks=blocks, fields=fields)) diff --git a/flopy4/mf6/codec/reader/grammar/filters.py b/flopy4/mf6/codec/reader/grammar/filters.py index 6eac2187..75e8cc22 100644 --- a/flopy4/mf6/codec/reader/grammar/filters.py +++ b/flopy4/mf6/codec/reader/grammar/filters.py @@ -1,12 +1,12 @@ from collections.abc import Mapping -from modflow_devtools.dfns.schema.v2 import FieldV2 +from modflow_devtools.dfn.schema import Field -def field_type(field: FieldV2) -> str: - match field.type: - case t if t in ["string", "integer", "double"] and field.shape: - if "period" in field.block: +def field_type(field: Field) -> str: + match field["type"]: + case t if t in ["string", "integer", "double"] and field.get("shape", None): + if "period" in field["block"]: return "list" return "array" case "keyword": @@ -14,17 +14,17 @@ def field_type(field: FieldV2) -> str: case "union": return "" # keystrings generate their own union rules case _: - return field.type + return field["type"] -def record_child_type(field: FieldV2) -> str: +def record_child_type(field: Field) -> str: """ Get the grammar type for a field within a record context. In records, string fields should use 'word' instead of 'string' to avoid consuming the rest of the line (since string matches token+ NEWLINE). """ - match field.type: + match field["type"]: case "string": return "word" # Use word for strings in records to match single tokens case t if t in ["double", "integer"]: @@ -34,21 +34,21 @@ def record_child_type(field: FieldV2) -> str: case "union": return "" # unions generate their own union rules case _: - return field.type + return field["type"] -def is_period_list_field(field: FieldV2) -> bool: +def is_period_list_field(field: Field) -> bool: """Check if a field is part of a period block list/recarray.""" - if not field.shape or not field.block: + if not field.get("shape", None) or not field.get("block", None): return False return ( - "period" in field.block - and field.type in ["string", "integer", "double"] - and field.shape is not None + "period" in field["block"] + and field["type"] in ["string", "integer", "double"] + and field["shape"] is not None ) -def group_period_fields(block_fields: Mapping[str, FieldV2]) -> dict[str, list[str]]: +def group_period_fields(block_fields: Mapping[str, Field]) -> dict[str, list[str]]: """ Group period block fields that should be combined into a single list. diff --git a/flopy4/mf6/codec/reader/transformer/typed.py b/flopy4/mf6/codec/reader/transformer/typed.py index ac2e5fa1..a59ee6d0 100644 --- a/flopy4/mf6/codec/reader/transformer/typed.py +++ b/flopy4/mf6/codec/reader/transformer/typed.py @@ -4,7 +4,7 @@ import numpy as np import xarray as xr from lark import Token, Transformer -from modflow_devtools.dfn import Dfn +from modflow_devtools.dfn import Dfn, get_fields from flopy4.utils import parse_number @@ -15,8 +15,8 @@ class TypedTransformer(Transformer): def __init__(self, visit_tokens=False, dfn: Dfn = None): super().__init__(visit_tokens) self.dfn = dfn - self.blocks = dfn.blocks if dfn else None - self.fields = dfn.fields if dfn else None + self.blocks = dfn["blocks"] if dfn else None + self.fields = get_fields(dfn) if dfn else None # Create a flattened fields dict that includes nested fields self._flat_fields = self._flatten_fields(self.fields) if self.fields else None @@ -32,13 +32,13 @@ def _flatten_fields(self, fields: dict) -> dict: """Recursively flatten fields dict to include children of records and unions.""" flat = dict(fields) # Start with top-level fields for field in fields.values(): - if hasattr(field, "children") and field.children: + if "children" in field and field["children"]: # Add children fields - for child_name, child_field in field.children.items(): + for child_name, child_field in field["children"].items(): flat[child_name] = child_field # Recursively flatten nested children - if hasattr(child_field, "children") and child_field.children: - nested_flat = self._flatten_fields(child_field.children) + if "children" in child_field and child_field["children"]: + nested_flat = self._flatten_fields(child_field["children"]) flat.update(nested_flat) return flat @@ -143,7 +143,7 @@ def string(self, items: list[Any]) -> str: return value.strip("\"'") else: # It's a tree, extract the token value - return str(value.children[0]) if hasattr(value, "children") else str(value) + return str(value["children"][0]) if hasattr(value, "children") else str(value) def simple_string(self, items: list[Any]) -> str: """Handle simple string (unquoted word or escaped string).""" @@ -197,7 +197,7 @@ def record(self, items: list[Any]) -> list[Any]: token_child = item.children[0] if hasattr(token_child, "children") and len(token_child.children) > 0: # This is a number tree, get the actual value - values.append(token_child.children[0]) + values.append(token_child["children"][0]) else: # This is a direct value (string) values.append(token_child) @@ -270,14 +270,14 @@ def __default__(self, data, children, meta): field_name, alternative_name = parts if (parent_field := self._flat_fields.get(field_name, None)) is not None: if ( - parent_field.type == "union" - and hasattr(parent_field, "children") - and parent_field.children - and alternative_name in parent_field.children + parent_field["type"] == "union" + and "children" in parent_field + and parent_field["children"] + and alternative_name in parent_field["children"] ): # This is a union alternative - alt_field = parent_field.children[alternative_name] - if alt_field.type == "keyword": + alt_field = parent_field["children"][alternative_name] + if alt_field["type"] == "keyword": # Keyword alternatives return just the alternative name return alternative_name else: @@ -289,17 +289,17 @@ def __default__(self, data, children, meta): # Try with hyphens instead of underscores (reverse of to_rule_name) field = self._flat_fields.get(data.replace("_", "-"), None) if field is not None: - if field.type == "keyword": + if field["type"] == "keyword": return data, True - elif field.type == "record" and hasattr(field, "children") and field.children: + elif field["type"] == "record" and "children" in field and field["children"]: # Transform record fields into dicts with child field names as keys # Keyword children are literals in the grammar and don't appear in children list # Only non-keyword children appear in the children list record_dict = {} non_keyword_children = [ (name, child) - for name, child in field.children.items() - if child.type != "keyword" + for name, child in field["children"].items() + if child["type"] != "keyword" ] for i, (child_name, child_field) in enumerate(non_keyword_children): if i < len(children): @@ -309,7 +309,7 @@ def __default__(self, data, children, meta): else: record_dict[child_name] = children[i] return data, record_dict - elif field.type == "union" and hasattr(field, "children") and field.children: + elif field["type"] == "union" and "children" in field and field["children"]: # For union fields, return the transformed child # The parser will have selected one alternative return data, children[0] if len(children) == 1 else children diff --git a/flopy4/mf6/converter/egress/unstructure.py b/flopy4/mf6/converter/egress/unstructure.py index 3cc7d95e..632f812b 100644 --- a/flopy4/mf6/converter/egress/unstructure.py +++ b/flopy4/mf6/converter/egress/unstructure.py @@ -7,14 +7,13 @@ import numpy as np import xarray as xr import xattree -from modflow_devtools.dfns.schema.block import block_sort_key from xattree import XatSpec from flopy4.mf6.binding import Binding from flopy4.mf6.component import Component from flopy4.mf6.constants import FILL_DNODATA from flopy4.mf6.context import Context -from flopy4.mf6.spec import FileInOut, blocks_dict +from flopy4.mf6.spec import FileInOut, block_sort_key, blocks_dict def _path_to_tuple(name: str, value: Path, inout: FileInOut) -> tuple[str, ...]: diff --git a/flopy4/mf6/spec.py b/flopy4/mf6/spec.py index acd08eef..bdc7509b 100644 --- a/flopy4/mf6/spec.py +++ b/flopy4/mf6/spec.py @@ -5,19 +5,13 @@ import builtins import types -import warnings from datetime import datetime from pathlib import Path from typing import Literal, Union, get_args, get_origin import numpy as np from attrs import NOTHING, Attribute - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message=".*modflow_devtools.dfns.*experimental.*") - from modflow_devtools.dfns.schema.block import block_sort_key -from modflow_devtools.dfns.schema.v2 import Field as FieldV2 -from modflow_devtools.dfns.schema.v2 import FieldType +from modflow_devtools.dfn.schema import Field, FieldType from flopy4.spec import array as flopy_array from flopy4.spec import coord as flopy_coord @@ -352,7 +346,7 @@ def get_field_type(attribute: Attribute) -> FieldType: raise ValueError(f"Could not map {attribute.name} to a valid MF6 type.") -def to_field(attribute: Attribute) -> FieldV2: +def to_field(attribute: Attribute) -> Field: """ Convert a `xattree` field specification to a field as defined by the MODFLOW 6 input definition language: @@ -360,7 +354,7 @@ def to_field(attribute: Attribute) -> FieldV2: """ if (xatmeta := attribute.metadata.get("xattree", None)) is None: raise ValueError(f"Attribute {attribute.name} in {attribute.name} has no xattree metadata.") - return FieldV2( + return Field( name=attribute.name, type=get_field_type(attribute), shape=xatmeta.get("dims", None), @@ -371,3 +365,19 @@ def to_field(attribute: Attribute) -> FieldV2: if attribute.metadata.get("kind", None) == "child" # type: ignore else None, # type: ignore ) + + +def block_sort_key(item) -> int: + # TODO: remove when no longer needed, block order should + # be as appears in the definition + k, _ = item + if k == "options": + return 0 + elif k == "dimensions": + return 1 + elif k == "griddata": + return 2 + elif "period" in k: + return 4 + else: + return 3 diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index 31272d1a..c973ca15 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -1,7 +1,7 @@ """ Python-side filters for MF6 code generation. -Converts modflow_devtools.dfns Dfn/Field dataclasses to the context +Converts modflow_devtools.dfn.Dfn/Field dataclasses to the context dicts consumed by Jinja templates. Keeping computation here (rather than in Jinja macros) makes edge-case handling easier to test and debug. """ @@ -12,8 +12,7 @@ from dataclasses import dataclass from pathlib import Path -from modflow_devtools.dfns import Dfn -from modflow_devtools.dfns.schema.field import Field +from modflow_devtools.dfn import Dfn, Field from .overrides import apply as apply_override @@ -82,7 +81,7 @@ def output_path(dfn_name: str, root: Path) -> Path: def has_period_block(dfn: Dfn) -> bool: """True if the DFN defines a period block (stress package).""" - return "period" in (dfn.blocks or {}) + return "period" in (dfn["blocks"] or {}) def has_dimensions_block(dfn: Dfn) -> bool: @@ -93,7 +92,7 @@ def has_dimensions_block(dfn: Dfn) -> bool: Packages like MVR/BUY/VSC have user-specified dimension scalars (maxmvr, maxpackages, nrhospecies) that must NOT be init=False. """ - dim_block = (dfn.blocks or {}).get("dimensions", {}) + dim_block = (dfn.get("blocks", {}) or {}).get("dimensions", {}) return "maxbound" in dim_block @@ -130,26 +129,26 @@ def has_dimensions_block(dfn: Dfn) -> bool: def _has_file_child(f: Field) -> bool: - if f.children: - return "filein" in f.children or "fileout" in f.children + if f.get("children", None): + return "filein" in f["children"] or "fileout" in f["children"] # v1 DFN fields encode subfields in the type string, e.g. # "record ts6 filein ts6_filename" — check there instead. - return " filein " in f.type or " fileout " in f.type + return " filein " in f["type"] or " fileout " in f["type"] def _has_file_child_of(f: Field, kind: str) -> bool: """True if the record has a child of the given kind ('filein' or 'fileout').""" - if f.children: - return kind in f.children - return f" {kind} " in f.type + if f.get("children", None): + return kind in f["children"] + return f" {kind} " in f["type"] def _file_record_subfield_names(f: Field) -> frozenset[str]: """Return the subfield names encoded in a v1 DFN record type string.""" - if f.children: - return frozenset(f.children) + if f.get("children", None): + return frozenset(f["children"]) # type string format: "record name1 name2 ..." - parts = f.type.split() + parts = f["type"].split() return frozenset(parts[1:]) if len(parts) > 1 else frozenset() @@ -162,15 +161,15 @@ def _resolve_alt_grid(shape: str) -> str: def _has_complex_shape(f: Field) -> bool: """True for shapes with unsupported alternative-grid or arithmetic notation.""" - if not f.shape: + if not f.get("shape", None): return False - resolved = _resolve_alt_grid(f.shape) + resolved = _resolve_alt_grid(f["shape"]) return "*" in resolved or ";" in resolved def is_scalar(f: Field) -> bool: """True for simple scalar fields (no shape).""" - return f.type in _SCALAR_TYPES and not f.shape + return f["type"] in _SCALAR_TYPES and not f.get("shape", None) _STRING_LENGTH_DIMS = frozenset({"lenbigline", "linelength"}) @@ -183,11 +182,15 @@ def is_array(f: Field) -> bool: """ # TODO: emit string fields shaped only by lenbigline/linelength as field(Optional[str]); # for now exclude so they fall to scalar handling (see ncf.py Ncf.wkt for the pattern). - if f.type == "string" and f.shape and all(s in _STRING_LENGTH_DIMS for s in f.shape): + if ( + f["type"] == "string" + and f.get("shape", None) + and all(s in _STRING_LENGTH_DIMS for s in f.get("shape", None)) + ): return False return ( - f.type in _ARRAY_BASE_TYPES - and bool(f.shape) + f["type"] in _ARRAY_BASE_TYPES + and bool(f.get("shape", None)) and not _has_complex_shape(f) and not is_aux_list_field(f) ) @@ -195,12 +198,12 @@ def is_array(f: Field) -> bool: def is_keyword_array(f: Field) -> bool: """True for boolean-array fields (keyword type with shape).""" - return f.type == "keyword" and bool(f.shape) and not _has_complex_shape(f) + return f["type"] == "keyword" and bool(f.get("shape", None)) and not _has_complex_shape(f) def is_file_record(f: Field) -> bool: """True for record fields whose children include filein or fileout.""" - return f.type.startswith("record") and _has_file_child(f) + return f["type"].startswith("record") and _has_file_child(f) def is_aux_list_field(f: Field) -> bool: @@ -209,22 +212,27 @@ def is_aux_list_field(f: Field) -> bool: These are generated as ``Optional[list[str]]`` with no dims and no structure_array converter, matching the hand-written pattern. """ - return f.type == "string" and f.block == "options" and bool(f.shape) and "naux" in f.shape + return ( + f["type"] == "string" + and f["block"] == "options" + and bool(f.get("shape", None)) + and "naux" in f.get("shape", None) + ) def is_period_array(f: Field) -> bool: """True for array fields in the period block.""" - return f.block == "period" and (is_array(f) or is_keyword_array(f)) + return f["block"] == "period" and (is_array(f) or is_keyword_array(f)) def is_dimensions_scalar(f: Field) -> bool: """True for scalar fields in the dimensions block (computed, init=False).""" - return f.block == "dimensions" and is_scalar(f) + return f["block"] == "dimensions" and is_scalar(f) def is_boundname_field(f: Field) -> bool: """True for the boundname string array in the period block.""" - return f.block == "period" and f.name == "boundname" + return f["block"] == "period" and f["name"] == "boundname" # Per-package OC record types. Each entry maps a DFN name to the list of @@ -245,16 +253,16 @@ def is_oc_record(f: Field, dfn_name: str) -> bool: being emitted as inner classes or TODO comments. """ return ( - f.block == "period" - and f.type.startswith("record") - and f.name in ("saverecord", "printrecord") + f["block"] == "period" + and f["type"].startswith("record") + and f["name"] in ("saverecord", "printrecord") and dfn_name in _OC_RTYPES ) def is_list_field(f: Field) -> bool: """True for list-type sub-table fields (packagedata, perioddata, etc.).""" - return f.type == "list" + return f["type"] == "list" def list_columns(f: Field) -> list[dict]: @@ -264,9 +272,9 @@ def list_columns(f: Field) -> list[dict]: objects. The list field has a single record child dict; the column entries are that record's ``children`` mapping. """ - if not f.children: + if not f.get("children", None): return [] - record_child = next(iter(f.children.values())) + record_child = next(iter(f["children"].values())) if not isinstance(record_child, dict): return [] return list(record_child.get("children", {}).values()) @@ -281,9 +289,9 @@ def list_col_dim(f: Field, dfn: Dfn) -> str | None: Falls back to the single entry in the DFN's dimensions block. Returns None when the dimension cannot be determined unambiguously. """ - dim_block = (dfn.blocks or {}).get("dimensions", {}) - if f.shape: - inner = f.shape.strip().strip("()") + dim_block = (dfn["blocks"] or {}).get("dimensions", {}) + if shape := f.get("shape", None): + inner = shape.strip().strip("()") parts = [p.strip() for p in inner.split(",") if p.strip()] if parts: shape_dim = _DIM_ALIASES.get(parts[-1], parts[-1]) @@ -343,9 +351,9 @@ def can_generate_record_class(f: Field) -> bool: empty inner class. Records with unsupported child types (recarray, union, complex shapes) fall back to TODO comments. """ - if is_file_record(f) or not f.children: + if is_file_record(f) or not f.get("children", None): return False - children = list(f.children.values()) + children = list(f["children"].values()) _supported = _RECORD_CLASS_SCALAR_TYPES | {"keyword"} all_supported = all(c.get("type") in _supported for c in children) if not all_supported: @@ -366,9 +374,9 @@ def can_expand_record(f: Field) -> bool: can't be generated standalone are noted in a TODO comment but don't block expansion. """ - if is_file_record(f) or not f.children: + if is_file_record(f) or not f.get("children", None): return False - for child in f.children.values(): + for child in f["children"].values(): if not child.get("optional", False) and not _is_expandable_child(child): return False return True @@ -383,10 +391,10 @@ def skip_reason(f: Field) -> str | None: if can_expand_record(f): return None # handled by _expand_record_field in make.py if _has_complex_shape(f): - return f"complex shape '{f.shape}' not yet supported" - if f.type in ("record", "recarray", "keystring"): - return f"complex type '{f.type}' not yet supported" - return f"type '{f.type}' not yet supported" + return f"complex shape '{f.get('shape', None)}' not yet supported" + if f["type"] in ("record", "recarray", "keystring"): + return f"complex type '{f['type']}' not yet supported" + return f"type '{f['type']}' not yet supported" # Field iteration @@ -404,18 +412,18 @@ def flat_fields(dfn: Dfn, *, developmode: bool = False) -> list[Field]: """ # Collect subfield names from file records so they can be suppressed. subfield_names: set[str] = set() - for block in (dfn.blocks or {}).values(): + for block in (dfn.get("blocks", {}) or {}).values(): for f in block.values(): if is_file_record(f): subfield_names.update(_file_record_subfield_names(f)) result = [] - for block in (dfn.blocks or {}).values(): + for block in (dfn.get("blocks", {}) or {}).values(): for f in block.values(): - f = apply_override(dfn.name, f) - if f.developmode and not developmode: + f = apply_override(dfn["name"], f) + if f.get("developmode", False) and not developmode: continue - if f.name in subfield_names: + if f["name"] in subfield_names: continue result.append(f) return result @@ -451,23 +459,23 @@ def py_type(f: Field) -> str: elif is_keyword_array(f): base = "NDArray[np.bool_]" elif is_array(f): - dtype = ARRAY_NUMPY_DTYPES.get(f.type, "np.object_") + dtype = ARRAY_NUMPY_DTYPES.get(f["type"], "np.object_") base = f"NDArray[{dtype}]" elif is_dimensions_scalar(f): # dimensions fields are computed (init=False) and always nullable - base = _SCALAR_PY_TYPES.get(f.type, "Any") + base = _SCALAR_PY_TYPES.get(f["type"], "Any") return f"Optional[{base}]" elif is_scalar(f): # Keywords are always bool (not Optional[bool]) regardless of optional flag. - if f.type == "keyword": + if f["type"] == "keyword": return "bool" - base = _SCALAR_PY_TYPES.get(f.type, "Any") + base = _SCALAR_PY_TYPES.get(f["type"], "Any") else: base = "Any" # Period-block arrays can be absent for a given stress period, so they're # implicitly nullable at the Python level even when the DFN marks them required. - is_nullable = f.optional or is_period_array(f) + is_nullable = f.get("optional", None) or is_period_array(f) return f"Optional[{base}]" if is_nullable else base @@ -519,15 +527,16 @@ def _longname_repr(longname: str | None) -> str | None: def _default_repr(f: Field) -> str: """Return the Python repr of a field's default value.""" - if f.default is None: + default = f.get("default", None) + if default is None: # Scalar keywords default to False (absent == not set). # Arrays (including keyword arrays) default to None. - if f.type == "keyword" and not f.shape: + if f["type"] == "keyword" and not f.get("shape", None): return "False" return "None" - if isinstance(f.default, str): - return repr(f.default) - return repr(f.default) + if isinstance(default, str): + return repr(default) + return repr(default) def _array_args(f: Field, *, has_maxbound: bool = False) -> list[str]: @@ -538,26 +547,26 @@ def _array_args(f: Field, *, has_maxbound: bool = False) -> list[str]: the boundname field; everything else (dims, netcdf, converter, on_setattr, longname) is identical. """ - shape = f.shape + shape = f.get("shape", None) # G/A variant period aux: DFN shape omits naux (one readarray block per aux # variable), but the array still needs a trailing naux dimension. - if f.name == "aux" and f.block == "period" and shape and "naux" not in shape: + if f["name"] == "aux" and f["block"] == "period" and shape and "naux" not in shape: shape = shape.rstrip(")").rstrip() + ", naux)" _keep = frozenset({"naux"}) if shape and "naux" in shape else None dims = _dims_tuple(shape, keep=_keep) if shape else '("nodes",)' args = [ - f'block="{f.block}"', + f'block="{f["block"]}"', f"dims={dims}", f"default={_default_repr(f)}", ] - if f.netcdf: + if f.get("netcdf", False): args.append("netcdf=True") args.append("converter=Converter(structure_array, takes_self=True, takes_field=True)") if is_period_array(f) and has_maxbound: args.append("on_setattr=update_maxbound") if is_boundname_field(f): args.insert(0, f"dtype={_BOUNDNAME_DTYPE}") - if ln := _longname_repr(f.longname): + if ln := _longname_repr(f.get("longname", None)): args.append(f"longname={ln}") return args @@ -577,15 +586,17 @@ def spec_call(f: Field, *, has_maxbound: bool = False) -> str: can reformat it to the project's style. """ if is_aux_list_field(f): - args = [f'block="{f.block}"', f"default={_default_repr(f)}"] - if ln := _longname_repr(f.longname): + block = f["block"] + args = [f'block="{block}"', f"default={_default_repr(f)}"] + if ln := _longname_repr(f["longname"]): args.append(f"longname={ln}") return f"array({', '.join(args)})" if is_file_record(f): + block = f["block"] inout = "filein" if _has_file_child_of(f, "filein") else "fileout" args = [ - f'block="{f.block}"', + f'block="{block}"', f"default={_default_repr(f)}", "converter=to_path", f'inout="{inout}"', @@ -597,32 +608,39 @@ def spec_call(f: Field, *, has_maxbound: bool = False) -> str: # scalar field if is_dimensions_scalar(f): - if has_maxbound and f.name == "maxbound": + block = f["block"] + if has_maxbound and f["name"] == "maxbound": # Only maxbound itself is auto-computed from data, so init=False. # Other dimension scalars in the same block (e.g. nseg in EVT) are # user-specified and must remain in __init__. - args = [f'block="{f.block}"', f"default={_default_repr(f)}", "init=False"] - if f.longname: - args.append(f"longname={repr(f.longname)}") + args = [f'block="{block}"', f"default={_default_repr(f)}", "init=False"] + if f.get("longname", None): + args.append(f"longname={repr(f['longname'])}") return f"field({', '.join(args)})" else: # User-specified dims (nseg, nrhospecies, maxmvr, maxpackages, …): # use dim(coord=False) so xattree includes the value in its # dimension resolution when expanding array fields. - args = [f'block="{f.block}"', "coord=False", f"default={_default_repr(f)}"] - if f.longname: - args.append(f"longname={repr(f.longname)}") + block = f["block"] + args = [f'block="{block}"', "coord=False", f"default={_default_repr(f)}"] + if f.get("longname", None): + args.append(f"longname={repr(f['longname'])}") return f"dim({', '.join(args)})" # Required scalar with no DFN default: omit default= entirely so the field is # positional-required at construction. Matches MF6 semantics (the user MUST # supply a value) and avoids the float/int annotation contradicting default=None. - if is_scalar(f) and not f.optional and f.default is None and f.type != "keyword": - args = [f'block="{f.block}"'] - if ln := _longname_repr(f.longname): + if ( + is_scalar(f) + and not f.get("optional", False) + and f.get("default", None) is None + and f["type"] != "keyword" + ): + args = [f'block="{f["block"]}"'] + if ln := _longname_repr(f.get("longname", None)): args.append(f"longname={ln}") return f"field({', '.join(args)})" - args = [f'block="{f.block}"', f"default={_default_repr(f)}"] - if ln := _longname_repr(f.longname): + args = [f'block="{f["block"]}"', f"default={_default_repr(f)}"] + if ln := _longname_repr(f.get("longname", None)): args.append(f"longname={ln}") return f"field({', '.join(args)})" @@ -658,7 +676,8 @@ def needed_imports( has_aux_list = any(is_aux_list_field(f) for f in generatable_fields) has_path = any(is_file_record(f) for f in generatable_fields) or has_injected_paths has_optional = any( - (f.optional or is_period_array(f)) and f.type != "keyword" for f in generatable_fields + (f.get("optional", False) or is_period_array(f)) and f["type"] != "keyword" + for f in generatable_fields ) has_dimensions = any(is_dimensions_scalar(f) for f in generatable_fields) has_stress_arrays = any(is_period_array(f) for f in generatable_fields) @@ -699,7 +718,7 @@ def needed_imports( } has_user_dims = ( - any(is_dimensions_scalar(f) and f.name != "maxbound" for f in generatable_fields) + any(is_dimensions_scalar(f) and f["name"] != "maxbound" for f in generatable_fields) or has_extra_dims ) @@ -764,28 +783,30 @@ class ColumnSpec: def block_schema(v1_dfn: Dfn, block_name: str) -> list[ColumnSpec]: """Derive column schema for a recarray block from a v1 DFN. - Skips the recarray header field (in_record=False). Returns one - ColumnSpec per in_record=True field in DFN order. + Finds the list-type header field in the block and reads columns from + its nested children (built by the v2.0.0.dev1 migration). Returns one + ColumnSpec per column in DFN order. """ - v1_block = (v1_dfn.blocks or {}).get(block_name) or {} + v1_block = (v1_dfn["blocks"] or {}).get(block_name) or {} + list_field = next((f for f in v1_block.values() if f.get("type") == "list"), None) + if list_field is None: + return [] result = [] - for name, f in v1_block.items(): - if not getattr(f, "in_record", False): - continue - ftype = getattr(f, "type", "") or "" - optional = _as_bool(getattr(f, "optional", False)) - tagged = _as_bool(getattr(f, "tagged", False)) - shape = str(getattr(f, "shape", "") or "") + for col in list_columns(list_field): + ftype = col.get("type", "") or "" + optional_val = col.get("optional") + optional = _as_bool(optional_val or False) + shape = str(col.get("shape", "") or "") is_keyword = ftype.lower() == "keyword" result.append( ColumnSpec( - name=name, + name=col.get("name", ""), type=ftype, - longname=getattr(f, "longname", "") or "", + longname=col.get("longname", "") or "", is_cellid="(ncelldim)" in shape, - is_prefix=is_keyword and tagged and not optional, + is_prefix=is_keyword and (optional_val is False), is_row_keyword=is_keyword and optional, - numeric_index=bool(getattr(f, "numeric_index", False)), + numeric_index=bool(col.get("numeric_index", False)), ) ) return result @@ -796,9 +817,9 @@ def list_block_names(dfn: Dfn) -> list[str]: seen: set[str] = set() result = [] for f in flat_fields(dfn): - if is_list_field(f) and f.block not in seen: - seen.add(f.block) - result.append(f.block) + if is_list_field(f) and f["block"] not in seen: + seen.add(f["block"]) + result.append(f["block"]) return result @@ -806,18 +827,18 @@ def v1_list_block_names(v1_dfn: Dfn) -> list[str]: """Return recarray block names from a v1 DFN, in order. A block is a recarray when it contains a header field whose ``type`` - starts with ``"recarray"``. This is more precise than checking - ``in_record=True``, which also matches sub-fields of compound records - (filerecord entries) inside scalar options blocks. + is ``"list"`` (v2.0.0.dev1) or starts with ``"recarray"`` (v1). Used to discover blocks that dfn2toml dropped from the v2 TOML. """ seen: set[str] = set() result = [] - for block_name, block in (v1_dfn.blocks or {}).items(): + for block_name, block in (v1_dfn["blocks"] or {}).items(): if block_name in seen: continue if any( - str(getattr(f, "type", "") or "").lower().startswith("recarray") for f in block.values() + (f.get("type", "") or "").lower() == "list" + or (f.get("type", "") or "").lower().startswith("recarray") + for f in block.values() ): result.append(block_name) seen.add(block_name) diff --git a/flopy4/mf6/utils/codegen/generate_classes.py b/flopy4/mf6/utils/codegen/generate_classes.py index a85bf08f..4838071c 100644 --- a/flopy4/mf6/utils/codegen/generate_classes.py +++ b/flopy4/mf6/utils/codegen/generate_classes.py @@ -15,7 +15,7 @@ from pathlib import Path from modflow_devtools.dfn import get_dfns -from modflow_devtools.dfns.dfn2toml import convert as dfn2toml +from modflow_devtools.dfn2toml import convert as dfn2toml from .make import make_all diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index e895ccc5..a779ac5b 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -14,8 +14,7 @@ from pathlib import Path import jinja2 -from modflow_devtools.dfns import Dfn, load_flat -from modflow_devtools.dfns.schema.field import Field as DfnField +from modflow_devtools.dfn import Dfn, Field from . import filters from .filters import ColumnSpec @@ -109,18 +108,18 @@ class ComponentSpec: # Context builders -def _build_field_spec(f: DfnField, *, has_maxbound: bool = False) -> FieldSpec: +def _build_field_spec(f: Field, *, has_maxbound: bool = False) -> FieldSpec: generatable = filters.is_generatable(f) # Strip 'record' suffix from file record names for a cleaner API # (e.g. head_filerecord → head_file, budget_filerecord → budget_file). # Compound records get the same treatment via _strip_record_words in # build_component_spec; this keeps the two paths consistent. if filters.is_file_record(f): - py_name = filters.safe_name("_".join(_strip_record_words(f.name))) + py_name = filters.safe_name("_".join(_strip_record_words(f["name"]))) else: - py_name = filters.safe_name(f.name) + py_name = filters.safe_name(f["name"]) return FieldSpec( - dfn_name=f.name, + dfn_name=f["name"], py_name=py_name, type_annotation=filters.py_type(f) if generatable else "Any", spec_call=filters.spec_call(f, has_maxbound=has_maxbound) if generatable else "", @@ -148,14 +147,14 @@ def _build_field_spec(f: DfnField, *, has_maxbound: bool = False) -> FieldSpec: ) -def _child_to_field(child_dict: dict) -> DfnField: +def _child_to_field(child_dict: dict) -> Field: """Convert a record child dict to a Field object.""" - return DfnField(**{k: v for k, v in child_dict.items() if k in _FIELD_KNOWN_KEYS}) + return Field(**{k: v for k, v in child_dict.items() if k in _FIELD_KNOWN_KEYS}) def _expand_record_field( - f: DfnField, *, has_maxbound: bool = False -) -> tuple[list[FieldSpec], list[DfnField]]: + f: Field, *, has_maxbound: bool = False +) -> tuple[list[FieldSpec], list[Field]]: """Expand a compound record into FieldSpecs for its generatable children. Returns (field_specs, generatable_child_fields). field_specs contains one @@ -163,8 +162,8 @@ def _expand_record_field( children that can't be generated standalone. generatable_child_fields is the corresponding list of Field objects used for import computation. """ - children = f.children or {} - expandable: list[DfnField] = [] + children = f.get("children", None) or {} + expandable: list[Field] = [] unexpandable_optional: list[str] = [] for child_dict in children.values(): @@ -175,7 +174,7 @@ def _expand_record_field( # required unexpandable children were already blocked by can_expand_record specs: list[FieldSpec] = [] - gen_fields: list[DfnField] = [] + gen_fields: list[Field] = [] for child_field in expandable: spec = _build_field_spec(child_field, has_maxbound=has_maxbound) specs.append(spec) @@ -185,8 +184,8 @@ def _expand_record_field( if unexpandable_optional: specs.append( FieldSpec( - dfn_name=f.name, - py_name=filters.safe_name(f.name), + dfn_name=f["name"], + py_name=filters.safe_name(f["name"]), type_annotation="Any", spec_call="", generatable=False, @@ -199,7 +198,7 @@ def _expand_record_field( return specs, gen_fields -def _expand_list_field(f: DfnField, dfn: Dfn) -> list[FieldSpec]: +def _expand_list_field(f: Field, dfn: Dfn) -> list[FieldSpec]: """Expand a list-type sub-table field into one FieldSpec per column. Each column becomes an array() field with the list block's block name @@ -211,8 +210,8 @@ def _expand_list_field(f: DfnField, dfn: Dfn) -> list[FieldSpec]: if not cols or dim is None: return [ FieldSpec( - dfn_name=f.name, - py_name=filters.safe_name(f.name), + dfn_name=f["name"], + py_name=filters.safe_name(f["name"]), type_annotation="Any", spec_call="", generatable=False, @@ -222,15 +221,16 @@ def _expand_list_field(f: DfnField, dfn: Dfn) -> list[FieldSpec]: specs = [] for col in cols: - col = apply_to_child(dfn.name, col) + col = apply_to_child(dfn["name"], col) col_name = col["name"] col_type = col.get("type", "string") col_longname = col.get("longname", "") dtype = filters.ARRAY_NUMPY_DTYPES.get(col_type, "np.object_") base = f"NDArray[{dtype}]" annotation = f"Optional[{base}]" # expanded columns always default to None + block = f["block"] args = [ - f'block="{f.block}"', + f'block="{block}"', f'dims=("{dim}",)', "default=None", "converter=Converter(structure_array, takes_self=True, takes_field=True)", @@ -249,7 +249,7 @@ def _expand_list_field(f: DfnField, dfn: Dfn) -> list[FieldSpec]: return specs -def _expand_oc_record_field(f: DfnField, dfn_name: str) -> list[FieldSpec]: +def _expand_oc_record_field(f: Field, dfn_name: str) -> list[FieldSpec]: """Expand saverecord/printrecord into per-rtype NDArray[np.str_] fields. Generates one array field per rtype (e.g. save_concentration, save_budget) @@ -257,7 +257,7 @@ def _expand_oc_record_field(f: DfnField, dfn_name: str) -> list[FieldSpec]: by splitting on ``_`` → replacing with space. """ rtypes = filters._OC_RTYPES.get(dfn_name, []) - action = "save" if f.name == "saverecord" else "print" + action = "save" if f["name"] == "saverecord" else "print" specs: list[FieldSpec] = [] for rtype in rtypes: py_name = f"{action}_{rtype}" @@ -271,7 +271,7 @@ def _expand_oc_record_field(f: DfnField, dfn_name: str) -> list[FieldSpec]: ) specs.append( FieldSpec( - dfn_name=f"{f.name}_{rtype}", + dfn_name=f"{f['name']}_{rtype}", py_name=py_name, type_annotation="Optional[NDArray[np.str_]]", spec_call=spec_call, @@ -308,7 +308,7 @@ def _strip_record_words(name: str) -> list[str]: } -def _build_inner_class_spec(f: DfnField, dfn_name: str) -> InnerClassSpec: +def _build_inner_class_spec(f: Field, dfn_name: str) -> InnerClassSpec: """Build an InnerClassSpec for a mixed-type compound record field. When the first child is a keyword type it becomes the trigger token @@ -324,7 +324,7 @@ def _build_inner_class_spec(f: DfnField, dfn_name: str) -> InnerClassSpec: sub-records lost in v2 TOML conversion) are appended after the direct children. All fields are sorted required-first to satisfy attrs. """ - children = list((f.children or {}).values()) + children = list((f.get("children", None) or {}).values()) first = children[0] if first.get("type") == "keyword": keyword = first["name"] @@ -372,13 +372,13 @@ def _process_child(child_dict: dict) -> None: for child_dict in data_children: _process_child(child_dict) - for child_dict in extra_record_children(dfn_name, f.name): + for child_dict in extra_record_children(dfn_name, f["name"]): _process_child(child_dict) # attrs requires fields with defaults to follow fields without defaults. - inner_fields.sort(key=lambda field: field.optional) + inner_fields.sort(key=lambda field: str(field.optional)) - words = _strip_record_words(f.name) + words = _strip_record_words(f["name"]) class_name = "".join(w.capitalize() for w in words) extra_tokens_repr = ( "(" + ", ".join(f'"{t}"' for t in extra_tokens) + ",)" if extra_tokens else "" @@ -405,17 +405,17 @@ def _build_block_property_specs( block property API for each recarray block. Returns (specs, block_names) where block_names is used as a skip-set in the main field loop. """ - dfn_dims = set((dfn.blocks or {}).get("dimensions", {}).keys()) - dfn_dims_ordered = list((dfn.blocks or {}).get("dimensions", {}).keys()) + dfn_dims = set((dfn["blocks"] or {}).get("dimensions", {}).keys()) + dfn_dims_ordered = list((dfn["blocks"] or {}).get("dimensions", {}).keys()) # Collect v2 list blocks, excluding those handled by TOML overrides. - list_fields_map: dict[str, DfnField | None] = { - f.block: f + list_fields_map: dict[str, Field | None] = { + f["block"]: f for f in filters.flat_fields(dfn) if filters.is_list_field(f) - and f.block not in extra_blocks - and f.block not in replace_blocks - and "period" not in f.block + and f["block"] not in extra_blocks + and f["block"] not in replace_blocks + and "period" not in f["block"] } # Add recarray blocks present in v1 DFN but dropped by dfn2toml (e.g. SSM sources/fileinput). for v1_block in filters.v1_list_block_names(v1_dfn): @@ -426,7 +426,7 @@ def _build_block_property_specs( v1_schemas = {block: filters.block_schema(v1_dfn, block) for block in list_fields_map} # Period field bare names: static columns sharing a name with a period field # must take the block-prefixed attr name so the period field keeps the bare name. - bare_period_names = frozenset(pf["keyword"].lower() for pf in extra_period_fields(dfn.name)) + bare_period_names = frozenset(pf["keyword"].lower() for pf in extra_period_fields(dfn["name"])) collisions = filters.collision_names(v1_schemas, reserved=bare_period_names) # Resolve which DFN dimension scalar each block maps to. @@ -442,10 +442,10 @@ def _build_block_property_specs( if dfn_dim and dfn_dim in dfn_dims: dim_resolutions[block_name] = (dfn_dim, True) claimed_dims.add(dfn_dim) - elif lf.shape and "maxbound" in str(lf.shape) and dfn_dims: + elif lf.get("shape", None) and "maxbound" in str(lf.get("shape", None)) and dfn_dims: maxbound_blocks.append(block_name) else: - override = block_dim_override(dfn.name, block_name) + override = block_dim_override(dfn["name"], block_name) dim_resolutions[block_name] = (override or f"n{block_name}", False) unclaimed = [d for d in dfn_dims_ordered if d not in claimed_dims] @@ -486,15 +486,15 @@ def _build_block_property_specs( def _base_class(dfn: Dfn) -> str: """Determine the Python base class for a component.""" - if dfn.name.split("-")[0] == _SLN_PREFIX: + if dfn["name"].split("-")[0] == _SLN_PREFIX: return "Solution" return "Package" def _slntype(dfn: Dfn) -> str | None: """Return the slntype string for solution DFNs, or None.""" - if dfn.name.split("-")[0] == _SLN_PREFIX: - return dfn.name.split("-")[1] + if dfn["name"].split("-")[0] == _SLN_PREFIX: + return dfn["name"].split("-")[1] return None @@ -529,9 +529,9 @@ def build_component_spec( period_specs: list[FieldSpec] = [] inner_class_specs: list[InnerClassSpec] = [] - generatable_field_objects: list[DfnField] = [] - _replace_blocks = replace_list_blocks(dfn.name) - _extra_blocks = {lb["block"] for lb in extra_list_blocks(dfn.name)} + generatable_field_objects: list[Field] = [] + _replace_blocks = replace_list_blocks(dfn["name"]) + _extra_blocks = {lb["block"] for lb in extra_list_blocks(dfn["name"])} # BlockPropertySpec for static list blocks — must precede the main field loop # since _bp_block_names is used there as a skip-set. @@ -548,16 +548,16 @@ def build_component_spec( has_list_cols = False has_oc_fields = False for f in all_fields: - if filters.is_list_field(f) and f.block in (_replace_blocks | _extra_blocks): + if filters.is_list_field(f) and f["block"] in (_replace_blocks | _extra_blocks): # List field replaced by explicit path fields or injected via extra_list_blocks. continue - if filters.is_list_field(f) and f.block in _bp_block_names: + if filters.is_list_field(f) and f["block"] in _bp_block_names: # List field covered by BlockPropertySpec; column attrs generated below. continue - if f.block in ("options", "dimensions"): + if f["block"] in ("options", "dimensions"): target = prefix_specs - elif "period" in f.block: + elif "period" in f["block"]: target = period_specs else: target = data_specs @@ -566,20 +566,21 @@ def build_component_spec( expanded = _expand_list_field(f, dfn) target.extend(expanded) has_list_cols = has_list_cols or any(fs.generatable for fs in expanded) - elif filters.is_oc_record(f, dfn.name): - expanded = _expand_oc_record_field(f, dfn.name) + elif filters.is_oc_record(f, dfn["name"]): + expanded = _expand_oc_record_field(f, dfn["name"]) target.extend(expanded) has_oc_fields = has_oc_fields or any(fs.generatable for fs in expanded) elif filters.can_generate_record_class(f): - record_spec = _build_inner_class_spec(f, dfn.name) + record_spec = _build_inner_class_spec(f, dfn["name"]) inner_class_specs.append(record_spec) - clean_name = filters.safe_name("_".join(_strip_record_words(f.name))) + clean_name = filters.safe_name("_".join(_strip_record_words(f["name"]))) + block = f["block"] target.append( FieldSpec( - dfn_name=f.name, + dfn_name=f["name"], py_name=clean_name, type_annotation=f"Optional[{record_spec.class_name}]", - spec_call=f'field(block="{f.block}", default=None)', + spec_call=f'field(block="{block}", default=None)', generatable=True, ) ) @@ -597,7 +598,7 @@ def build_component_spec( # Inject path fields that replace heterogeneous list blocks (e.g. prt-fmi packagedata). # Each injected entry becomes an Optional[Path] field using the path() spec. has_injected_paths = False - for entry in replace_list_fields(dfn.name): + for entry in replace_list_fields(dfn["name"]): has_injected_paths = True ln = entry.get("longname", "") args = [ @@ -625,8 +626,8 @@ def build_component_spec( # Entries must be listed in dfn_overrides.toml in v1 DFN block order so that # stable sort on key 3 in block_sort_key produces the correct write sequence. has_extra_dims = False - _dfn_dim_names = set((dfn.blocks or {}).get("dimensions", {}).keys()) - for lb in extra_list_blocks(dfn.name): + _dfn_dim_names = set((dfn.get("blocks", {}) or {}).get("dimensions", {}).keys()) + for lb in extra_list_blocks(dfn["name"]): block_name = lb["block"] dim_name = lb["dim"] # Only add the dim field when it is not already declared in the DFN's @@ -642,7 +643,7 @@ def build_component_spec( generatable=True, ) ) - v1_block = (v1_dfn.blocks or {}).get(block_name, {}) if v1_dfn else {} + v1_block = (v1_dfn.get("blocks", {}) or {}).get(block_name, {}) if v1_dfn else {} for col in lb.get("columns", []): col_name = col["name"] col_py_name = filters.safe_name(col.get("py_name", col_name)) @@ -688,7 +689,7 @@ def build_component_spec( # Period fields always use the bare keyword name. Static block columns that # share a name with a period field take the block-prefixed attr name instead # (see _bare_period_names + collision_names(reserved=...)). - for pf in extra_period_fields(dfn.name): + for pf in extra_period_fields(dfn["name"]): kw = pf["keyword"] feat_dim = pf["feature_dim"] py_name = filters.safe_name(kw.lower()) @@ -813,10 +814,10 @@ def build_component_spec( if not _naux_emitted: for _f in all_fields: if ( - _f.block == "period" + _f["block"] == "period" and filters.is_array(_f) - and _f.shape - and ("naux" in _f.shape or _f.name == "aux") + and _f.get("shape", None) + and ("naux" in _f.get("shape", None) or _f["name"] == "aux") ): _naux_emitted = True has_extra_dims = True @@ -841,19 +842,21 @@ def build_component_spec( for _f in all_fields: # Only node-based floating/integer list columns: excludes keyword period arrays # (STO steady-state/transient flags) and non-list period blocks. - if not (_f.block == "period" and filters.is_array(_f)): + if not (_f["block"] == "period" and filters.is_array(_f)): continue - if _f.name in ("boundname", "aux"): + if _f["name"] in ("boundname", "aux"): continue - if _f.shape and "naux" in _f.shape: + if _f.get("shape", None) and "naux" in _f.get("shape", None): continue # Require node dimension (nnodes/nodes in shape) — excludes OC and period-level scalars - if not _f.shape or ("nnodes" not in _f.shape and "nodes" not in _f.shape): + if not _f.get("shape", None) or ( + "nnodes" not in _f.get("shape", None) and "nodes" not in _f.get("shape", None) + ): continue - period_col_map[_f.name] = filters.safe_name(_f.name) + period_col_map[_f["name"]] = filters.safe_name(_f["name"]) base = _base_class(dfn) - multi = bool(dfn.multi) + multi = bool(dfn.get("multi", False)) slntype = _slntype(dfn) has_inner_classes = bool(inner_class_specs) @@ -874,15 +877,15 @@ def build_component_spec( ) return ComponentSpec( - dfn_name=dfn.name, - class_name=filters.class_name(dfn.name), + dfn_name=dfn["name"], + class_name=filters.class_name(dfn["name"]), base_class=base, multi=multi, slntype=slntype, imports=imports, fields=field_specs, inner_classes=inner_class_specs, - outpath=filters.output_path(dfn.name, root), + outpath=filters.output_path(dfn["name"], root), block_properties=block_properties, has_aux=_naux_emitted, period_col_map=period_col_map, @@ -982,8 +985,8 @@ def make_all( outdir = Path(outdir) skip = skip or set() env = _get_env() - dfns = load_flat(dfndir) - v1_dfns = load_flat(v1dfndir) if v1dfndir else {} + dfns = Dfn.load_all(dfndir, schema_version="2.0.0.dev1") + v1_dfns = Dfn.load_all(v1dfndir, schema_version="2.0.0.dev1") if v1dfndir else {} specs = [] for name, dfn in dfns.items(): if name in skip: diff --git a/flopy4/mf6/utils/codegen/overrides.py b/flopy4/mf6/utils/codegen/overrides.py index 64a627c8..01d232fd 100644 --- a/flopy4/mf6/utils/codegen/overrides.py +++ b/flopy4/mf6/utils/codegen/overrides.py @@ -18,11 +18,10 @@ patched_field = apply("gwf-ic", field) """ -import dataclasses import tomllib from pathlib import Path -from modflow_devtools.dfns.schema.field import Field +from modflow_devtools.dfn import Field _OVERRIDES_PATH = Path(__file__).parent / "dfn_overrides.toml" @@ -53,12 +52,12 @@ def apply(dfn_name: str, f: Field) -> Field: The original field if no overrides exist, otherwise a new dataclass instance with the patched attributes. """ - patches = _OVERRIDES.get(dfn_name, {}).get(f.name, {}) + patches = _OVERRIDES.get(dfn_name, {}).get(f["name"], {}) # extra_children is consumed by extra_record_children(), not a Field attribute patches = {k: v for k, v in patches.items() if k != "extra_children"} if not patches: return f - return dataclasses.replace(f, **patches) + return {**f, **patches} def extra_list_blocks(dfn_name: str) -> list[dict]: diff --git a/pixi.lock b/pixi.lock index 71c6c108..874744dc 100644 --- a/pixi.lock +++ b/pixi.lock @@ -88,7 +88,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -113,6 +113,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -228,7 +229,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -254,6 +255,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl @@ -376,7 +378,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz @@ -399,6 +401,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl @@ -520,7 +523,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl @@ -548,6 +551,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl @@ -683,7 +687,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -747,6 +751,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -952,7 +957,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1018,6 +1023,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -1227,7 +1233,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -1296,6 +1302,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -1498,7 +1505,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -1560,6 +1567,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl @@ -1788,7 +1796,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl @@ -1856,6 +1864,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl @@ -2046,7 +2055,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -2116,6 +2125,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -2313,7 +2323,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl @@ -2384,6 +2394,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -2575,7 +2586,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/bf/6f506a37c7f8ecc4576caf9486e303c7af249f6d70447bb51dde9d78cb99/sphinx_book_theme-1.2.0-py3-none-any.whl @@ -2640,6 +2651,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/c0/d28e62407f4733bbe0169287bc012f0ac3b4a2021066b285570654119c8b/sphinxcontrib_bibtex-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -2856,7 +2868,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2912,6 +2924,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -3092,7 +3105,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -3150,6 +3163,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -3337,7 +3351,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -3397,6 +3411,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -3578,7 +3593,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -3632,6 +3647,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl @@ -3837,7 +3853,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -3889,6 +3905,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -4070,7 +4087,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -4126,6 +4143,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -4312,7 +4330,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -4366,6 +4384,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/57/bc/76f8f8c5cf9adee47fdb7bbb03be8900f76f902d451d7477cf12b845e1de/numba-0.65.1-cp312-cp312-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -4550,7 +4569,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -4604,6 +4623,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -4805,7 +4825,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -4856,6 +4876,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -5040,7 +5061,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -5093,6 +5114,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/52/07/1922a0c20ed4c95769937619d2c0217ee3f665a8b967438277d3f40860e4/xugrid-0.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -5284,7 +5306,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -5333,6 +5355,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -5524,7 +5547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_37.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: . - - pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d + - pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -5578,6 +5601,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl @@ -9624,7 +9648,7 @@ packages: - build ; extra == 'build' - twine ; extra == 'build' requires_python: '>=3.11,<3.14' -- pypi: git+https://github.com/modflow-org/modflow-devtools#06e14c20c250bada77b3facb57fa3a286c4c211d +- pypi: git+https://github.com/modflow-org/modflow-devtools#5fde75e2b737b698795c30d993340f956350dbda name: modflow-devtools version: 1.10.0.dev1 requires_dist: @@ -9642,6 +9666,7 @@ packages: - numpy ; extra == 'dev' - pandas ; extra == 'dev' - pooch ; extra == 'dev' + - pyaml ; extra == 'dev' - pydantic ; extra == 'dev' - pytest!=8.1.0 ; extra == 'dev' - pytest-cov ; extra == 'dev' @@ -9656,6 +9681,7 @@ packages: - tomli-w ; extra == 'dev' - boltons ; extra == 'dfn' - pooch ; extra == 'dfn' + - pyaml ; extra == 'dfn' - pydantic ; extra == 'dfn' - tomli ; extra == 'dfn' - tomli-w ; extra == 'dfn' @@ -9665,6 +9691,7 @@ packages: - boltons ; extra == 'ecosystem' - filelock ; extra == 'ecosystem' - pooch ; extra == 'ecosystem' + - pyaml ; extra == 'ecosystem' - pydantic ; extra == 'ecosystem' - tomli ; extra == 'ecosystem' - tomli-w ; extra == 'ecosystem' @@ -9674,6 +9701,7 @@ packages: - boltons ; extra == 'models' - filelock ; extra == 'models' - pooch ; extra == 'models' + - pyaml ; extra == 'models' - pydantic ; extra == 'models' - tomli ; extra == 'models' - tomli-w ; extra == 'models' @@ -9693,7 +9721,7 @@ packages: - pyyaml ; extra == 'test' - ruff ; extra == 'test' - syrupy<5.0.0 ; extra == 'test' - requires_python: '>=3.10' + requires_python: '>=3.11' - pypi: git+https://github.com/wpbonelli/xattree#82942ffb8237d56c97451765ac3e2b16d6cb0d2c name: xattree version: 0.1.0.dev0 @@ -12599,6 +12627,14 @@ packages: version: 1.8.0 sha256: 014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl + name: pyaml + version: 26.2.1 + sha256: 6261c2f0a2f33245286c794ad6ec234be33a73d2b05427079fd343e2812a87cf + requires_dist: + - pyyaml + - unidecode ; extra == 'anchors' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl name: soupsieve version: 2.8.4 diff --git a/test/test_mf6_codegen.py b/test/test_mf6_codegen.py index cd700d90..c9bb7a32 100644 --- a/test/test_mf6_codegen.py +++ b/test/test_mf6_codegen.py @@ -18,9 +18,7 @@ from pathlib import Path import pytest -from modflow_devtools.dfns import load_flat -from modflow_devtools.dfns.dfn2toml import convert as dfn2toml -from modflow_devtools.dfns.schema.v2 import FieldV2 +from modflow_devtools.dfn import Dfn, Field from flopy4.mf6.component import COMPONENTS from flopy4.mf6.utils.codegen.filters import ( @@ -39,17 +37,9 @@ # Shared fixtures @pytest.fixture(scope="session") -def v2_dfn_dir(dfn_path, tmp_path_factory): - """Convert the session-level v1 DFN directory to v2 TOML once.""" - v2dir = tmp_path_factory.mktemp("dfn_v2") - dfn2toml(dfn_path, v2dir) - return v2dir - - -@pytest.fixture(scope="session") -def all_dfns(v2_dfn_dir): - """Load all v2 DFNs as a flat dict.""" - return load_flat(v2_dfn_dir) +def all_dfns(dfn_path): + """Load all DFNs as a flat dict.""" + return Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") # Simple tier: Package subclasses with only scalars, arrays, and path records. @@ -209,7 +199,7 @@ def test_safe_name(self, name, expected): ], ) def test_py_type(self, ftype, shape, optional, expected): - f = FieldV2(name="x", type=ftype, block="options", shape=shape, optional=optional) + f = Field(name="x", type=ftype, block="options", shape=shape, optional=optional) assert py_type(f) == expected @pytest.mark.parametrize( @@ -222,7 +212,7 @@ def test_py_type(self, ftype, shape, optional, expected): ) def test_py_type_period_array_always_optional(self, ftype, shape): """Period arrays get Optional even when DFN marks them required.""" - f = FieldV2(name="x", type=ftype, block="period", shape=shape, optional=False) + f = Field(name="x", type=ftype, block="period", shape=shape, optional=False) result = py_type(f) assert result.startswith("Optional["), ( f"Expected Optional for period {ftype} array but got {result!r}" @@ -241,13 +231,13 @@ def test_py_type_period_array_always_optional(self, ftype, shape): ], ) def test_is_generatable(self, ftype, shape, generatable): - f = FieldV2(name="x", type=ftype, block="options", shape=shape) + f = Field(name="x", type=ftype, block="options", shape=shape) assert is_generatable(f) == generatable def test_is_generatable_file_record(self): """A record with a filein/fileout child is generatable as a path.""" - child_in = FieldV2(name="filein", type="keyword", block="options") - f = FieldV2( + child_in = Field(name="filein", type="keyword", block="options") + f = Field( name="my_filerecord", type="record", block="options", @@ -264,12 +254,12 @@ def test_is_generatable_file_record(self): ], ) def test_spec_call_prefix(self, ftype, shape, check): - f = FieldV2(name="x", type=ftype, block="options", shape=shape) + f = Field(name="x", type=ftype, block="options", shape=shape) assert check(spec_call(f)) def test_spec_call_file_record(self): - child = FieldV2(name="fileout", type="keyword", block="options") - f = FieldV2( + child = Field(name="fileout", type="keyword", block="options") + f = Field( name="budget_filerecord", type="record", block="options", @@ -380,7 +370,7 @@ def test_lak_numeric_index_autodetects_cellid(all_dfns, dfn_path): """v1 DFN numeric_index=True on ifno/iconn auto-sets cellid; is_cellid stored as object.""" if "gwf-lak" not in all_dfns: pytest.skip("gwf-lak not in DFN set") - v1_dfns = load_flat(dfn_path) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") spec = build_component_spec( all_dfns["gwf-lak"], root=Path("/fake"), v1_dfn=v1_dfns.get("gwf-lak") ) @@ -439,7 +429,7 @@ class TestBlockPropertySpec: def lak_spec(self, all_dfns, dfn_path): if "gwf-lak" not in all_dfns: pytest.skip("gwf-lak not in DFN set") - v1_dfns = load_flat(dfn_path) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") return build_component_spec( all_dfns["gwf-lak"], root=Path("/fake"), v1_dfn=v1_dfns.get("gwf-lak") ) @@ -519,7 +509,7 @@ def test_can_expand_record_all_keywords(all_dfns): """A record whose children are all keyword type (like cvoptions) is expandable.""" if "gwf-npf" not in all_dfns: pytest.skip("gwf-npf not in DFN set") - cvoptions = all_dfns["gwf-npf"].blocks["options"]["cvoptions"] + cvoptions = all_dfns["gwf-npf"]["blocks"]["options"]["cvoptions"] assert can_expand_record(cvoptions) @@ -529,7 +519,7 @@ def test_can_expand_record_with_positional_required_data(all_dfns): pytest.skip("gwf-npf not in DFN set") from flopy4.mf6.utils.codegen.filters import can_generate_record_class - rewet_record = all_dfns["gwf-npf"].blocks["options"]["rewet_record"] + rewet_record = all_dfns["gwf-npf"]["blocks"]["options"]["rewet_record"] assert not can_expand_record(rewet_record) assert can_generate_record_class(rewet_record) @@ -540,7 +530,7 @@ def test_rcloserecord_generates_inner_class(all_dfns): pytest.skip("sln-ims not in DFN set") from flopy4.mf6.utils.codegen.filters import can_generate_record_class - rcloserecord = all_dfns["sln-ims"].blocks["linear"]["rcloserecord"] + rcloserecord = all_dfns["sln-ims"]["blocks"]["linear"]["rcloserecord"] assert can_generate_record_class(rcloserecord) @@ -605,12 +595,12 @@ def test_npf_compound_records_expanded(all_dfns): # Layer 3: End-to-end generation -def test_simple_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): +def test_simple_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): """Run make_all() and verify each simple-tier file is importable with the right class.""" (tmp_path / "gwf").mkdir() - skip = {n for n in load_flat(v2_dfn_dir) if n not in SIMPLE_TIER} - specs = make_all(dfndir=v2_dfn_dir, outdir=tmp_path, fmt=False, skip=skip) + skip = {n for n in all_dfns if n not in SIMPLE_TIER} + specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} @@ -643,12 +633,12 @@ def test_simple_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): assert hasattr(mod, expected_class), f"Class {expected_class} not found in {spec.outpath}" -def test_solution_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): +def test_solution_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): """Run make_all() and verify each solution-tier file is importable with Solution base.""" from flopy4.mf6.solution import Solution - skip = {n for n in load_flat(v2_dfn_dir) if n not in SOLUTION_TIER} - specs = make_all(dfndir=v2_dfn_dir, outdir=tmp_path, fmt=False, skip=skip) + skip = {n for n in all_dfns if n not in SOLUTION_TIER} + specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} @@ -700,14 +690,14 @@ def _load_class_from_spec(spec, mod_name: str, expected_class: str): del sys.modules[key] -def test_transport_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): +def test_transport_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): """gwt-disv, gwt-ist, gwe-disv generate importable Package subclasses.""" for subdir in ("gwt", "gwe"): (tmp_path / subdir).mkdir() target = {n for n in TRANSPORT_TIER if n in all_dfns} - skip = {n for n in load_flat(v2_dfn_dir) if n not in target} - specs = make_all(dfndir=v2_dfn_dir, outdir=tmp_path, fmt=False, skip=skip) + skip = {n for n in all_dfns if n not in target} + specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package @@ -722,14 +712,14 @@ def test_transport_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfn assert issubclass(cls, Package) -def test_oc_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): +def test_oc_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): """gwt-oc, gwe-oc, prt-oc generate importable Package subclasses with OC period fields.""" for subdir in ("gwt", "gwe", "prt"): (tmp_path / subdir).mkdir() target = {n for n in OC_TIER if n in all_dfns} - skip = {n for n in load_flat(v2_dfn_dir) if n not in target} - specs = make_all(dfndir=v2_dfn_dir, outdir=tmp_path, fmt=False, skip=skip) + skip = {n for n in all_dfns if n not in target} + specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package @@ -747,13 +737,13 @@ def test_oc_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): assert oc_fields, f"{dfn_name} should have save_/print_ period fields" -def test_utl_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): +def test_utl_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): """utl-* packages generate importable Package subclasses (including utl-tas inner classes).""" (tmp_path / "utl").mkdir() target = {n for n in UTL_TIER if n in all_dfns} - skip = {n for n in load_flat(v2_dfn_dir) if n not in target} - specs = make_all(dfndir=v2_dfn_dir, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) + skip = {n for n in all_dfns if n not in target} + specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package @@ -768,13 +758,13 @@ def test_utl_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): assert issubclass(cls, Package) -def test_exg_tier_generates_importable_files(v2_dfn_dir, tmp_path, all_dfns): +def test_exg_tier_generates_importable_files(dfn_path, tmp_path, all_dfns): """exg-* packages generate importable Package subclasses (including 0-field pass-only).""" (tmp_path / "exg").mkdir() target = {n for n in EXG_TIER if n in all_dfns} - skip = {n for n in load_flat(v2_dfn_dir) if n not in target} - specs = make_all(dfndir=v2_dfn_dir, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) + skip = {n for n in all_dfns if n not in target} + specs = make_all(dfndir=dfn_path, outdir=tmp_path, fmt=False, makedirs=True, skip=skip) generated = {s.dfn_name: s for s in specs} from flopy4.mf6.package import Package diff --git a/test/test_mf6_grammar_gen.py b/test/test_mf6_grammar_gen.py index de6150b4..b4afe0b2 100644 --- a/test/test_mf6_grammar_gen.py +++ b/test/test_mf6_grammar_gen.py @@ -1,6 +1,5 @@ import pytest -from modflow_devtools.dfns import Dfn -from modflow_devtools.dfns.schema.v2 import FieldV2 +from modflow_devtools.dfn import Dfn, Field from packaging.version import Version from flopy4.mf6.codec.reader.grammar import make_grammar, make_grammars @@ -14,7 +13,7 @@ def minimal_dfn(): name="test-component", blocks={ "options": { - "test_field": FieldV2( + "test_field": Field( name="test_field", type="keyword", block="options", @@ -32,14 +31,14 @@ def simple_dfn(): name="gwf-test", blocks={ "options": { - "export_ascii": FieldV2( + "export_ascii": Field( name="export_array_ascii", type="keyword", block="options", ) }, "griddata": { - "strt": FieldV2( + "strt": Field( name="strt", type="double", block="griddata", @@ -147,20 +146,20 @@ def test_make_grammar_with_period_block(tmp_path): name="gwf-test", blocks={ "options": { - "print_input": FieldV2( + "print_input": Field( name="print_input", type="keyword", block="options", ) }, "period": { - "q": FieldV2( + "q": Field( name="q", type="double", block="period", shape="(nper, nnodes)", ), - "aux": FieldV2( + "aux": Field( name="aux", type="double", block="period", @@ -196,7 +195,7 @@ def test_make_grammar_with_named_subfields(tmp_path): name="gwf-rch", blocks={ "period": { - "recharge": FieldV2( + "recharge": Field( name="recharge", type="double", block="period", @@ -226,21 +225,21 @@ def test_make_grammar_with_oc_style_records(tmp_path): name="gwf-oc", blocks={ "period": { - "saverecord": FieldV2( + "saverecord": Field( name="saverecord", type="record", block="period", children={ - "save": FieldV2(name="save", type="keyword", block="period"), - "rtype": FieldV2(name="rtype", type="string", block="period"), - "ocsetting": FieldV2( + "save": Field(name="save", type="keyword", block="period"), + "rtype": Field(name="rtype", type="string", block="period"), + "ocsetting": Field( name="ocsetting", type="union", block="period", children={ - "all": FieldV2(name="all", type="keyword", block="period"), - "first": FieldV2(name="first", type="keyword", block="period"), - "last": FieldV2(name="last", type="keyword", block="period"), + "all": Field(name="all", type="keyword", block="period"), + "first": Field(name="first", type="keyword", block="period"), + "last": Field(name="last", type="keyword", block="period"), }, ), }, diff --git a/test/test_mf6_reader.py b/test/test_mf6_reader.py index 3e0997ec..5b25fcc9 100644 --- a/test/test_mf6_reader.py +++ b/test/test_mf6_reader.py @@ -7,7 +7,7 @@ import pytest import xarray as xr from lark import Lark -from modflow_devtools.dfns import Dfn, MapV1To2, load_flat +from modflow_devtools.dfn import Dfn from modflow_devtools.download import download_and_unzip from packaging.version import Version @@ -239,8 +239,8 @@ def test_transform_layered_array(): def test_transform_full_component(): - dfn = Dfn.from_dict( - { + dfn = Dfn( + **{ "name": "test_transform", "schema_version": Version("2"), "blocks": { @@ -402,12 +402,8 @@ def test_parse_gwf_wel_file(model_workspace): def test_transform_gwf_ic_file(model_workspace, dfn_path): """Test transforming a parsed GWF IC file into structured data.""" - # Load the DFN for IC and convert to V2 - from modflow_devtools.dfns import MapV1To2 - - v1_dfns = load_flat(dfn_path) - mapper = MapV1To2() - ic_dfn = mapper.map(v1_dfns["gwf-ic"]) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") + ic_dfn = v1_dfns["gwf-ic"] # Find the IC file ic_files = list(model_workspace.rglob("*.ic")) @@ -444,12 +440,8 @@ def test_transform_gwf_ic_file(model_workspace, dfn_path): def test_transform_gwf_wel_file(model_workspace, dfn_path): """Test transforming a parsed GWF WEL file into structured data.""" - # Load the DFN for WEL and convert to V2 - from modflow_devtools.dfns import MapV1To2 - - v1_dfns = load_flat(dfn_path) - mapper = MapV1To2() - wel_dfn = mapper.map(v1_dfns["gwf-wel"]) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") + wel_dfn = v1_dfns["gwf-wel"] # Find the WEL file wel_files = list(model_workspace.rglob("*.wel")) @@ -532,12 +524,8 @@ def test_parse_gwf_oc_file(model_workspace): def test_transform_gwf_oc_file(model_workspace, dfn_path): """Test transforming a parsed GWF OC file into structured data.""" - # Load the DFN for OC and convert to V2 - from modflow_devtools.dfns import MapV1To2 - - v1_dfns = load_flat(dfn_path) - mapper = MapV1To2() - oc_dfn = mapper.map(v1_dfns["gwf-oc"]) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") + oc_dfn = v1_dfns["gwf-oc"] # Find the OC file oc_files = list(model_workspace.rglob("*.oc")) @@ -593,9 +581,8 @@ def test_transform_gwf_dis_file(model_workspace, dfn_path): """Test transforming a parsed GWF DIS file into structured data.""" # Load the DFN for DIS and convert to V2 - v1_dfns = load_flat(dfn_path) - mapper = MapV1To2() - dis_dfn = mapper.map(v1_dfns["gwf-dis"]) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") + dis_dfn = v1_dfns["gwf-dis"] # Find the DIS file dis_files = list(model_workspace.rglob("*.dis")) @@ -642,9 +629,8 @@ def test_transform_gwf_npf_file(model_workspace, dfn_path): """Test transforming a parsed GWF NPF file into structured data.""" # Load the DFN for NPF and convert to V2 - v1_dfns = load_flat(dfn_path) - mapper = MapV1To2() - npf_dfn = mapper.map(v1_dfns["gwf-npf"]) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") + npf_dfn = v1_dfns["gwf-npf"] # Find the NPF file npf_files = list(model_workspace.rglob("*.npf")) @@ -692,9 +678,8 @@ def test_transform_gwf_sto_file(model_workspace, dfn_path): """Test transforming a parsed GWF STO file into structured data.""" # Load the DFN for STO and convert to V2 - v1_dfns = load_flat(dfn_path) - mapper = MapV1To2() - sto_dfn = mapper.map(v1_dfns["gwf-sto"]) + v1_dfns = Dfn.load_all(dfn_path, schema_version="2.0.0.dev1") + sto_dfn = v1_dfns["gwf-sto"] # Find the STO file sto_files = list(model_workspace.rglob("*.sto")) From b36ee27d88512d014ccbdca8562aa78632b1b32e Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sun, 31 May 2026 13:46:11 -0700 Subject: [PATCH 2/4] compat fixes --- flopy4/mf6/utils/codegen/filters.py | 8 ++++---- flopy4/mf6/utils/codegen/make.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index c973ca15..14b58920 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -81,7 +81,7 @@ def output_path(dfn_name: str, root: Path) -> Path: def has_period_block(dfn: Dfn) -> bool: """True if the DFN defines a period block (stress package).""" - return "period" in (dfn["blocks"] or {}) + return "period" in (dfn.get("blocks") or {}) def has_dimensions_block(dfn: Dfn) -> bool: @@ -289,7 +289,7 @@ def list_col_dim(f: Field, dfn: Dfn) -> str | None: Falls back to the single entry in the DFN's dimensions block. Returns None when the dimension cannot be determined unambiguously. """ - dim_block = (dfn["blocks"] or {}).get("dimensions", {}) + dim_block = (dfn.get("blocks") or {}).get("dimensions", {}) if shape := f.get("shape", None): inner = shape.strip().strip("()") parts = [p.strip() for p in inner.split(",") if p.strip()] @@ -787,7 +787,7 @@ def block_schema(v1_dfn: Dfn, block_name: str) -> list[ColumnSpec]: its nested children (built by the v2.0.0.dev1 migration). Returns one ColumnSpec per column in DFN order. """ - v1_block = (v1_dfn["blocks"] or {}).get(block_name) or {} + v1_block = (v1_dfn.get("blocks") or {}).get(block_name) or {} list_field = next((f for f in v1_block.values() if f.get("type") == "list"), None) if list_field is None: return [] @@ -832,7 +832,7 @@ def v1_list_block_names(v1_dfn: Dfn) -> list[str]: """ seen: set[str] = set() result = [] - for block_name, block in (v1_dfn["blocks"] or {}).items(): + for block_name, block in (v1_dfn.get("blocks") or {}).items(): if block_name in seen: continue if any( diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index a779ac5b..558ec87c 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -405,8 +405,8 @@ def _build_block_property_specs( block property API for each recarray block. Returns (specs, block_names) where block_names is used as a skip-set in the main field loop. """ - dfn_dims = set((dfn["blocks"] or {}).get("dimensions", {}).keys()) - dfn_dims_ordered = list((dfn["blocks"] or {}).get("dimensions", {}).keys()) + dfn_dims = set((dfn.get("blocks") or {}).get("dimensions", {}).keys()) + dfn_dims_ordered = list((dfn.get("blocks") or {}).get("dimensions", {}).keys()) # Collect v2 list blocks, excluding those handled by TOML overrides. list_fields_map: dict[str, Field | None] = { @@ -832,7 +832,13 @@ def build_component_spec( ) break - field_specs = prefix_specs + extra_specs + data_specs + period_specs + _seen_py_names: set[str] = set() + _deduped: list[FieldSpec] = [] + for _fs in prefix_specs + extra_specs + data_specs + period_specs: + if _fs.py_name not in _seen_py_names: + _seen_py_names.add(_fs.py_name) + _deduped.append(_fs) + field_specs = _deduped # Build period_col_map: value columns in the period block (cellid/aux/boundname excluded). # Only packages with a standard stress-period list format produce a non-empty map. From d334a20f0142e3fb2d0a7292048cb80863b5dfc4 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sun, 31 May 2026 18:07:23 -0700 Subject: [PATCH 3/4] regenerate mf6 module --- flopy4/mf6/gwe/est.py | 6 +++--- flopy4/mf6/gwe/ic.py | 2 +- flopy4/mf6/gwe/lke.py | 1 + flopy4/mf6/gwf/buy.py | 4 +++- flopy4/mf6/gwf/chd.py | 6 +++--- flopy4/mf6/gwf/chdg.py | 8 ++++---- flopy4/mf6/gwf/csub.py | 18 ++++++++++-------- flopy4/mf6/gwf/drng.py | 6 +++--- flopy4/mf6/gwf/evt.py | 16 ++++++++-------- flopy4/mf6/gwf/evta.py | 6 +++--- flopy4/mf6/gwf/ghbg.py | 6 +++--- flopy4/mf6/gwf/lak.py | 10 +++++++++- flopy4/mf6/gwf/npf.py | 20 ++++++++++---------- flopy4/mf6/gwf/rcha.py | 2 +- flopy4/mf6/gwf/rivg.py | 8 ++++---- flopy4/mf6/gwf/sto.py | 8 ++++---- flopy4/mf6/gwf/vsc.py | 12 ++++++++---- flopy4/mf6/gwf/welg.py | 2 +- flopy4/mf6/gwt/ic.py | 2 +- flopy4/mf6/gwt/lkt.py | 1 + flopy4/mf6/ims.py | 6 +++--- flopy4/mf6/prt/prp.py | 4 ++-- flopy4/mf6/utl/ats.py | 2 +- flopy4/mf6/utl/spca.py | 2 +- 24 files changed, 88 insertions(+), 70 deletions(-) diff --git a/flopy4/mf6/gwe/est.py b/flopy4/mf6/gwe/est.py index 792d9de0..ab1ba4b6 100644 --- a/flopy4/mf6/gwe/est.py +++ b/flopy4/mf6/gwe/est.py @@ -24,13 +24,13 @@ class Est(Package): block="options", default=False, longname="activate zero-order decay in solid phase" ) density_water: Optional[float] = field( - block="options", default=None, longname="density of water" + block="options", default="1000.0", longname="density of water" ) heat_capacity_water: Optional[float] = field( - block="options", default=None, longname="heat capacity of water" + block="options", default="4184.0", longname="heat capacity of water" ) latent_heat_vaporization: Optional[float] = field( - block="options", default=None, longname="latent heat of vaporization" + block="options", default="2453500.0", longname="latent heat of vaporization" ) porosity: NDArray[np.float64] = array( block="griddata", diff --git a/flopy4/mf6/gwe/ic.py b/flopy4/mf6/gwe/ic.py index ddf27ec1..ed4631f5 100644 --- a/flopy4/mf6/gwe/ic.py +++ b/flopy4/mf6/gwe/ic.py @@ -21,7 +21,7 @@ class Ic(Package): strt: NDArray[np.float64] = array( block="griddata", dims=("nodes",), - default=None, + default="0.0", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="starting temperature", diff --git a/flopy4/mf6/gwe/lke.py b/flopy4/mf6/gwe/lke.py index 820bec5d..e089a019 100644 --- a/flopy4/mf6/gwe/lke.py +++ b/flopy4/mf6/gwe/lke.py @@ -104,6 +104,7 @@ class Lke(Package): converter=Converter(structure_array, takes_self=True, takes_field=True), longname="lake name", ) + # TODO: laksetting — type 'union' not yet supported status: Optional[NDArray[np.object_]] = embedded_keystring( "STATUS", "nlakes", diff --git a/flopy4/mf6/gwf/buy.py b/flopy4/mf6/gwf/buy.py index 1fe1dc7d..5e7bc5a1 100644 --- a/flopy4/mf6/gwf/buy.py +++ b/flopy4/mf6/gwf/buy.py @@ -20,7 +20,9 @@ class Buy(Package): hhformulation_rhs: bool = field( block="options", default=False, longname="hh formulation on right-hand side" ) - denseref: Optional[float] = field(block="options", default=None, longname="reference density") + denseref: Optional[float] = field( + block="options", default="1000.", longname="reference density" + ) density_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="fileout" ) diff --git a/flopy4/mf6/gwf/chd.py b/flopy4/mf6/gwf/chd.py index c7b5bd31..67c16d01 100644 --- a/flopy4/mf6/gwf/chd.py +++ b/flopy4/mf6/gwf/chd.py @@ -31,17 +31,17 @@ class Chd(Package): block="options", default=False, longname="print input to listing file" ) print_flows: bool = field( - block="options", default=False, longname="print chd flows to listing file" + block="options", default=False, longname="print CHD flows to listing file" ) save_flows: bool = field( - block="options", default=False, longname="save chd flows to budget file" + block="options", default=False, longname="save CHD flows to budget file" ) ts_file: Optional[Path] = path(block="options", default=None, converter=to_path, inout="filein") obs_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="filein" ) dev_no_newton: bool = field( - block="options", default=False, longname="turn off newton for unconfined cells" + block="options", default=False, longname="turn off Newton for unconfined cells" ) maxbound: Optional[int] = field( block="dimensions", default=None, init=False, longname="maximum number of constant heads" diff --git a/flopy4/mf6/gwf/chdg.py b/flopy4/mf6/gwf/chdg.py index bf4844b9..37e3d8bc 100644 --- a/flopy4/mf6/gwf/chdg.py +++ b/flopy4/mf6/gwf/chdg.py @@ -32,10 +32,10 @@ class Chdg(Package): block="options", default=False, longname="print input to listing file" ) print_flows: bool = field( - block="options", default=False, longname="print chd flows to listing file" + block="options", default=False, longname="print CHD flows to listing file" ) save_flows: bool = field( - block="options", default=False, longname="save chd flows to budget file" + block="options", default=False, longname="save CHD flows to budget file" ) obs_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="filein" @@ -44,7 +44,7 @@ class Chdg(Package): block="options", default=False, longname="export array variables to netcdf output files." ) dev_no_newton: bool = field( - block="options", default=False, longname="turn off newton for unconfined cells" + block="options", default=False, longname="turn off Newton for unconfined cells" ) maxbound: Optional[int] = field( block="dimensions", @@ -56,7 +56,7 @@ class Chdg(Package): head: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, diff --git a/flopy4/mf6/gwf/csub.py b/flopy4/mf6/gwf/csub.py index d55bebf4..809f6a01 100644 --- a/flopy4/mf6/gwf/csub.py +++ b/flopy4/mf6/gwf/csub.py @@ -21,10 +21,12 @@ class Csub(Package): print_input: bool = field( block="options", default=False, longname="print input to listing file" ) - save_flows: bool = field(block="options", default=False, longname="keyword to save csub flows") - gammaw: Optional[float] = field(block="options", default=None, longname="unit weight of water") + save_flows: bool = field(block="options", default=False, longname="keyword to save CSUB flows") + gammaw: Optional[float] = field( + block="options", default="9806.65", longname="unit weight of water" + ) beta: Optional[float] = field( - block="options", default=None, longname="compressibility of water" + block="options", default="4.6512e-10", longname="compressibility of water" ) head_based: bool = field( block="options", @@ -42,7 +44,7 @@ class Csub(Package): compression_indices: bool = field( block="options", default=False, - longname="keyword to indicate cr and cc are read instead of sse and ssv", + longname="keyword to indicate CR and CC are read instead of SSE and SSV", ) update_material_properties: bool = field( block="options", @@ -106,7 +108,7 @@ class Csub(Package): block="options", default=None, converter=to_path, inout="filein" ) ninterbeds: Optional[int] = dim( - block="dimensions", coord=False, default=None, longname="number of csub interbed systems" + block="dimensions", coord=False, default=None, longname="number of CSUB interbed systems" ) maxsig0: Optional[int] = dim( block="dimensions", @@ -199,12 +201,12 @@ class Csub(Package): dims=("ninterbeds",), default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), - longname="well name", + longname="interbed name", ) cg_ske_cr: NDArray[np.float64] = array( block="griddata", dims=("nodes",), - default=None, + default="1e-5", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="elastic coarse specific storage", @@ -212,7 +214,7 @@ class Csub(Package): cg_theta: NDArray[np.float64] = array( block="griddata", dims=("nodes",), - default=None, + default="0.2", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="initial coarse-grained material porosity", diff --git a/flopy4/mf6/gwf/drng.py b/flopy4/mf6/gwf/drng.py index a4cdcf68..0f6768fd 100644 --- a/flopy4/mf6/gwf/drng.py +++ b/flopy4/mf6/gwf/drng.py @@ -38,7 +38,7 @@ class Drng(Package): block="options", default=False, longname="print calculated flows to listing file" ) save_flows: bool = field( - block="options", default=False, longname="save drng flows to budget file" + block="options", default=False, longname="save DRNG flows to budget file" ) obs_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="filein" @@ -58,7 +58,7 @@ class Drng(Package): elev: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, @@ -67,7 +67,7 @@ class Drng(Package): cond: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, diff --git a/flopy4/mf6/gwf/evt.py b/flopy4/mf6/gwf/evt.py index 2f98e838..e60ab1b2 100644 --- a/flopy4/mf6/gwf/evt.py +++ b/flopy4/mf6/gwf/evt.py @@ -48,7 +48,7 @@ class Evt(Package): surf_rate_specified: bool = field( block="options", default=False, - longname="specify proportion of evapotranspiration rate at et surface", + longname="specify proportion of evapotranspiration rate at ET surface", ) maxbound: Optional[int] = field( block="dimensions", @@ -57,7 +57,7 @@ class Evt(Package): longname="maximum number of evapotranspiration cells", ) nseg: Optional[int] = dim( - block="dimensions", coord=False, default=None, longname="number of et segments" + block="dimensions", coord=False, default=None, longname="number of ET segments" ) naux: Optional[int] = dim(block="__dim__", coord=False, default=None) surface: Optional[NDArray[np.float64]] = array( @@ -66,7 +66,7 @@ class Evt(Package): default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, - longname="et surface", + longname="ET surface", ) rate: Optional[NDArray[np.float64]] = array( block="period", @@ -74,7 +74,7 @@ class Evt(Package): default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, - longname="maximum et rate", + longname="maximum ET rate", ) depth: Optional[NDArray[np.float64]] = array( block="period", @@ -82,7 +82,7 @@ class Evt(Package): default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, - longname="et extinction depth", + longname="ET extinction depth", ) pxdp: Optional[NDArray[np.float64]] = array( block="period", @@ -90,7 +90,7 @@ class Evt(Package): default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, - longname="proportion of et extinction depth", + longname="proportion of ET extinction depth", ) petm: Optional[NDArray[np.float64]] = array( block="period", @@ -98,7 +98,7 @@ class Evt(Package): default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, - longname="proportion of maximum et rate", + longname="proportion of maximum ET rate", ) petm0: Optional[NDArray[np.float64]] = array( block="period", @@ -106,7 +106,7 @@ class Evt(Package): default=None, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, - longname="proportion of maximum et rate at et surface", + longname="proportion of maximum ET rate at ET surface", ) aux: Optional[NDArray[np.float64]] = array( block="period", diff --git a/flopy4/mf6/gwf/evta.py b/flopy4/mf6/gwf/evta.py index 7f5d2bae..f7afbefb 100644 --- a/flopy4/mf6/gwf/evta.py +++ b/flopy4/mf6/gwf/evta.py @@ -60,7 +60,7 @@ class Evta(Package): surface: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "ncpl"), - default=None, + default="0.", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="evapotranspiration surface", @@ -68,7 +68,7 @@ class Evta(Package): rate: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "ncpl"), - default=None, + default="1.e-3", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="evapotranspiration rate", @@ -76,7 +76,7 @@ class Evta(Package): depth: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "ncpl"), - default=None, + default="1.0", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="extinction depth", diff --git a/flopy4/mf6/gwf/ghbg.py b/flopy4/mf6/gwf/ghbg.py index 3cd5aafa..df944428 100644 --- a/flopy4/mf6/gwf/ghbg.py +++ b/flopy4/mf6/gwf/ghbg.py @@ -35,7 +35,7 @@ class Ghbg(Package): block="options", default=False, longname="print calculated flows to listing file" ) save_flows: bool = field( - block="options", default=False, longname="save ghbg flows to budget file" + block="options", default=False, longname="save GHBG flows to budget file" ) obs_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="filein" @@ -54,7 +54,7 @@ class Ghbg(Package): bhead: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, @@ -63,7 +63,7 @@ class Ghbg(Package): cond: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, diff --git a/flopy4/mf6/gwf/lak.py b/flopy4/mf6/gwf/lak.py index 5872ffd7..3b639bac 100644 --- a/flopy4/mf6/gwf/lak.py +++ b/flopy4/mf6/gwf/lak.py @@ -56,7 +56,7 @@ class Lak(Package): block="options", default=None, longname="surface depression depth" ) maximum_iterations: Optional[int] = field( - block="options", default=None, longname="lak newton-raphson iterations" + block="options", default=None, longname="LAK Newton-Raphson iterations" ) maximum_stage_change: Optional[float] = field( block="options", default=None, longname="stage closure tolerance" @@ -259,6 +259,14 @@ class Lak(Package): converter=Converter(structure_array, takes_self=True, takes_field=True), longname="bed slope", ) + number: Optional[NDArray[np.int64]] = array( + block="period", + dims=("nper",), + default=None, + converter=Converter(structure_array, takes_self=True, takes_field=True), + longname="lake or outlet number for this entry", + ) + # TODO: laksetting — type 'union' not yet supported status: Optional[NDArray[np.object_]] = embedded_keystring( "STATUS", "nlakes", diff --git a/flopy4/mf6/gwf/npf.py b/flopy4/mf6/gwf/npf.py index edf81461..5daa84fd 100644 --- a/flopy4/mf6/gwf/npf.py +++ b/flopy4/mf6/gwf/npf.py @@ -35,26 +35,26 @@ class Xt3doptions(Record): _keyword: ClassVar[str] = "xt3d" rhs: Optional[bool] = attrs.field(default=None) - save_flows: bool = field(block="options", default=False, longname="keyword to save npf flows") + save_flows: bool = field(block="options", default=False, longname="keyword to save NPF flows") print_flows: bool = field( - block="options", default=False, longname="keyword to print npf flows to listing file" + block="options", default=False, longname="keyword to print NPF flows to listing file" ) alternative_cell_averaging: Optional[str] = field( block="options", default=None, longname="conductance weighting option" ) thickstrt: bool = field( - block="options", default=False, longname="keyword to activate thickstrt option" + block="options", default=False, longname="keyword to activate THICKSTRT option" ) cvoptions: Optional[Cvoptions] = field(block="options", default=None) perched: bool = field( - block="options", default=False, longname="keyword to activate perched option" + block="options", default=False, longname="keyword to activate PERCHED option" ) rewet: Optional[Rewet] = field(block="options", default=None) xt3doptions: Optional[Xt3doptions] = field(block="options", default=None) highest_cell_saturation: bool = field( block="options", default=False, - longname="keyword to activate highest_cell_saturation option", + longname="keyword to activate HIGHEST_CELL_SATURATION option", ) save_specific_discharge: bool = field( block="options", default=False, longname="keyword to save specific discharge" @@ -63,10 +63,10 @@ class Xt3doptions(Record): block="options", default=False, longname="keyword to save saturation" ) k22overk: bool = field( - block="options", default=False, longname="keyword to indicate that specified k22 is a ratio" + block="options", default=False, longname="keyword to indicate that specified K22 is a ratio" ) k33overk: bool = field( - block="options", default=False, longname="keyword to indicate that specified k33 is a ratio" + block="options", default=False, longname="keyword to indicate that specified K33 is a ratio" ) tvk_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="filein" @@ -78,7 +78,7 @@ class Xt3doptions(Record): block="options", default=False, longname="export array variables to netcdf output files." ) dev_no_newton: bool = field( - block="options", default=False, longname="turn off newton for unconfined cells" + block="options", default=False, longname="turn off Newton for unconfined cells" ) dev_omega: Optional[float] = field( block="options", default=None, longname="set saturation omega value" @@ -97,7 +97,7 @@ class Xt3doptions(Record): default=1.0, netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), - longname="hydraulic conductivity (l/t)", + longname="hydraulic conductivity (L/T)", ) k22: Optional[NDArray[np.float64]] = array( block="griddata", @@ -113,7 +113,7 @@ class Xt3doptions(Record): default=None, netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), - longname="hydraulic conductivity of third ellipsoid axis (l/t)", + longname="hydraulic conductivity of third ellipsoid axis (L/T)", ) angle1: Optional[NDArray[np.float64]] = array( block="griddata", diff --git a/flopy4/mf6/gwf/rcha.py b/flopy4/mf6/gwf/rcha.py index 61a14393..a47616b3 100644 --- a/flopy4/mf6/gwf/rcha.py +++ b/flopy4/mf6/gwf/rcha.py @@ -60,7 +60,7 @@ class Rcha(Package): recharge: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "ncpl"), - default=None, + default="1.e-3", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="recharge rate", diff --git a/flopy4/mf6/gwf/rivg.py b/flopy4/mf6/gwf/rivg.py index 9261f59b..b2077610 100644 --- a/flopy4/mf6/gwf/rivg.py +++ b/flopy4/mf6/gwf/rivg.py @@ -35,7 +35,7 @@ class Rivg(Package): block="options", default=False, longname="print calculated flows to listing file" ) save_flows: bool = field( - block="options", default=False, longname="save riv flows to budget file" + block="options", default=False, longname="save RIV flows to budget file" ) obs_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="filein" @@ -54,7 +54,7 @@ class Rivg(Package): stage: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, @@ -63,7 +63,7 @@ class Rivg(Package): cond: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, @@ -72,7 +72,7 @@ class Rivg(Package): rbot: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, diff --git a/flopy4/mf6/gwf/sto.py b/flopy4/mf6/gwf/sto.py index 873cf994..ff72afbb 100644 --- a/flopy4/mf6/gwf/sto.py +++ b/flopy4/mf6/gwf/sto.py @@ -16,11 +16,11 @@ @xattree(kw_only=True) class Sto(Package): - save_flows: bool = field(block="options", default=False, longname="keyword to save npf flows") + save_flows: bool = field(block="options", default=False, longname="keyword to save NPF flows") storagecoefficient: bool = field( block="options", default=False, - longname="keyword to indicate ss is read as storage coefficient", + longname="keyword to indicate SS is read as storage coefficient", ) ss_confined_only: bool = field( block="options", @@ -55,7 +55,7 @@ class Sto(Package): ss: NDArray[np.float64] = array( block="griddata", dims=("nodes",), - default=None, + default="1.e-5", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="specific storage", @@ -63,7 +63,7 @@ class Sto(Package): sy: NDArray[np.float64] = array( block="griddata", dims=("nodes",), - default=None, + default="0.15", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="specific yield", diff --git a/flopy4/mf6/gwf/vsc.py b/flopy4/mf6/gwf/vsc.py index bf87154c..1644336b 100644 --- a/flopy4/mf6/gwf/vsc.py +++ b/flopy4/mf6/gwf/vsc.py @@ -17,7 +17,7 @@ @xattree(kw_only=True) class Vsc(Package): - viscref: Optional[float] = field(block="options", default=None, longname="reference viscosity") + viscref: Optional[float] = field(block="options", default="1.0", longname="reference viscosity") temperature_species_name: Optional[str] = field( block="options", default=None, longname="auxspeciesname that corresponds to temperature" ) @@ -27,13 +27,17 @@ class Vsc(Package): longname="keyword to specify viscosity formulation for the temperature species", ) thermal_a2: Optional[float] = field( - block="options", default=None, longname="coefficient used in nonlinear viscosity function" + block="options", default="10.", longname="coefficient used in nonlinear viscosity function" ) thermal_a3: Optional[float] = field( - block="options", default=None, longname="coefficient used in nonlinear viscosity function" + block="options", + default="248.37", + longname="coefficient used in nonlinear viscosity function", ) thermal_a4: Optional[float] = field( - block="options", default=None, longname="coefficient used in nonlinear viscosity function" + block="options", + default="133.15", + longname="coefficient used in nonlinear viscosity function", ) viscosity_file: Optional[Path] = path( block="options", default=None, converter=to_path, inout="fileout" diff --git a/flopy4/mf6/gwf/welg.py b/flopy4/mf6/gwf/welg.py index 8aab79a4..a4996f12 100644 --- a/flopy4/mf6/gwf/welg.py +++ b/flopy4/mf6/gwf/welg.py @@ -63,7 +63,7 @@ class Welg(Package): q: Optional[NDArray[np.float64]] = array( block="period", dims=("nper", "nodes"), - default=None, + default="3.e30", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), on_setattr=update_maxbound, diff --git a/flopy4/mf6/gwt/ic.py b/flopy4/mf6/gwt/ic.py index 57d36e4e..12175e47 100644 --- a/flopy4/mf6/gwt/ic.py +++ b/flopy4/mf6/gwt/ic.py @@ -21,7 +21,7 @@ class Ic(Package): strt: NDArray[np.float64] = array( block="griddata", dims=("nodes",), - default=None, + default="0.0", netcdf=True, converter=Converter(structure_array, takes_self=True, takes_field=True), longname="starting concentration", diff --git a/flopy4/mf6/gwt/lkt.py b/flopy4/mf6/gwt/lkt.py index 758b29bc..c78d015b 100644 --- a/flopy4/mf6/gwt/lkt.py +++ b/flopy4/mf6/gwt/lkt.py @@ -90,6 +90,7 @@ class Lkt(Package): converter=Converter(structure_array, takes_self=True, takes_field=True), longname="lake name", ) + # TODO: laksetting — type 'union' not yet supported status: Optional[NDArray[np.object_]] = embedded_keystring( "STATUS", "nlakes", diff --git a/flopy4/mf6/ims.py b/flopy4/mf6/ims.py index 22bc6bbd..61af2727 100644 --- a/flopy4/mf6/ims.py +++ b/flopy4/mf6/ims.py @@ -56,7 +56,7 @@ class Rclose(Record): under_relaxation_gamma: Optional[float] = field( block="nonlinear", default=None, - longname="relaxation factor for simple or the history or memory term factor for the cooley and delta-bar-delta algorithms", + longname="relaxation factor for SIMPLE or the history or memory term factor for the Cooley and delta-bar-delta algorithms", ) under_relaxation_theta: Optional[float] = field( block="nonlinear", default=None, longname="under relaxation reduction factor" @@ -95,10 +95,10 @@ class Rclose(Record): rclose: Optional[Rclose] = field(block="linear", default=None) linear_acceleration: str = field(block="linear", longname="linear acceleration method") relaxation_factor: Optional[float] = field( - block="linear", default=None, longname="relaxation factor used by ilu factorization" + block="linear", default=None, longname="relaxation factor used by ILU factorization" ) preconditioner_levels: Optional[int] = field( - block="linear", default=None, longname="level of fill for ilu decomposition" + block="linear", default=None, longname="level of fill for ILU decomposition" ) preconditioner_drop_tolerance: Optional[float] = field( block="linear", default=None, longname="drop tolerance used to drop preconditioner terms" diff --git a/flopy4/mf6/prt/prp.py b/flopy4/mf6/prt/prp.py index 92409084..3ce9117c 100644 --- a/flopy4/mf6/prt/prp.py +++ b/flopy4/mf6/prt/prp.py @@ -38,7 +38,7 @@ class ReleaseTimesfile(Record): block="options", default=None, longname="exit solve method" ) exit_solve_tolerance: Optional[float] = field( - block="options", default=None, longname="exit solve tolerance" + block="options", default="1e-5", longname="exit solve tolerance" ) local_z: bool = field( block="options", default=False, longname="whether to use local z coordinates" @@ -76,7 +76,7 @@ class ReleaseTimesfile(Record): block="options", default=None, longname="release time frequency" ) coordinate_check_method: Optional[str] = field( - block="options", default=None, longname="coordinate checking method" + block="options", default="eager", longname="coordinate checking method" ) dev_cycle_detection_window: Optional[int] = field( block="options", default=None, longname="cycle detection window size" diff --git a/flopy4/mf6/utl/ats.py b/flopy4/mf6/utl/ats.py index 11510d88..0b611d8d 100644 --- a/flopy4/mf6/utl/ats.py +++ b/flopy4/mf6/utl/ats.py @@ -15,7 +15,7 @@ @xattree(kw_only=True) class Ats(Package): maxats: Optional[int] = dim( - block="dimensions", coord=False, default=None, longname="number of ats periods" + block="dimensions", coord=False, default="1", longname="number of ATS periods" ) iperats: Optional[NDArray[np.int64]] = array( block="perioddata", diff --git a/flopy4/mf6/utl/spca.py b/flopy4/mf6/utl/spca.py index 0a6b22e7..ad6c79fc 100644 --- a/flopy4/mf6/utl/spca.py +++ b/flopy4/mf6/utl/spca.py @@ -18,7 +18,7 @@ class Spca(Package): multi_package: ClassVar[bool] = True - readasarrays: bool = field(block="options", default=False, longname="use array-based input") + readasarrays: bool = field(block="options", default=True, longname="use array-based input") print_input: bool = field( block="options", default=False, longname="print input to listing file" ) From 9ba64713fa601f7302e40e961898eefdd87c65a1 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sun, 31 May 2026 18:23:24 -0700 Subject: [PATCH 4/4] don't fill arrays with default val --- flopy4/mf6/converter/ingress/structure.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flopy4/mf6/converter/ingress/structure.py b/flopy4/mf6/converter/ingress/structure.py index 8f6afc9b..d19261a0 100644 --- a/flopy4/mf6/converter/ingress/structure.py +++ b/flopy4/mf6/converter/ingress/structure.py @@ -1003,7 +1003,7 @@ def structure_array( coords, list(coords_dict.values()), shape=shape, - fill_value=field.default or FILL_DNODATA, + fill_value=FILL_DNODATA, ) else: # Empty dict - return empty sparse array @@ -1011,7 +1011,7 @@ def structure_array( np.empty((len(shape), 0), dtype=int), [], shape=shape, - fill_value=field.default or FILL_DNODATA, + fill_value=FILL_DNODATA, ) else: # Dense approach @@ -1086,10 +1086,6 @@ def structure_array( # For multi-dimensional arrays with object dtype, store the object result[kper] = val - # Apply fill value replacement (skip for object dtypes) - if field.dtype != np.object_: - result[result == FILL_DNODATA] = field.default or FILL_DNODATA - elif isinstance(value, list): # List format result = _parse_list_format(value, dims_names, tuple(shape), field)