From bc5230ab74a34c2b0186538df55c7c125a0e1027 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 10:44:02 -0700 Subject: [PATCH 01/10] support number-or-word unions and derived union tagging devtools now derives `tagged` on unions and records: a union is tagged if every arm begins with a keyword. LAK's `bedleak` and MAW's `head_limit` become untagged unions of a word (NONE, OFF) and a number. Track both: - Codegen tells a union column (each arm one value: OBS's id, `bedleak`) from a keystring (arms are line forms) by its arms, not by `tagged`, which LAK's `laksetting` (arms led by a lake number) no longer sets. - A keyword arm types as a literal: `Union[Literal["NONE"], float]`. It reads inline, matched in any case, else as a number. - An arm of a keyword and one value names the value after the keyword, like a tagged scalar arm, so MAW's `HeadLimit.head_limit` keeps its name. - `_is_instance` checks every arm of a union, so `str` fields no longer need to accept numbers. - The grammar prefixes a tagged `valid` string's name itself, since the union made of it no longer says it's tagged. Grammars are unchanged. - Bump modflow-devtools to 7df4ae97. Co-Authored-By: Claude Opus 5.5 --- flopy4/mf6/codec/reader/grammar/__init__.py | 5 ++ flopy4/mf6/codec/reader/grammar/filters.py | 8 +- .../grammar/templates/component.lark.jinja | 2 +- .../reader/grammar/templates/macros.jinja | 19 +++-- flopy4/mf6/gwf/lak.py | 4 +- flopy4/mf6/gwf/maw.py | 4 +- flopy4/mf6/item.py | 9 +- flopy4/mf6/record.py | 31 +++++-- flopy4/mf6/utils/codegen/filters.py | 35 ++++++-- flopy4/mf6/utils/codegen/make.py | 19 ++++- pixi.lock | 50 +++++------ test/mf6/test_mf6_codegen.py | 82 ++++++++++++++++++- test/mf6/test_mf6_grammar_gen.py | 25 ++++++ 13 files changed, 230 insertions(+), 63 deletions(-) diff --git a/flopy4/mf6/codec/reader/grammar/__init__.py b/flopy4/mf6/codec/reader/grammar/__init__.py index 19c9f920..e8252d71 100644 --- a/flopy4/mf6/codec/reader/grammar/__init__.py +++ b/flopy4/mf6/codec/reader/grammar/__init__.py @@ -63,10 +63,14 @@ def _to_context(blocks: dict[str, Block]) -> tuple[list[dict], dict[str, object] standalone_fields = [] # a list's element lines, each read into the list (see names.py) elements = set() + # tagged `valid` scalars, whose name precedes their value + prefixed = set() for field_name, field in block.fields.items(): if field.removed: continue + if field.tagged and getattr(field, "valid", None): + prefixed.add(field_name) field = filters.valid_as_union(field) if isinstance(field, List): if isinstance(field.item, Union): @@ -107,6 +111,7 @@ def _to_context(blocks: dict[str, Block]) -> tuple[list[dict], dict[str, object] "header": header.name if isinstance(header, Record) else None, "standalone_fields": standalone_fields, "rules": rules, + "prefixed": prefixed, "recarrays": recarrays, "redirect": not bare_array, } diff --git a/flopy4/mf6/codec/reader/grammar/filters.py b/flopy4/mf6/codec/reader/grammar/filters.py index 509d2bda..f1ba4b03 100644 --- a/flopy4/mf6/codec/reader/grammar/filters.py +++ b/flopy4/mf6/codec/reader/grammar/filters.py @@ -13,15 +13,13 @@ def valid_as_union(field: InputField) -> InputField: """Turn a ``valid=``-restricted scalar (e.g. STO's ``storage``, one of STEADY-STATE/TRANSIENT) into a keyword union with one arm per value, so it reuses the existing union grammar/dispatch instead of a second path. + A union never begins with its own name, so whether a tagged scalar's + name precedes the value is up to the caller (see ``union_field``). """ valid = getattr(field, "valid", None) if not valid: return field - return Union( - name=field.name, - tagged=field.tagged, - arms={str(v): Keyword(name=str(v)) for v in valid}, - ) + return Union(name=field.name, arms={str(v): Keyword(name=str(v)) for v in valid}) def keyword_literal(field: InputField) -> str: diff --git a/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja b/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja index 4fbbc39a..655bf658 100644 --- a/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja +++ b/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja @@ -70,7 +70,7 @@ double_index: block_index {% elif field.type == 'record' and field.children is not none -%} {{ m.record_field(field_name, field, name) }} {% elif field.type == 'union' and field.children is not none -%} -{{ m.union_field(field_name, field, name) }} +{{ m.union_field(field_name, field, name, field_name in block.prefixed) }} {% elif field.type == 'file' -%} {{ m.file_field(name, field) }} {% else -%} diff --git a/flopy4/mf6/codec/reader/grammar/templates/macros.jinja b/flopy4/mf6/codec/reader/grammar/templates/macros.jinja index 035df6af..7366da94 100644 --- a/flopy4/mf6/codec/reader/grammar/templates/macros.jinja +++ b/flopy4/mf6/codec/reader/grammar/templates/macros.jinja @@ -104,9 +104,9 @@ {%- if child.type == 'keyword' -%} !{{ rule(field_name, child_name, tags=['kw']) }}: {{ child|keyword_literal }} {%- elif child.type == 'array' -%} -{{ rule(field_name, child_name, tags=['val']) }}: "{{ child.name }}"i {{ child|field_type }} +{{ rule(field_name, child_name, tags=['val']) }}: {% if child.tagged %}"{{ child.name }}"i {% endif %}{{ child|field_type }} {%- else -%} -{{ rule(field_name, child_name, tags=['val']) }}: "{{ child.name }}"i {{ child.type }} +{{ rule(field_name, child_name, tags=['val']) }}: {% if child.tagged %}"{{ child.name }}"i {% endif %}{{ child.type }} {%- endif %} {% endfor -%} @@ -119,16 +119,17 @@ {%- endfor -%} {%- endmacro %} -{% macro union_field(field_name, field, name) -%} +{% macro union_field(field_name, field, name, prefixed=false) -%} {# The alternation must be parenthesized before appending `[_remark] _NL` -- `|` binds looser than concatenation, so an unparenthesized "A | B | C [_remark] _NL" only requires the line ending after the *last* arm. A - `tagged true` union (e.g. NPF's alternative_cell_averaging, confirmed via - valid_as_union) additionally needs its own field-name keyword prefixed -- - real files write "ALTERNATIVE_CELL_AVERAGING LOGARITHMIC", not a bare - "LOGARITHMIC" -- unlike an untagged keystring (e.g. ocsetting's ALL/ - FIRST/LAST), where the chosen value's own keyword is the whole field. #} -{{ name }}: {% if field.tagged %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL + tagged `valid` scalar made a union (`prefixed`, e.g. NPF's + alternative_cell_averaging, via valid_as_union) additionally needs its + own field-name keyword prefixed -- real files write + "ALTERNATIVE_CELL_AVERAGING LOGARITHMIC", not a bare "LOGARITHMIC" -- + unlike a keystring (e.g. ocsetting's ALL/FIRST/LAST), where the chosen + value's own keyword is the whole field. #} +{{ name }}: {% if prefixed %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL {{ union_arms(field_name, field) }} {%- endmacro %} diff --git a/flopy4/mf6/gwf/lak.py b/flopy4/mf6/gwf/lak.py index 2783ed1d..da359eab 100644 --- a/flopy4/mf6/gwf/lak.py +++ b/flopy4/mf6/gwf/lak.py @@ -1,6 +1,6 @@ # autogenerated file, do not modify from pathlib import Path -from typing import ClassVar, Optional, Union +from typing import ClassVar, Literal, Optional, Union import attrs import numpy as np @@ -59,7 +59,7 @@ class Connectiondata(Item): iconn: int = field(index=True) cellid: tuple = field(cellid=True) claktype: str = field() - bedleak: str = field() + bedleak: Union[Literal["NONE"], float] = field(union=("keyword", "double")) belev: float = field() telev: float = field() connlen: float = field() diff --git a/flopy4/mf6/gwf/maw.py b/flopy4/mf6/gwf/maw.py index c1b241bf..f624f1bc 100644 --- a/flopy4/mf6/gwf/maw.py +++ b/flopy4/mf6/gwf/maw.py @@ -1,6 +1,6 @@ # autogenerated file, do not modify from pathlib import Path -from typing import ClassVar, Optional, Union +from typing import ClassVar, Literal, Optional, Union import attrs import numpy as np @@ -107,7 +107,7 @@ class WellHead(Item): class HeadLimit(Item): _keyword: ClassVar[str] = "head_limit" ifno: int = field(index=True, fk="packagedata.ifno") - head_limit: str = field() + head_limit: Union[Literal["OFF"], float] = field(union=("keyword", "double")) @attrs.define class Shutoff(Item): diff --git a/flopy4/mf6/item.py b/flopy4/mf6/item.py index 29b27fd6..3dfc0903 100644 --- a/flopy4/mf6/item.py +++ b/flopy4/mf6/item.py @@ -637,7 +637,14 @@ def width(f: attrs.Attribute) -> int: and _nested_union_classes(cls, t) is not None # type: ignore[arg-type] ] main_fields = [f for f in main_fields if f not in nested_union_fields] - union_fields = [f for f in main_fields if f.metadata.get("union")] + # Trailing columns of varying width (OBS's ids, see _read_unions). A + # union with a keyword arm is one token, wherever it falls, read like + # any other value (LAK's bedleak: a number or NONE, see _coerce). + union_fields = [ + f + for f in main_fields + if (kinds := f.metadata.get("union")) is not None and "keyword" not in kinds + ] main_fields = [f for f in main_fields if f not in union_fields] array_fields = [ f for f in main_fields if f.metadata.get("array") and not _dim_counted(cls, f) diff --git a/flopy4/mf6/record.py b/flopy4/mf6/record.py index 024d7fe8..3f8220ac 100644 --- a/flopy4/mf6/record.py +++ b/flopy4/mf6/record.py @@ -16,7 +16,7 @@ from collections.abc import Iterable, Mapping from functools import lru_cache from pathlib import Path, PurePath -from typing import Any, Union, cast, get_args, get_origin +from typing import Any, Literal, Union, cast, get_args, get_origin import attrs import numpy as np @@ -66,11 +66,18 @@ def _is_bool_field(f: attrs.Attribute) -> bool: return t in (bool, "bool") +def _words(t: Any) -> tuple[str, ...]: + """The words a union type's ``Literal`` arms allow (LAK's bedleak: NONE).""" + return tuple(w for a in get_args(t) if get_origin(a) is Literal for w in get_args(a)) + + def _coerce(token: Any, f: attrs.Attribute) -> Any: """Cast a raw token to a field's declared type (time_series falls back to the raw string if it isn't a float). Only Optional[X] (a single non-None union arm) is unwrapped -- a genuine multi-type union like - Union[float, str] is deliberately ambiguous and left as the raw token.""" + Union[float, str] is deliberately ambiguous and left as the raw token -- + but a word a ``Literal`` arm allows is tried first, matched in any case + (``Union[Literal["NONE"], float]``: NONE, or else a number).""" if f.metadata.get("time_series"): try: return float(token) @@ -80,6 +87,10 @@ def _coerce(token: Any, f: attrs.Attribute) -> Any: origin = get_origin(t) if origin is types.UnionType or origin is Union: args = [a for a in get_args(t) if a is not type(None)] + if words := _words(t): + if (word := str(token).upper()) in words: + return word + args = [a for a in args if get_origin(a) is not Literal] if len(args) != 1: return token t = args[0] @@ -121,11 +132,17 @@ def _list_elem_coerce(token: Any, f: attrs.Attribute) -> Any: def _is_instance(value: Any, f: attrs.Attribute) -> bool: """Whether a value fits a scalar field's declared type (bool, int, - float, str or Path); any other field takes anything.""" - t: Any = f.type + float, str or Path, a ``Literal`` word, or a union of them); any other + field takes anything.""" + return _fits(value, f.type) + + +def _fits(value: Any, t: Any) -> bool: origin = get_origin(t) if origin is types.UnionType or origin is Union: - t = next((a for a in get_args(t) if a is not type(None)), t) + return any(_fits(value, a) for a in get_args(t) if a is not type(None)) + if origin is Literal: + return isinstance(value, str) and value.upper() in get_args(t) if t in (bool, "bool"): return isinstance(value, (bool, np.bool_)) if isinstance(value, (bool, np.bool_)): @@ -135,9 +152,7 @@ def _is_instance(value: Any, f: attrs.Attribute) -> bool: if t in (float, "float"): return isinstance(value, (int, float, np.integer, np.floating)) if t in (str, "str"): - # A DFN string is any token, and some hold a number or a word (LAK's - # bedleak: a value or NONE). - return isinstance(value, (str, int, float, np.integer, np.floating)) + return isinstance(value, str) if t in (Path, "Path"): return isinstance(value, (str, PurePath)) return True diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index e7d7426c..44bef828 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -775,6 +775,11 @@ def uses_union(self) -> bool: """Whether the module needs ``Union`` imported.""" return "Union[" in self.annotation + @property + def uses_literal(self) -> bool: + """Whether the module needs ``Literal`` imported.""" + return "Literal[" in self.annotation + @property def uses_ts_ref(self) -> bool: """Whether the module needs ``TimeSeriesRef`` imported.""" @@ -890,11 +895,19 @@ def feature_id_column( def union_column( - name: str, arms: tuple[str, ...], *, optional: bool = False, time_series: bool = False + name: str, + arms: tuple[str, ...], + *, + words: tuple[str, ...] = (), + optional: bool = False, + time_series: bool = False, ) -> ItemColumn: """One column that is any of several arm kinds (OBS's id: a cellid, an - index or a boundname), told apart when read.""" - types = dict.fromkeys(_UNION_ARM_PY[arm] for arm in arms) + index or a boundname), told apart when read. A keyword arm is one of + ``words``, typed as a literal (LAK's bedleak: ``Literal["NONE"]``).""" + quoted = ", ".join('"' + w.upper() + '"' for w in words) + literal = f"Literal[{quoted}]" + types = dict.fromkeys(literal if arm == "keyword" else _UNION_ARM_PY[arm] for arm in arms) return attr_column( name, f"Union[{', '.join(types)}]", @@ -1100,6 +1113,19 @@ class ColumnSpec: is_index: bool # dev3 Integer.index -- 0-based, written as 1-based (+1 at write time) +def is_column_union(f: FieldV3) -> bool: + """Whether a union is one column, any of its arms (OBS's id: a cellid, + an index or a boundname; LAK's bedleak: a number or NONE), rather than a + keystring whose arms are line forms. Each arm of a column is one value: + a keyword, or an untagged scalar or array. A keystring's arms are + records or tagged values, whose keyword picks the arm wherever it falls + (LAK's laksetting arms lead with a lake or outlet number).""" + return isinstance(f, UnionField) and all( + isinstance(a, KeywordField) or not (isinstance(a, Record) or a.tagged) + for a in f.arms.values() + ) + + def find_keystring_union(list_field: ListField) -> UnionField | None: """Find the discriminating Union in a List field's per-row shape, if any. @@ -1123,8 +1149,7 @@ def find_keystring_union(list_field: ListField) -> UnionField | None: return item if isinstance(item, Record): for f in item.fields.values(): - # An untagged union (OBS's id) is one column, not a keystring. - if isinstance(f, UnionField) and f.tagged: + if isinstance(f, UnionField) and not is_column_union(f): return f return None diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index d8cf963d..05e6d375 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -279,12 +279,13 @@ class names already built for it (see ``_build_arm_specs_from_union``) item = filters.attr_column( name, "bool", {"tagged": True}, optional=True, default="False" ) - elif isinstance(f, UnionField) and not f.tagged: + elif filters.is_column_union(f): # One column, any of the arms (OBS's id: a cellid, an index or # a boundname), told apart when read (see Item.from_tokens). item = filters.union_column( name, tuple(_arm_kind(arm) for arm in f.arms.values()), + words=tuple(a.name for a in f.arms.values() if isinstance(a, KeywordField)), optional=optional, time_series=ts, ) @@ -306,7 +307,10 @@ class names already built for it (see ``_build_arm_specs_from_union``) def _arm_kind(arm: FieldV3) -> str: - """An untagged union arm's kind: "cellid", "index", or its DFN type.""" + """An untagged union arm's kind: "cellid", "index", or its DFN type. A + keyword arm is its own name, a literal word (LAK's bedleak: NONE).""" + if isinstance(arm, KeywordField): + return "keyword" if isinstance(arm, Array) and arm.cellid: return "cellid" if getattr(arm, "index", False): @@ -607,6 +611,11 @@ def _build_arm_spec( if kw_idx is not None: keyword = arm_fields[kw_idx][0] rest = arm_fields[:kw_idx] + arm_fields[kw_idx + 1 :] + if kw_idx == 0 and len(rest) == 1: + # A keyword and one value reads like a tagged scalar arm + # (Rate's RATE ), so name the value after the keyword + # too: MAW's HEAD_LIMIT is head_limit. + rest = [(keyword, rest[0][1])] else: keyword = "_".join(_strip_record_words(arm_name)) rest = arm_fields @@ -621,8 +630,8 @@ def _build_arm_spec( nested_arm_classes: dict[str, list[str]] = {} for field_name, fld in rest: - if not isinstance(fld, UnionField): - continue + if not isinstance(fld, UnionField) or filters.is_column_union(fld): + continue # a column union is read as a column (see _item_columns) cache_key = tuple(sorted(fld.arms.keys())) cached = nested_union_cache.get(cache_key) if cached is None: @@ -951,6 +960,8 @@ def _generated_imports( typing_parts.append("Optional") if has_union: typing_parts.append("Union") + if any(col.uses_literal for col in _all_schema_cols): + typing_parts.append("Literal") if typing_parts: stdlib.append(f"from typing import {', '.join(sorted(typing_parts))}") diff --git a/pixi.lock b/pixi.lock index e5ac8873..df6ff02b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -106,7 +106,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -255,7 +255,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -404,7 +404,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -547,7 +547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -709,7 +709,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -987,7 +987,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1263,7 +1263,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1533,7 +1533,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1822,7 +1822,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2089,7 +2089,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2357,7 +2357,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2618,7 +2618,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2898,7 +2898,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3143,7 +3143,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3389,7 +3389,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz @@ -3629,7 +3629,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3887,7 +3887,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -4129,7 +4129,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -4372,7 +4372,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl @@ -4609,7 +4609,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl @@ -4863,7 +4863,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5107,7 +5107,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl @@ -5352,7 +5352,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5591,7 +5591,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - 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 @@ -10852,7 +10852,7 @@ packages: - build ; extra == 'build' - twine ; extra == 'build' requires_python: '>=3.11,<3.14' -- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#fbec2ab33b6332967c1cbcba84f8cd2a48052288 +- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed name: modflow-devtools version: 1.10.0.dev1 requires_dist: diff --git a/test/mf6/test_mf6_codegen.py b/test/mf6/test_mf6_codegen.py index 6ffa68a3..50033cba 100644 --- a/test/mf6/test_mf6_codegen.py +++ b/test/mf6/test_mf6_codegen.py @@ -22,7 +22,7 @@ import attrs import numpy as np import pytest -from modflow_devtools.dfns.schema import Array, Double, Integer, Keyword, Record, String +from modflow_devtools.dfns.schema import Array, Double, Integer, Keyword, Record, String, Union from flopy4.mf6.component import FNAMES from flopy4.mf6.utils.codegen.filters import ( @@ -31,6 +31,7 @@ can_expand_record, class_name, feature_id_column, + is_column_union, is_generatable, item_class, model_abbr, @@ -1496,3 +1497,82 @@ def test_counted_array_column(): ) assert (disv.ncpl, disv.nvert) == (1, 4) assert disv.cell2d[0].icvert == (0, 1, 2, 3) + + +def test_is_column_union(): + """A union is one column if each arm is one value (OBS's id, LAK's + bedleak), and a keystring if its arms are line forms (MAW's mawsetting, + LAK's laksetting, whose arms lead with a lake number).""" + value = Double(name="value", tagged=False) + assert is_column_union( + Union(name="bedleak", arms={"none": Keyword(name="none"), "value": value}) + ) + obs_id = Union( + name="id", + arms={ + "cellid": Array(name="cellid", dtype="integer", shape=["ncelldim"], tagged=False), + "boundname": String(name="boundname", tagged=False), + }, + ) + assert is_column_union(obs_id) + assert not is_column_union( + Union( + name="mawsetting", + arms={"status": String(name="status"), "bottom": Double(name="bottom")}, + ) + ) + lakeno = Integer(name="lakeno", tagged=False) + stage = Record( + name="stage", fields={"lakeno": lakeno, "stage": Keyword(name="stage"), "v": value} + ) + assert not is_column_union(Union(name="laksetting", arms={"stage": stage})) + + +def test_number_or_word_column(): + """A union column with a keyword arm types the word as a literal.""" + from flopy4.mf6.utils.codegen.filters import union_column + + col = union_column("bedleak", ("keyword", "double"), words=("none",)) + assert col.annotation == 'Union[Literal["NONE"], float]' + assert col.uses_literal + + +@pytest.mark.parametrize("bedleak", ["NONE", 0.1]) +def test_number_or_word_from_tokens(bedleak): + """LAK's bedleak, mid-row, is NONE (in any case) or a number.""" + from flopy4.mf6.gwf import Lak + + tokens = [1, 1, 1, 1, 1, "vertical", bedleak, 10.0, 20.0, 0.0, 0.0] + item = Lak.Connectiondata.from_tokens(tokens, ncelldim=3) + assert item.bedleak == bedleak + assert item.belev == 10.0 + assert item.to_tokens() == tuple(tokens) + lower = Lak.Connectiondata.from_tokens( + [str(t).lower() if t == "NONE" else t for t in tokens], ncelldim=3 + ) + assert lower.bedleak == bedleak + + +def test_number_or_word_in_keystring_arm(): + """MAW's HEAD_LIMIT takes OFF or a number.""" + from flopy4.mf6.gwf import Maw + + off = Maw.HeadLimit.from_tokens([1, "head_limit", "off"]) + assert (off.ifno, off.head_limit) == (0, "OFF") + limit = Maw.HeadLimit.from_tokens([1, "HEAD_LIMIT", 2.5]) + assert limit.head_limit == 2.5 + assert limit.to_tokens() == (1, "HEAD_LIMIT", 2.5) + + +def test_number_or_word_validated(): + """A number-or-word field takes its word or a number, and a string field + no longer takes a number.""" + from flopy4.mf6.gwf import Lak + from flopy4.mf6.record import _is_instance + + fields = attrs.fields_dict(Lak.Connectiondata) + assert _is_instance("NONE", fields["bedleak"]) + assert _is_instance("none", fields["bedleak"]) + assert _is_instance(0.1, fields["bedleak"]) + assert not _is_instance("OFF", fields["bedleak"]) + assert not _is_instance(1.0, fields["claktype"]) diff --git a/test/mf6/test_mf6_grammar_gen.py b/test/mf6/test_mf6_grammar_gen.py index 1e29765c..1acc87ac 100644 --- a/test/mf6/test_mf6_grammar_gen.py +++ b/test/mf6/test_mf6_grammar_gen.py @@ -444,3 +444,28 @@ def test_make_grammar_with_record_header(tmp_path): assert '!output__hdr: "fileout"i word [output__binary__kw]' in content assert 'output__binary__kw: "binary"i' in content assert "block_index" not in content + + +def test_make_grammar_prefixes_tagged_valid_strings(tmp_path): + """A tagged `valid` string is preceded by its name (NPF's + ALTERNATIVE_CELL_AVERAGING LOGARITHMIC), an untagged one isn't (STO's + STEADY-STATE); both read as a union of their words.""" + dfn = Package( + name="test-component", + blocks={ + "options": Block( + name="options", + fields={ + "averaging": String(name="averaging", valid=["logarithmic", "amt-lmk"]), + "storage": String( + name="storage", tagged=False, valid=["steady-state", "transient"] + ), + }, + ) + }, + ) + make_grammar(dfn, tmp_path) + content = (tmp_path / "test-component.lark").read_text() + assert '"averaging"i (' in content + assert '"storage"i' not in content + assert '"steady-state"i' in content From 58b314629d79cef2ee10367e033bd6d5d58cd9a2 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 13:40:23 -0700 Subject: [PATCH 02/10] call tagged valid strings tagged in the grammar, not prefixed Co-Authored-By: Claude Opus 5.5 --- flopy4/mf6/codec/reader/grammar/__init__.py | 10 ++++++---- .../reader/grammar/templates/component.lark.jinja | 2 +- flopy4/mf6/codec/reader/grammar/templates/macros.jinja | 10 +++++----- test/mf6/test_mf6_grammar_gen.py | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/flopy4/mf6/codec/reader/grammar/__init__.py b/flopy4/mf6/codec/reader/grammar/__init__.py index e8252d71..845b5d7f 100644 --- a/flopy4/mf6/codec/reader/grammar/__init__.py +++ b/flopy4/mf6/codec/reader/grammar/__init__.py @@ -63,14 +63,16 @@ def _to_context(blocks: dict[str, Block]) -> tuple[list[dict], dict[str, object] standalone_fields = [] # a list's element lines, each read into the list (see names.py) elements = set() - # tagged `valid` scalars, whose name precedes their value - prefixed = set() + # tagged `valid` scalars, whose name precedes their value. The union + # valid_as_union makes of one is tagged by its keyword arms, so it + # can't say whether the scalar was. + tagged_valid = set() for field_name, field in block.fields.items(): if field.removed: continue if field.tagged and getattr(field, "valid", None): - prefixed.add(field_name) + tagged_valid.add(field_name) field = filters.valid_as_union(field) if isinstance(field, List): if isinstance(field.item, Union): @@ -111,7 +113,7 @@ def _to_context(blocks: dict[str, Block]) -> tuple[list[dict], dict[str, object] "header": header.name if isinstance(header, Record) else None, "standalone_fields": standalone_fields, "rules": rules, - "prefixed": prefixed, + "tagged_valid": tagged_valid, "recarrays": recarrays, "redirect": not bare_array, } diff --git a/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja b/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja index 655bf658..7225738c 100644 --- a/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja +++ b/flopy4/mf6/codec/reader/grammar/templates/component.lark.jinja @@ -70,7 +70,7 @@ double_index: block_index {% elif field.type == 'record' and field.children is not none -%} {{ m.record_field(field_name, field, name) }} {% elif field.type == 'union' and field.children is not none -%} -{{ m.union_field(field_name, field, name, field_name in block.prefixed) }} +{{ m.union_field(field_name, field, name, field_name in block.tagged_valid) }} {% elif field.type == 'file' -%} {{ m.file_field(name, field) }} {% else -%} diff --git a/flopy4/mf6/codec/reader/grammar/templates/macros.jinja b/flopy4/mf6/codec/reader/grammar/templates/macros.jinja index 7366da94..f0b4fcb7 100644 --- a/flopy4/mf6/codec/reader/grammar/templates/macros.jinja +++ b/flopy4/mf6/codec/reader/grammar/templates/macros.jinja @@ -119,17 +119,17 @@ {%- endfor -%} {%- endmacro %} -{% macro union_field(field_name, field, name, prefixed=false) -%} +{% macro union_field(field_name, field, name, tagged=false) -%} {# The alternation must be parenthesized before appending `[_remark] _NL` -- `|` binds looser than concatenation, so an unparenthesized "A | B | C [_remark] _NL" only requires the line ending after the *last* arm. A - tagged `valid` scalar made a union (`prefixed`, e.g. NPF's - alternative_cell_averaging, via valid_as_union) additionally needs its - own field-name keyword prefixed -- real files write + tagged `valid` scalar made a union (`tagged`, e.g. NPF's + alternative_cell_averaging, via valid_as_union) also begins with its + own name -- real files write "ALTERNATIVE_CELL_AVERAGING LOGARITHMIC", not a bare "LOGARITHMIC" -- unlike a keystring (e.g. ocsetting's ALL/FIRST/LAST), where the chosen value's own keyword is the whole field. #} -{{ name }}: {% if prefixed %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL +{{ name }}: {% if tagged %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL {{ union_arms(field_name, field) }} {%- endmacro %} diff --git a/test/mf6/test_mf6_grammar_gen.py b/test/mf6/test_mf6_grammar_gen.py index 1acc87ac..897917ce 100644 --- a/test/mf6/test_mf6_grammar_gen.py +++ b/test/mf6/test_mf6_grammar_gen.py @@ -446,7 +446,7 @@ def test_make_grammar_with_record_header(tmp_path): assert "block_index" not in content -def test_make_grammar_prefixes_tagged_valid_strings(tmp_path): +def test_make_grammar_names_tagged_valid_strings(tmp_path): """A tagged `valid` string is preceded by its name (NPF's ALTERNATIVE_CELL_AVERAGING LOGARITHMIC), an untagged one isn't (STO's STEADY-STATE); both read as a union of their words.""" From d6c0b09e9c8e828a0673b5a0b60a737f2859d157 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 14:12:45 -0700 Subject: [PATCH 03/10] name a lone value after its keyword only in union arms A tagged list's record (SCALE ) keeps its value's name. Co-Authored-By: Claude Opus 5.5 --- flopy4/mf6/utils/codegen/make.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index 05e6d375..50f2dc57 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -611,10 +611,12 @@ def _build_arm_spec( if kw_idx is not None: keyword = arm_fields[kw_idx][0] rest = arm_fields[:kw_idx] + arm_fields[kw_idx + 1 :] - if kw_idx == 0 and len(rest) == 1: - # A keyword and one value reads like a tagged scalar arm - # (Rate's RATE ), so name the value after the keyword - # too: MAW's HEAD_LIMIT is head_limit. + if kw_idx == 0 and len(rest) == 1 and not class_name: + # A union arm of a keyword and one value reads like a tagged + # scalar arm (Rate's RATE ), so name the value after + # the keyword too: MAW's HEAD_LIMIT is + # head_limit. A tagged list's record (named by the caller) + # keeps its value's name. rest = [(keyword, rest[0][1])] else: keyword = "_".join(_strip_record_words(arm_name)) From 6f7579ec74abde4e2a974e7bbf633314db2966d8 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 14:29:03 -0700 Subject: [PATCH 04/10] type union arms one by one and mirror the DFN's list elements - A union arm types by its kind: a keyword is a literal word, an untagged value its own type, and a record or tagged value a class. OC's ocsetting is `Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps`; `Oc.All`, `Oc.First` and `Oc.Last` are gone. This replaces `is_column_union`. - A list's element class mirrors the DFN's element. A record with a union field (LKT, LKE, MAW, SFR, SPC, TVK, TVS) is one row class holding the index and the union, not one class per arm with the index copied in: `Lkt.StressPeriodData(ifno=0, laksetting=Lkt.Status(status="active"))`. Flat tuples like `(0, "STATUS", "ACTIVE")` still work. LAK is wrapped too, until devtools makes its element the union itself, as PRP's is. - A row's children include those in its union arm (SFR's CROSS_SECTION TAB6 file). - Static list columns no longer take a block prefix for sharing a name with a period setting keyword; period settings aren't package attributes, so nothing collided. - Drop the "keystring" term (v1's union type) from code, docs and tests. Co-Authored-By: Claude Opus 5.5 --- docs/dev/sdd.md | 4 +- docs/examples/quickstart.py | 2 +- .../reader/grammar/templates/macros.jinja | 4 +- flopy4/mf6/codec/writer/filters.py | 4 +- flopy4/mf6/component.py | 13 +- flopy4/mf6/gwe/ctp.py | 2 +- flopy4/mf6/gwe/disu.py | 2 +- flopy4/mf6/gwe/disv.py | 2 +- flopy4/mf6/gwe/esl.py | 2 +- flopy4/mf6/gwe/lke.py | 19 +- flopy4/mf6/gwe/oc.py | 21 +- flopy4/mf6/gwf/chd.py | 2 +- flopy4/mf6/gwf/disu.py | 2 +- flopy4/mf6/gwf/disv.py | 2 +- flopy4/mf6/gwf/drn.py | 2 +- flopy4/mf6/gwf/evt.py | 8 +- flopy4/mf6/gwf/ghb.py | 2 +- flopy4/mf6/gwf/lak.py | 23 +- flopy4/mf6/gwf/maw.py | 31 +-- flopy4/mf6/gwf/oc.py | 21 +- flopy4/mf6/gwf/rch.py | 2 +- flopy4/mf6/gwf/riv.py | 2 +- flopy4/mf6/gwf/sfr.py | 37 +-- flopy4/mf6/gwf/uzf.py | 2 +- flopy4/mf6/gwf/wel.py | 2 +- flopy4/mf6/gwt/cnc.py | 2 +- flopy4/mf6/gwt/disu.py | 2 +- flopy4/mf6/gwt/disv.py | 2 +- flopy4/mf6/gwt/lkt.py | 19 +- flopy4/mf6/gwt/oc.py | 21 +- flopy4/mf6/gwt/src.py | 2 +- flopy4/mf6/item.py | 171 +++++++----- flopy4/mf6/package.py | 8 +- flopy4/mf6/prt/disv.py | 2 +- flopy4/mf6/prt/oc.py | 21 +- flopy4/mf6/record.py | 7 +- flopy4/mf6/utils/codegen/filters.py | 119 +++------ flopy4/mf6/utils/codegen/make.py | 243 +++++++++--------- flopy4/mf6/utl/obs.py | 2 +- flopy4/mf6/utl/spc.py | 10 +- flopy4/mf6/utl/tvk.py | 11 +- flopy4/mf6/utl/tvs.py | 10 +- test/mf6/test_mf6_codec.py | 119 +++++---- test/mf6/test_mf6_codegen.py | 92 +++---- 44 files changed, 472 insertions(+), 604 deletions(-) diff --git a/docs/dev/sdd.md b/docs/dev/sdd.md index dc45994e..0dc1604e 100644 --- a/docs/dev/sdd.md +++ b/docs/dev/sdd.md @@ -187,8 +187,8 @@ A top-level-template `blocks.jinja` iterates over blocks, calling field macros d - **Scalars**: keywords, integers, floats, strings - **Records**: tuples of values (e.g., file specifications, cell IDs with values) - **Arrays**: numeric arrays with control records (`CONSTANT`, `INTERNAL`, `OPEN/CLOSE`) -- **Lists**: stress period data, either tabular or keystring format -- **Keystrings**: option records with keyword-value pairs +- **Lists**: stress period data, rows of records or unions of keyword-led settings +- **Unions**: one of several alternatives, e.g. a keyword-led setting Custom Jinja filters in `flopy4.mf6.codec.writer.filters` implement field-specific logic. diff --git a/docs/examples/quickstart.py b/docs/examples/quickstart.py index b265d027..a87f6728 100644 --- a/docs/examples/quickstart.py +++ b/docs/examples/quickstart.py @@ -111,7 +111,7 @@ assert chd.stress_period_data[0][1].head == 0.0 assert gwf.dis.to_dataarray("botm").sel(layer=0, y=0, x=0) == 0.0 assert gwf.oc.stress_period_data[0][0].rtype == "HEAD" -assert isinstance(gwf.oc.stress_period_data[0][0].ocsetting, Oc.All) +assert gwf.oc.stress_period_data[0][0].ocsetting == "ALL" # ### Read results # diff --git a/flopy4/mf6/codec/reader/grammar/templates/macros.jinja b/flopy4/mf6/codec/reader/grammar/templates/macros.jinja index f0b4fcb7..234173fd 100644 --- a/flopy4/mf6/codec/reader/grammar/templates/macros.jinja +++ b/flopy4/mf6/codec/reader/grammar/templates/macros.jinja @@ -127,8 +127,8 @@ alternative_cell_averaging, via valid_as_union) also begins with its own name -- real files write "ALTERNATIVE_CELL_AVERAGING LOGARITHMIC", not a bare "LOGARITHMIC" -- - unlike a keystring (e.g. ocsetting's ALL/FIRST/LAST), where the chosen - value's own keyword is the whole field. #} + unlike a union of keywords (e.g. ocsetting's ALL/FIRST/LAST), where the + chosen value's own keyword is the whole field. #} {{ name }}: {% if tagged %}"{{ field.name }}"i {% endif %}({{ union_alternation(field_name, field) }}) [_remark] _NL {{ union_arms(field_name, field) }} diff --git a/flopy4/mf6/codec/writer/filters.py b/flopy4/mf6/codec/writer/filters.py index d0a9e89c..68bdb4ca 100644 --- a/flopy4/mf6/codec/writer/filters.py +++ b/flopy4/mf6/codec/writer/filters.py @@ -346,8 +346,8 @@ def dataset2list(value: xr.Dataset): Yield records (tuples) from an `xarray.Dataset`. If the first data variable is a string type, assume all are - string type. Then the dataset represents a keystring; yield - tuples of (name, *value). Otherwise, yield tuples: (*value) + string type. Then the dataset represents keyword settings; + yield tuples of (name, *value). Otherwise, yield tuples: (*value) if no spatial dimensions, or (*cellid, *value) when spatial dimensions are present. """ diff --git a/flopy4/mf6/component.py b/flopy4/mf6/component.py index 22c9ceea..06d3274a 100644 --- a/flopy4/mf6/component.py +++ b/flopy4/mf6/component.py @@ -137,11 +137,18 @@ def _resolve_child_name( def _row_children(row: Any) -> "list[Component]": - """The children in a list's row (LAK's ``ifno TAB6 FILEIN ``).""" + """The children in a list's row (LAK's ``ifno TAB6 FILEIN ``), + including in its union arm (SFR's ``CROSS_SECTION TAB6 FILEIN``).""" if isinstance(row, Component) or not attrs.has(type(row)): return [] - children = (getattr(row, f.name) for f in fields(type(row)) if f.metadata.get("child")) - return [c for c in children if isinstance(c, Component)] + children = [] + for f in fields(type(row)): + value = getattr(row, f.name) + if f.metadata.get("child"): + children += [value] if isinstance(value, Component) else [] + elif attrs.has(type(value)): + children += _row_children(value) + return children def _period_row_children(value: Any) -> "list[Component]": diff --git a/flopy4/mf6/gwe/ctp.py b/flopy4/mf6/gwe/ctp.py index 1073ee07..2e264f25 100644 --- a/flopy4/mf6/gwe/ctp.py +++ b/flopy4/mf6/gwe/ctp.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): temp: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwe/disu.py b/flopy4/mf6/gwe/disu.py index f079649d..f6c68e8e 100644 --- a/flopy4/mf6/gwe/disu.py +++ b/flopy4/mf6/gwe/disu.py @@ -33,7 +33,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwe/disv.py b/flopy4/mf6/gwe/disv.py index 6c9c2a3a..fba71e02 100644 --- a/flopy4/mf6/gwe/disv.py +++ b/flopy4/mf6/gwe/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwe/esl.py b/flopy4/mf6/gwe/esl.py index 142f1c6b..4ef59df3 100644 --- a/flopy4/mf6/gwe/esl.py +++ b/flopy4/mf6/gwe/esl.py @@ -30,7 +30,7 @@ class StressPeriodData(Item): senerrate: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwe/lke.py b/flopy4/mf6/gwe/lke.py index a202bda2..e4518857 100644 --- a/flopy4/mf6/gwe/lke.py +++ b/flopy4/mf6/gwe/lke.py @@ -44,55 +44,49 @@ class Packagedata(Item): rbthcnd: float = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) @attrs.define class Status(Item): _keyword: ClassVar[str] = "status" - lakeno: int = field(index=True, fk="packagedata.lakeno") status: str = field() @attrs.define class Temperature(Item): _keyword: ClassVar[str] = "temperature" - lakeno: int = field(index=True, fk="packagedata.lakeno") temperature: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Rainfall(Item): _keyword: ClassVar[str] = "rainfall" - lakeno: int = field(index=True, fk="packagedata.lakeno") rainfall: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Evaporation(Item): _keyword: ClassVar[str] = "evaporation" - lakeno: int = field(index=True, fk="packagedata.lakeno") evaporation: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Runoff(Item): _keyword: ClassVar[str] = "runoff" - lakeno: int = field(index=True, fk="packagedata.lakeno") runoff: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class ExtInflow(Item): _keyword: ClassVar[str] = "ext-inflow" - lakeno: int = field(index=True, fk="packagedata.lakeno") ext_inflow: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Auxiliary(Item): _keyword: ClassVar[str] = "auxiliary" - lakeno: int = field(index=True, fk="packagedata.lakeno") auxname: str = field() auxval: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = ( - Status | Temperature | Rainfall | Evaporation | Runoff | ExtInflow | Auxiliary - ) + @attrs.define + class StressPeriodData(Item): + lakeno: int = field(index=True, fk="packagedata.lakeno") + laksetting: "Lke.Status | Lke.Temperature | Lke.Rainfall | Lke.Evaporation | Lke.Runoff | Lke.ExtInflow | Lke.Auxiliary" = field() # noqa: E501 flow_package_name: Optional[str] = field( default=None, @@ -176,7 +170,7 @@ class Auxiliary(Item): block="packagedata", write_if_empty=True, ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -193,3 +187,4 @@ class Auxiliary(Item): LkeRunoff = Lke.Runoff LkeExtInflow = Lke.ExtInflow LkeAuxiliary = Lke.Auxiliary +LkeStressPeriodData = Lke.StressPeriodData diff --git a/flopy4/mf6/gwe/oc.py b/flopy4/mf6/gwe/oc.py index 06f659e3..40182364 100644 --- a/flopy4/mf6/gwe/oc.py +++ b/flopy4/mf6/gwe/oc.py @@ -1,6 +1,6 @@ # autogenerated file, do not modify from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar, Literal, Optional import attrs @@ -28,18 +28,6 @@ class Temperatureprint(Record): _extra_tokens: ClassVar[tuple[str, ...]] = ("PRINT_FORMAT",) formatrecord: "Oc.Format" = attrs.field() - @attrs.define - class All(Item): - _keyword: ClassVar[str] = "all" - - @attrs.define - class First(Item): - _keyword: ClassVar[str] = "first" - - @attrs.define - class Last(Item): - _keyword: ClassVar[str] = "last" - @attrs.define class Frequency(Item): _keyword: ClassVar[str] = "frequency" @@ -54,13 +42,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() _StressPeriodDataItem = Save | Print @@ -102,9 +90,6 @@ class Print(Item): ) -OcAll = Oc.All -OcFirst = Oc.First -OcLast = Oc.Last OcFrequency = Oc.Frequency OcSteps = Oc.Steps OcSave = Oc.Save diff --git a/flopy4/mf6/gwf/chd.py b/flopy4/mf6/gwf/chd.py index e8a097d0..09507d3a 100644 --- a/flopy4/mf6/gwf/chd.py +++ b/flopy4/mf6/gwf/chd.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): head: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/disu.py b/flopy4/mf6/gwf/disu.py index df6a092b..5f170b20 100644 --- a/flopy4/mf6/gwf/disu.py +++ b/flopy4/mf6/gwf/disu.py @@ -33,7 +33,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwf/disv.py b/flopy4/mf6/gwf/disv.py index 3062693a..b7366eb4 100644 --- a/flopy4/mf6/gwf/disv.py +++ b/flopy4/mf6/gwf/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwf/drn.py b/flopy4/mf6/gwf/drn.py index 82c97b38..159b89d7 100644 --- a/flopy4/mf6/gwf/drn.py +++ b/flopy4/mf6/gwf/drn.py @@ -31,7 +31,7 @@ class StressPeriodData(Item): cond: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/evt.py b/flopy4/mf6/gwf/evt.py index 2250df0f..048f0b18 100644 --- a/flopy4/mf6/gwf/evt.py +++ b/flopy4/mf6/gwf/evt.py @@ -31,16 +31,16 @@ class StressPeriodData(Item): depth: Union[float, TimeSeriesRef] = field(time_series=True) pxdp: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("nseg-1",), time_series=True, optional=True - ) + ) # noqa: E501 petm: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("nseg-1",), time_series=True, optional=True - ) + ) # noqa: E501 petm0: Optional[Union[float, TimeSeriesRef]] = field( default=None, time_series=True, optional=True - ) + ) # noqa: E501 aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) fixed_cell: bool = field( diff --git a/flopy4/mf6/gwf/ghb.py b/flopy4/mf6/gwf/ghb.py index 159efc6e..389483b7 100644 --- a/flopy4/mf6/gwf/ghb.py +++ b/flopy4/mf6/gwf/ghb.py @@ -31,7 +31,7 @@ class StressPeriodData(Item): cond: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/lak.py b/flopy4/mf6/gwf/lak.py index da359eab..bbeea4e6 100644 --- a/flopy4/mf6/gwf/lak.py +++ b/flopy4/mf6/gwf/lak.py @@ -50,7 +50,7 @@ class Packagedata(Item): nlakeconn: int = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) @attrs.define @@ -160,21 +160,9 @@ class Auxiliary(Item): auxname: str = field() auxval: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = ( - Status - | Stage - | Rainfall - | Evaporation - | Runoff - | Inflow - | Withdrawal - | Rate - | Invert - | Width - | Slope - | Rough - | Auxiliary - ) + @attrs.define + class StressPeriodData(Item): + laksetting: "Lak.Status | Lak.Stage | Lak.Rainfall | Lak.Evaporation | Lak.Runoff | Lak.Inflow | Lak.Withdrawal | Lak.Rate | Lak.Invert | Lak.Width | Lak.Slope | Lak.Rough | Lak.Auxiliary" = field() # noqa: E501 auxiliary: Optional[NDArray[np.str_]] = field( default=None, @@ -325,7 +313,7 @@ class Auxiliary(Item): block="outlets", dim="noutlets", ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -351,3 +339,4 @@ class Auxiliary(Item): LakSlope = Lak.Slope LakRough = Lak.Rough LakAuxiliary = Lak.Auxiliary +LakStressPeriodData = Lak.StressPeriodData diff --git a/flopy4/mf6/gwf/maw.py b/flopy4/mf6/gwf/maw.py index f624f1bc..34dfcd7a 100644 --- a/flopy4/mf6/gwf/maw.py +++ b/flopy4/mf6/gwf/maw.py @@ -44,7 +44,7 @@ class Packagedata(Item): ngwfnodes: int = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) @attrs.define @@ -67,26 +67,22 @@ class Angledata(Item): @attrs.define class Status(Item): _keyword: ClassVar[str] = "status" - ifno: int = field(index=True, fk="packagedata.ifno") status: str = field() @attrs.define class ConnectionStatus(Item): _keyword: ClassVar[str] = "connection_status" - ifno: int = field(index=True, fk="packagedata.ifno") icon: int = field(index=True) connstatus: str = field() @attrs.define class Bottom(Item): _keyword: ClassVar[str] = "bottom" - ifno: int = field(index=True, fk="packagedata.ifno") bottom: float = field() @attrs.define class FlowingWell(Item): _keyword: ClassVar[str] = "flowing_well" - ifno: int = field(index=True, fk="packagedata.ifno") fwelev: float = field() fwcond: float = field() fwrlen: float = field() @@ -94,54 +90,40 @@ class FlowingWell(Item): @attrs.define class Rate(Item): _keyword: ClassVar[str] = "rate" - ifno: int = field(index=True, fk="packagedata.ifno") rate: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class WellHead(Item): _keyword: ClassVar[str] = "well_head" - ifno: int = field(index=True, fk="packagedata.ifno") well_head: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class HeadLimit(Item): _keyword: ClassVar[str] = "head_limit" - ifno: int = field(index=True, fk="packagedata.ifno") head_limit: Union[Literal["OFF"], float] = field(union=("keyword", "double")) @attrs.define class Shutoff(Item): _keyword: ClassVar[str] = "shut_off" - ifno: int = field(index=True, fk="packagedata.ifno") minrate: float = field() maxrate: float = field() @attrs.define class RateScaling(Item): _keyword: ClassVar[str] = "rate_scaling" - ifno: int = field(index=True, fk="packagedata.ifno") pump_elevation: float = field() scaling_length: float = field() @attrs.define class Auxiliary(Item): _keyword: ClassVar[str] = "auxiliary" - ifno: int = field(index=True, fk="packagedata.ifno") auxname: str = field() auxval: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = ( - Status - | ConnectionStatus - | Bottom - | FlowingWell - | Rate - | WellHead - | HeadLimit - | Shutoff - | RateScaling - | Auxiliary - ) + @attrs.define + class StressPeriodData(Item): + ifno: int = field(index=True, fk="packagedata.ifno") + mawsetting: "Maw.Status | Maw.ConnectionStatus | Maw.Bottom | Maw.FlowingWell | Maw.Rate | Maw.WellHead | Maw.HeadLimit | Maw.Shutoff | Maw.RateScaling | Maw.Auxiliary" = field() # noqa: E501 auxiliary: Optional[NDArray[np.str_]] = field( default=None, @@ -276,7 +258,7 @@ class Auxiliary(Item): default=None, block="angledata", ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -298,3 +280,4 @@ class Auxiliary(Item): MawShutoff = Maw.Shutoff MawRateScaling = Maw.RateScaling MawAuxiliary = Maw.Auxiliary +MawStressPeriodData = Maw.StressPeriodData diff --git a/flopy4/mf6/gwf/oc.py b/flopy4/mf6/gwf/oc.py index dc08c210..e46a14ba 100644 --- a/flopy4/mf6/gwf/oc.py +++ b/flopy4/mf6/gwf/oc.py @@ -1,6 +1,6 @@ # autogenerated file, do not modify from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar, Literal, Optional import attrs @@ -28,18 +28,6 @@ class Headprint(Record): _extra_tokens: ClassVar[tuple[str, ...]] = ("PRINT_FORMAT",) formatrecord: "Oc.Format" = attrs.field() - @attrs.define - class All(Item): - _keyword: ClassVar[str] = "all" - - @attrs.define - class First(Item): - _keyword: ClassVar[str] = "first" - - @attrs.define - class Last(Item): - _keyword: ClassVar[str] = "last" - @attrs.define class Frequency(Item): _keyword: ClassVar[str] = "frequency" @@ -54,13 +42,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() _StressPeriodDataItem = Save | Print @@ -102,9 +90,6 @@ class Print(Item): ) -OcAll = Oc.All -OcFirst = Oc.First -OcLast = Oc.Last OcFrequency = Oc.Frequency OcSteps = Oc.Steps OcSave = Oc.Save diff --git a/flopy4/mf6/gwf/rch.py b/flopy4/mf6/gwf/rch.py index ee3e5558..413f9648 100644 --- a/flopy4/mf6/gwf/rch.py +++ b/flopy4/mf6/gwf/rch.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): recharge: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) fixed_cell: bool = field( diff --git a/flopy4/mf6/gwf/riv.py b/flopy4/mf6/gwf/riv.py index 9a20ab7a..10f5a2f6 100644 --- a/flopy4/mf6/gwf/riv.py +++ b/flopy4/mf6/gwf/riv.py @@ -32,7 +32,7 @@ class StressPeriodData(Item): rbot: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/sfr.py b/flopy4/mf6/gwf/sfr.py index 927190e3..e28b850d 100644 --- a/flopy4/mf6/gwf/sfr.py +++ b/flopy4/mf6/gwf/sfr.py @@ -57,7 +57,7 @@ class Packagedata(Item): ndv: int = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) @attrs.define @@ -75,7 +75,7 @@ class Connectiondata(Item): shape=("packagedata.ncon(ifno)",), signed=True, optional=True, - ) + ) # noqa: E501 @attrs.define class Diversions(Item): @@ -92,91 +92,69 @@ class Initialstages(Item): @attrs.define class Status(Item): _keyword: ClassVar[str] = "status" - ifno: int = field(index=True, fk="packagedata.ifno") status: str = field() @attrs.define class Bedk(Item): _keyword: ClassVar[str] = "bedk" - ifno: int = field(index=True, fk="packagedata.ifno") bedk: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Manning(Item): _keyword: ClassVar[str] = "manning" - ifno: int = field(index=True, fk="packagedata.ifno") manning: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Stage(Item): _keyword: ClassVar[str] = "stage" - ifno: int = field(index=True, fk="packagedata.ifno") stage: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Inflow(Item): _keyword: ClassVar[str] = "inflow" - ifno: int = field(index=True, fk="packagedata.ifno") inflow: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Rainfall(Item): _keyword: ClassVar[str] = "rainfall" - ifno: int = field(index=True, fk="packagedata.ifno") rainfall: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Evaporation(Item): _keyword: ClassVar[str] = "evaporation" - ifno: int = field(index=True, fk="packagedata.ifno") evaporation: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Runoff(Item): _keyword: ClassVar[str] = "runoff" - ifno: int = field(index=True, fk="packagedata.ifno") runoff: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Diversion(Item): _keyword: ClassVar[str] = "diversion" - ifno: int = field(index=True, fk="packagedata.ifno") idv: int = field(index=True) divflow: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class UpstreamFraction(Item): _keyword: ClassVar[str] = "upstream_fraction" - ifno: int = field(index=True, fk="packagedata.ifno") upstream_fraction: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class CrossSection(Item): _keyword: ClassVar[str] = "cross_section" - ifno: int = field(index=True, fk="packagedata.ifno") sfrtab: Sfrtab = child(keyword="tab6", direction="in", default=attrs.NOTHING) @attrs.define class Auxiliary(Item): _keyword: ClassVar[str] = "auxiliary" - ifno: int = field(index=True, fk="packagedata.ifno") auxname: str = field() auxval: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = ( - Status - | Bedk - | Manning - | Stage - | Inflow - | Rainfall - | Evaporation - | Runoff - | Diversion - | UpstreamFraction - | CrossSection - | Auxiliary - ) + @attrs.define + class StressPeriodData(Item): + ifno: int = field(index=True, fk="packagedata.ifno") + sfrsetting: "Sfr.Status | Sfr.Bedk | Sfr.Manning | Sfr.Stage | Sfr.Inflow | Sfr.Rainfall | Sfr.Evaporation | Sfr.Runoff | Sfr.Diversion | Sfr.UpstreamFraction | Sfr.CrossSection | Sfr.Auxiliary" = field() # noqa: E501 storage: bool = field( default=False, @@ -332,7 +310,7 @@ class Auxiliary(Item): block="initialstages", dim="nreaches", ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -358,3 +336,4 @@ class Auxiliary(Item): SfrUpstreamFraction = Sfr.UpstreamFraction SfrCrossSection = Sfr.CrossSection SfrAuxiliary = Sfr.Auxiliary +SfrStressPeriodData = Sfr.StressPeriodData diff --git a/flopy4/mf6/gwf/uzf.py b/flopy4/mf6/gwf/uzf.py index 9476e2ad..993135a7 100644 --- a/flopy4/mf6/gwf/uzf.py +++ b/flopy4/mf6/gwf/uzf.py @@ -61,7 +61,7 @@ class StressPeriodData(Item): rootact: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 auxiliary: Optional[NDArray[np.str_]] = field( default=None, diff --git a/flopy4/mf6/gwf/wel.py b/flopy4/mf6/gwf/wel.py index 6a8d3281..dfdd6c2f 100644 --- a/flopy4/mf6/gwf/wel.py +++ b/flopy4/mf6/gwf/wel.py @@ -32,7 +32,7 @@ class StressPeriodData(Item): q: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwt/cnc.py b/flopy4/mf6/gwt/cnc.py index 4729ceeb..2062b4fd 100644 --- a/flopy4/mf6/gwt/cnc.py +++ b/flopy4/mf6/gwt/cnc.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): conc: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwt/disu.py b/flopy4/mf6/gwt/disu.py index 46f4ed17..d12489df 100644 --- a/flopy4/mf6/gwt/disu.py +++ b/flopy4/mf6/gwt/disu.py @@ -33,7 +33,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwt/disv.py b/flopy4/mf6/gwt/disv.py index 57aa9c2c..812c52af 100644 --- a/flopy4/mf6/gwt/disv.py +++ b/flopy4/mf6/gwt/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwt/lkt.py b/flopy4/mf6/gwt/lkt.py index a867ce82..624e1386 100644 --- a/flopy4/mf6/gwt/lkt.py +++ b/flopy4/mf6/gwt/lkt.py @@ -42,55 +42,49 @@ class Packagedata(Item): strt: float = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) @attrs.define class Status(Item): _keyword: ClassVar[str] = "status" - ifno: int = field(index=True, fk="packagedata.ifno") status: str = field() @attrs.define class Concentration(Item): _keyword: ClassVar[str] = "concentration" - ifno: int = field(index=True, fk="packagedata.ifno") concentration: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Rainfall(Item): _keyword: ClassVar[str] = "rainfall" - ifno: int = field(index=True, fk="packagedata.ifno") rainfall: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Evaporation(Item): _keyword: ClassVar[str] = "evaporation" - ifno: int = field(index=True, fk="packagedata.ifno") evaporation: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Runoff(Item): _keyword: ClassVar[str] = "runoff" - ifno: int = field(index=True, fk="packagedata.ifno") runoff: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class ExtInflow(Item): _keyword: ClassVar[str] = "ext-inflow" - ifno: int = field(index=True, fk="packagedata.ifno") ext_inflow: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Auxiliary(Item): _keyword: ClassVar[str] = "auxiliary" - ifno: int = field(index=True, fk="packagedata.ifno") auxname: str = field() auxval: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = ( - Status | Concentration | Rainfall | Evaporation | Runoff | ExtInflow | Auxiliary - ) + @attrs.define + class StressPeriodData(Item): + ifno: int = field(index=True, fk="packagedata.ifno") + laksetting: "Lkt.Status | Lkt.Concentration | Lkt.Rainfall | Lkt.Evaporation | Lkt.Runoff | Lkt.ExtInflow | Lkt.Auxiliary" = field() # noqa: E501 flow_package_name: Optional[str] = field( default=None, @@ -174,7 +168,7 @@ class Auxiliary(Item): block="packagedata", write_if_empty=True, ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -191,3 +185,4 @@ class Auxiliary(Item): LktRunoff = Lkt.Runoff LktExtInflow = Lkt.ExtInflow LktAuxiliary = Lkt.Auxiliary +LktStressPeriodData = Lkt.StressPeriodData diff --git a/flopy4/mf6/gwt/oc.py b/flopy4/mf6/gwt/oc.py index 78ae8eec..877fc678 100644 --- a/flopy4/mf6/gwt/oc.py +++ b/flopy4/mf6/gwt/oc.py @@ -1,6 +1,6 @@ # autogenerated file, do not modify from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar, Literal, Optional import attrs @@ -28,18 +28,6 @@ class Concentrationprint(Record): _extra_tokens: ClassVar[tuple[str, ...]] = ("PRINT_FORMAT",) formatrecord: "Oc.Format" = attrs.field() - @attrs.define - class All(Item): - _keyword: ClassVar[str] = "all" - - @attrs.define - class First(Item): - _keyword: ClassVar[str] = "first" - - @attrs.define - class Last(Item): - _keyword: ClassVar[str] = "last" - @attrs.define class Frequency(Item): _keyword: ClassVar[str] = "frequency" @@ -54,13 +42,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() _StressPeriodDataItem = Save | Print @@ -102,9 +90,6 @@ class Print(Item): ) -OcAll = Oc.All -OcFirst = Oc.First -OcLast = Oc.Last OcFrequency = Oc.Frequency OcSteps = Oc.Steps OcSave = Oc.Save diff --git a/flopy4/mf6/gwt/src.py b/flopy4/mf6/gwt/src.py index dd3729d8..8a9ce753 100644 --- a/flopy4/mf6/gwt/src.py +++ b/flopy4/mf6/gwt/src.py @@ -30,7 +30,7 @@ class StressPeriodData(Item): smassrate: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) + ) # noqa: E501 boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/item.py b/flopy4/mf6/item.py index 3dfc0903..34c52d7d 100644 --- a/flopy4/mf6/item.py +++ b/flopy4/mf6/item.py @@ -9,12 +9,13 @@ tagged field here is always a bare presence flag (e.g. LAK tables' MIXED) -- unlike Record's tagged fields, which may carry a value. -An Item class MAY declare ``_keyword: ClassVar[str]`` for the -keystring-union-arm case (e.g. LAK/SFR/MAW/UZF period settings): several -Item subclasses share one field (a Union of their types), each identified -by its own leading keyword token (STATUS/STAGE/RATE/...). +An Item class MAY declare ``_keyword: ClassVar[str]`` when it's one arm +of a union (OC's Save/Print, LKT's Status/Concentration/...): several Item +subclasses share one list or field (a Union of their types), each +identified by its own keyword token (SAVE/PRINT, STATUS/CONCENTRATION/...). """ +import ast from collections.abc import Callable, Mapping from functools import lru_cache from pathlib import Path @@ -131,23 +132,56 @@ def _has_boundname_field(cls: type) -> bool: @lru_cache(maxsize=None) -def _nested_union_classes(cls: type, type_str: str) -> "tuple[type[Item], ...] | None": - """If a field's raw type annotation is a `` | ``-joined forward - reference to sibling Item classes (e.g. ``"Oc.All | Oc.First | ..."``, - see make.py's _build_arm_specs_from_union), return the tuple of - resolved classes; else None. Resolvability is itself the signal, same - as record.py's _nested_class. +def _union_arms( + cls: type, type_str: str +) -> "tuple[tuple[str, ...], tuple[type[Item], ...]] | None": + """If a field's raw type annotation is a `` | ``-joined union of words + and forward references to sibling Item classes (OC's ocsetting, + ``"Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps"``, see + make.py's _union_arm_specs), return its words and resolved classes; + else None. Resolvability is itself the signal, same as record.py's + _nested_class. Cached since to_tokens/from_tokens call this per field, often repeatedly while parsing many rows. """ - names = [part.strip().rsplit(".", 1)[-1] for part in type_str.split(" | ")] - if len(names) < 2: + parts = type_str.split(" | ") + if len(parts) < 2: return None - resolved = [_resolve_sibling_class(cls, name) for name in names] - if any(not (isinstance(r, type) and issubclass(r, Item)) for r in resolved): - return None - return tuple(cast("list[type[Item]]", resolved)) + words: list[str] = [] + classes: list[type[Item]] = [] + for part in parts: + if part.startswith("Literal[") and part.endswith("]"): + words.extend(ast.literal_eval(f"({part[len('Literal[') : -1]},)")) + continue + arm = _resolve_sibling_class(cls, part.rsplit(".", 1)[-1]) + if not (isinstance(arm, type) and issubclass(arm, Item)): + return None + classes.append(arm) + return (tuple(words), tuple(classes)) if classes else None + + +def _union_field_arms( + cls: type, f: attrs.Attribute +) -> "tuple[tuple[str, ...], tuple[type[Item], ...]] | None": + """A field's words and arm classes, if it's a union with arm classes.""" + t = _field_type_str(f) + # mypy false positive: type[Item] vs. Hashable (lru_cache arg) + return _union_arms(cls, t) if t is not None else None # type: ignore[arg-type] + + +def _union_value(values: list, arms: "tuple[tuple[str, ...], tuple[type[Item], ...]]") -> Any: + """A union field's value from the user-supplied values it spans: one of + its words, an arm instance given as is, or an arm built from them by + its keyword (see construct_union_item).""" + words, classes = arms + if len(values) == 1: + (value,) = values + if isinstance(value, classes): + return value + if isinstance(value, str) and value.upper() in words: + return value.upper() + return construct_union_item(values, classes) def _array_elem_type(f: attrs.Attribute) -> "type | None": @@ -169,11 +203,12 @@ def _field_type_str(f: attrs.Attribute) -> "str | None": def construct_item(item_cls: type, values) -> "Item": """Build an Item from a flat positional tuple, e.g. ``(cellid, q, 35.0)`` - for one aux variable, or ``("HEAD", "FREQUENCY", 2)`` for OC's Save - (rtype, ocsetting). Everything from the aux/array/nested-union field's - position onward collects into that one field's value, except a - trailing string when the class also has boundname (always declared - last) -- a string there unambiguously isn't a numeric aux value. + for one aux variable, ``("HEAD", "FREQUENCY", 2)`` for OC's Save + (rtype, ocsetting), or ``(0, "STATUS", "ACTIVE")`` for LKT's ifno and + laksetting. Everything from the aux/array/union field's position onward + collects into that one field's value, except a trailing string when the + class also has boundname (always declared last) -- a string there + unambiguously isn't a numeric aux value. """ fields = cast(type[Record], item_cls).fields() tuple_idx = next( @@ -182,30 +217,21 @@ def construct_item(item_cls: type, values) -> "Item": for i, f in enumerate(fields) if f in _tail_fields(item_cls) or (f.metadata.get("array") and not _dim_counted(item_cls, f)) - or ( - (t := _field_type_str(f)) is not None - and _nested_union_classes(item_cls, t) is not None - ) + or _union_field_arms(item_cls, f) is not None ), None, ) values = list(values) if tuple_idx is None: return cast("Item", item_cls(*values)) - nested_field = fields[tuple_idx] - nested_field_type = _field_type_str(nested_field) - arm_classes = ( - _nested_union_classes(item_cls, nested_field_type) - if nested_field_type is not None - else None - ) + arms = _union_field_arms(item_cls, fields[tuple_idx]) boundname_val = None if ( fields and fields[-1].name == "boundname" and len(values) > tuple_idx and isinstance(values[-1], str) - and arm_classes is None + and arms is None ): boundname_val = values[-1] values = values[:-1] @@ -217,12 +243,10 @@ def construct_item(item_cls: type, values) -> "Item": "Item", item_cls(*before, boundname=boundname_val) if boundname_val else item_cls(*before), ) - if arm_classes is None and len(trailing) == 1 and isinstance(trailing[0], (list, tuple)): + if arms is None and len(trailing) == 1 and isinstance(trailing[0], (list, tuple)): # The array given as one value, e.g. (0, 0.5, 0.5, 4, (0, 1, 4, 3)). trailing = list(trailing[0]) - tuple_vals = ( - construct_union_item(trailing, arm_classes) if arm_classes is not None else tuple(trailing) - ) + tuple_vals = _union_value(trailing, arms) if arms is not None else tuple(trailing) if boundname_val is not None: return cast("Item", item_cls(*before, tuple_vals, boundname=boundname_val)) return cast("Item", item_cls(*before, tuple_vals)) @@ -242,7 +266,7 @@ def _n_fixed_tokens(cls: type, sizes: Mapping[str, int] | None = None) -> tuple[ continue if f in _tail_fields(cls) or f.name == "boundname": continue - if f.metadata.get("optional"): + if f.metadata.get("optional") or _union_field_arms(cls, f) is not None: continue n += count + (1 if f.metadata.get("_keyword") else 0) if f.metadata.get("direction"): @@ -250,6 +274,19 @@ def _n_fixed_tokens(cls: type, sizes: Mapping[str, int] | None = None) -> tuple[ return n, ncellids +def _union_start(cls: type, row: list) -> "int | None": + """Where a row's union field begins: its first word or arm keyword + after the row's first token (TVK's ``1 2 3 K 1.0`` at K). None if the + class has no union field with arm classes, or the row has no such + token.""" + for f in cast(type[Record], cls).fields(): + if (arms := _union_field_arms(cls, f)) is not None: + words, classes = arms + keys = {*words, *(c.keyword().upper() for c in classes)} + return next((i for i, t in enumerate(row) if i and str(t).upper() in keys), None) + return None + + def ncelldim_from_dims(dims: "dict | None") -> "int | None": """Cellid element count implied by grid dimensions -- 3 for a structured (DIS) grid, 2 for vertex (DISV), 1 for unstructured (DISU). @@ -289,6 +326,7 @@ def infer_ncelldim( first = next((r for r in items if r), None) if not first: return 0 + first = first[: _union_start(item_cls, first)] n_fixed, ncellids = _n_fixed_tokens(item_cls, sizes) last = first[-1] has_bn = isinstance(last, str) and not _token_fits(last, float) @@ -438,8 +476,8 @@ def _is_row_key(f: attrs.Attribute) -> bool: class Item(Record): - """Mixin for generated table-item types (plain items and keystring-union - arms alike -- see module docstring).""" + """Mixin for generated table-item types (plain items and union arms + alike -- see module docstring).""" # Column order, when it differs from the field order (see from_tokens). _columns: ClassVar[tuple[str, ...]] = () @@ -507,10 +545,12 @@ def to_tokens(self) -> tuple: if val: row.append(f.name.upper()) elif isinstance(val, Record): - # Nested keystring-union field (OC's ocsetting) -- val is - # already the resolved arm instance and knows how to + # A union field's arm (OC's ocsetting, LKT's laksetting) -- + # val is already the resolved arm instance and knows how to # serialize itself. row.extend(val.to_tokens()) + elif _union_field_arms(cls, f) is not None: + row.append(str(val).upper()) # one of its words (OC's ALL) else: if file_kw := f.metadata.get("_keyword"): row.append(file_kw.upper()) @@ -629,14 +669,9 @@ def width(f: attrs.Attribute) -> int: return w main_fields = [f for f in fields if f not in tail and f.name != "boundname"] - nested_union_fields = [ - f - for f in main_fields - if (t := _field_type_str(f)) is not None - # mypy false positive: type[Item] vs. Hashable (lru_cache arg) - and _nested_union_classes(cls, t) is not None # type: ignore[arg-type] - ] - main_fields = [f for f in main_fields if f not in nested_union_fields] + # Unions with arm classes, the rest of the row (OC's ocsetting) + class_union_fields = [f for f in main_fields if _union_field_arms(cls, f) is not None] + main_fields = [f for f in main_fields if f not in class_union_fields] # Trailing columns of varying width (OBS's ids, see _read_unions). A # union with a keyword arm is one token, wherever it falls, read like # any other value (LAK's bedleak: a number or NONE, see _coerce). @@ -754,27 +789,23 @@ def width(f: attrs.Attribute) -> int: vals.append(tok) tok_idx += 1 kwargs[f.name] = tuple(vals) - elif nested_union_fields: - # Nested keystring-union field (OC's ocsetting) -- same span - # logic as array_fields above, but dispatches a typed arm - # instance instead of collecting a raw tuple. + elif class_union_fields: + # A union with arm classes (OC's ocsetting, LKT's laksetting) -- + # same span logic as array_fields above, but one of its words, + # or a typed arm instance, instead of a raw tuple. if not keyword_skipped: tok_idx += 1 keyword_skipped = True - f = nested_union_fields[0] - nested_field_type = _field_type_str(f) - assert nested_field_type is not None - arm_classes = _nested_union_classes(cls, nested_field_type) # type: ignore[arg-type] - assert arm_classes is not None + f = class_union_fields[0] + words, arm_classes = cast(tuple, _union_field_arms(cls, f)) end = n - (1 if has_bn_token else 0) - nsized - nested_tokens = list(tokens[tok_idx:end]) - arm_cls = dispatch_union_item(nested_tokens, arm_classes) - if arm_cls is None: - raise ValueError( - f"{cls.__name__}.{f.name}: no matching arm in {arm_classes} " - f"for tokens {nested_tokens}" - ) - kwargs[f.name] = arm_cls.from_tokens(nested_tokens) + span = list(tokens[tok_idx:end]) + if len(span) == 1 and str(span[0]).upper() in words: + kwargs[f.name] = str(span[0]).upper() + elif (arm_cls := dispatch_union_item(span, arm_classes)) is not None: + kwargs[f.name] = arm_cls.from_tokens(span, sizes=sizes, load_child=load_child) + else: + raise ValueError(f"{cls.__name__}.{f.name}: no arm of {f.type} reads {span}") tok_idx = end elif not keyword_skipped: tok_idx += 1 @@ -790,7 +821,7 @@ def width(f: attrs.Attribute) -> int: def _unwrap_item(item) -> "type[Item] | tuple[type[Item], ...] | None": """A single Item subclass, or the tuple of arm subclasses for a Union - (keystring-arm) item type.""" + item type.""" if isinstance(item, type) and issubclass(item, Item): return item origin = get_origin(item) @@ -829,8 +860,8 @@ def dispatch_union_item(item: list, arm_classes: "tuple[type[Item], ...]") -> "t def construct_union_item(values, arm_classes: "tuple[type[Item], ...]") -> "Item | None": - """Build an Item from a flat user-supplied positional tuple for a - keystring-union field, e.g. ``(0, "STATUS", "ACTIVE")``. + """Build an Item from a flat user-supplied positional tuple for a union + arm, e.g. ``(0, "STATUS", "ACTIVE")`` for LAK's Status. Dispatches to the right arm by keyword token, same as dispatch_union_item, then drops that token and builds the rest positionally via construct_item diff --git a/flopy4/mf6/package.py b/flopy4/mf6/package.py index a5a7d4a2..05b3885c 100644 --- a/flopy4/mf6/package.py +++ b/flopy4/mf6/package.py @@ -363,8 +363,8 @@ def _coerce_item_list(data, item_cls: "type[Item] | tuple[type[Item], ...]") -> - list of dicts → named columns - dict of lists → column-oriented {col_name: [values]} - For a keystring-union item_cls (a tuple of arm classes, e.g. LAK's - (LakStatus, LakStage, ...)): existing arm instances pass through; + For a union item_cls (a tuple of arm classes, e.g. OC's (OcSave, + OcPrint)): existing arm instances pass through; tuples/lists are dispatched to the right arm by their keyword token and built positionally (see flopy4.mf6.item.construct_union_item -- NOT from_tokens, since these values are already Python-side, not @@ -660,8 +660,8 @@ def from_dataframe(self, df: pd.DataFrame) -> None: item_cls = self._period_item_cls() if isinstance(item_cls, tuple): raise ValueError( - f"{type(self).__name__}.from_dataframe() doesn't support a keystring-union " - "period field (multiple possible row shapes) -- construct arm instances directly." + f"{type(self).__name__}.from_dataframe() doesn't support a union period " + "field (multiple possible row shapes) -- construct arm instances directly." ) spd: dict[int, list] = {} for kper, group in df.groupby("kper"): diff --git a/flopy4/mf6/prt/disv.py b/flopy4/mf6/prt/disv.py index 5f760523..b9882c27 100644 --- a/flopy4/mf6/prt/disv.py +++ b/flopy4/mf6/prt/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) + ) # noqa: E501 length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/prt/oc.py b/flopy4/mf6/prt/oc.py index 37ddca8c..38092a3b 100644 --- a/flopy4/mf6/prt/oc.py +++ b/flopy4/mf6/prt/oc.py @@ -1,6 +1,6 @@ # autogenerated file, do not modify from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar, Literal, Optional import attrs @@ -17,18 +17,6 @@ class Oc(Package): class Tracktimes(Item): time: float = field() - @attrs.define - class All(Item): - _keyword: ClassVar[str] = "all" - - @attrs.define - class First(Item): - _keyword: ClassVar[str] = "first" - - @attrs.define - class Last(Item): - _keyword: ClassVar[str] = "last" - @attrs.define class Frequency(Item): _keyword: ClassVar[str] = "frequency" @@ -43,13 +31,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Oc.All | Oc.First | Oc.Last | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() _StressPeriodDataItem = Save | Print @@ -160,9 +148,6 @@ class Print(Item): OcTracktimes = Oc.Tracktimes -OcAll = Oc.All -OcFirst = Oc.First -OcLast = Oc.Last OcFrequency = Oc.Frequency OcSteps = Oc.Steps OcSave = Oc.Save diff --git a/flopy4/mf6/record.py b/flopy4/mf6/record.py index 3f8220ac..b4b60f2d 100644 --- a/flopy4/mf6/record.py +++ b/flopy4/mf6/record.py @@ -25,10 +25,9 @@ def _resolve_sibling_class(cls: type, name: str) -> Any | None: """Resolve a bare sibling class name one level up from `cls` in `__qualname__`, where every generated flat-sibling class (composed - Records, keystring-union arms) lives regardless of DFN nesting depth. - No type check here -- callers (`_nested_class`, item.py's - `_nested_union_classes`) apply their own, against different base - classes. + Records, union arms) lives regardless of DFN nesting depth. No type + check here -- callers (`_nested_class`, item.py's `_union_arms`) apply + their own, against different base classes. Must resolve at runtime: a class body can't see sibling names from an enclosing scope, which is why the qualified string form diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index 44bef828..7f0359d5 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -718,7 +718,7 @@ def python_repr(v) -> str: def pascal_name(name: str) -> str: """snake_case, hyphenated, or a plain lowercase word -> PascalCase, e.g. ``stress_period_data`` -> ``StressPeriodData``, ``packagedata`` -> - ``Packagedata``, ``ext-inflow`` (a real MF6 keystring keyword, LKT/LKE) + ``Packagedata``, ``ext-inflow`` (a real MF6 keyword, LKT/LKE) -> ``ExtInflow``. Only "_"/"-" split words -- the wire keyword itself (unlike the class name) keeps its original separator, since a hyphen is fine inside a Python string but not an identifier.""" @@ -755,8 +755,9 @@ class ItemColumn: and emits them. ``annotation`` and ``rhs`` render as ``name: annotation = rhs``. A - nested-union column sets ``arm_classes`` instead of ``annotation``, as - its forward-reference union needs the package class name. + union column with arm classes sets ``arm_classes`` (and any ``words``) + instead of ``annotation``, as its forward-reference union needs the + package class name. """ name: str @@ -764,6 +765,7 @@ class ItemColumn: rhs: str optional: bool = False # declared after the required columns arm_classes: tuple[str, ...] = () + words: tuple[str, ...] = () @property def uses_path(self) -> bool: @@ -778,7 +780,7 @@ def uses_union(self) -> bool: @property def uses_literal(self) -> bool: """Whether the module needs ``Literal`` imported.""" - return "Literal[" in self.annotation + return "Literal[" in self.annotation or bool(self.words) @property def uses_ts_ref(self) -> bool: @@ -797,9 +799,13 @@ def uses_field(self) -> bool: def line(self, package_class_name: str = "") -> str: annotation = self.annotation if self.arm_classes: - arms = " | ".join(f"{package_class_name}.{c}" for c in self.arm_classes) - annotation = f'"{arms}"' - return f" {self.name}: {annotation} = {self.rhs}" + arms = [f"{package_class_name}.{c}" for c in self.arm_classes] + if self.words: + arms.insert(0, f"Literal[{', '.join(repr(w.upper()) for w in self.words)}]") + annotation = f'"{" | ".join(arms)}"' + line = f" {self.name}: {annotation} = {self.rhs}" + # a string annotation can't wrap + return line + " # noqa: E501" if len(line) > 100 else line def _margs(meta: dict) -> str: @@ -1017,17 +1023,20 @@ def child_column( ) -def nested_union_column( - name: str, arm_classes: list[str], *, optional: bool = False, time_series: bool = False +def class_union_column( + name: str, arm_classes: list[str], words: list[str], *, optional: bool = False ) -> ItemColumn: - """A union nested inside this arm (OC's ocsetting), arms already built as - sibling classes (see make.py's _build_arm_specs_from_union).""" + """A union column some of whose arms are classes, already built as + siblings (see make.py's _union_arm_specs), and the rest words: OC's + ocsetting, ``"Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps"``. + It takes the rest of the row.""" return ItemColumn( name=name, annotation="", rhs="field()", optional=optional, arm_classes=tuple(arm_classes), + words=tuple(words), ) @@ -1045,9 +1054,9 @@ def item_class( {{ block_schema | item_class("Packagedata") }} {{ arm_schema | item_class("Status", keyword="STATUS") }} - ``package_class_name`` (e.g. ``"Oc"``) is only needed for a nested-union - column -- it qualifies that field's forward-reference union annotation - (``"Oc.All | Oc.First | ..."``). + ``package_class_name`` (e.g. ``"Oc"``) is only needed for a union column + with arm classes -- it qualifies that field's forward-reference union + annotation (``"Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | ..."``). Produces a 4-space-indented ``@attrs.define`` class whose fields carry real metadata (``index=``/``pk=``/``fk=``/``cellid=``/``time_series=``/ @@ -1057,10 +1066,10 @@ def item_class( Required fields (no default) are declared before optional fields to satisfy attrs ordering constraints. - ``keyword``, when given, is one arm of a keystring-union period field - (e.g. LAK's STAGE/RATE/STATUS settings, OC's SAVE/PRINT records) -- - emitted as ``_keyword`` so flopy4.mf6.item.dispatch_union_item can pick - the right arm class for a raw row by its leading token. An arm can be a + ``keyword``, when given, is one arm of a union (e.g. LAK's STAGE/RATE/ + STATUS settings, OC's SAVE/PRINT records) -- emitted as ``_keyword`` so + flopy4.mf6.item.dispatch_union_item can pick the right arm class for a + raw row by its keyword token. An arm can be a bare keyword with no data of its own (e.g. PRP's releasesetting ALL/ FIRST/LAST) -- schema_list is then empty, but the class itself (just _keyword) is still real and must still be emitted. @@ -1113,52 +1122,19 @@ class ColumnSpec: is_index: bool # dev3 Integer.index -- 0-based, written as 1-based (+1 at write time) -def is_column_union(f: FieldV3) -> bool: - """Whether a union is one column, any of its arms (OBS's id: a cellid, - an index or a boundname; LAK's bedleak: a number or NONE), rather than a - keystring whose arms are line forms. Each arm of a column is one value: - a keyword, or an untagged scalar or array. A keystring's arms are - records or tagged values, whose keyword picks the arm wherever it falls - (LAK's laksetting arms lead with a lake or outlet number).""" - return isinstance(f, UnionField) and all( - isinstance(a, KeywordField) or not (isinstance(a, Record) or a.tagged) - for a in f.arms.values() - ) - - -def find_keystring_union(list_field: ListField) -> UnionField | None: - """Find the discriminating Union in a List field's per-row shape, if any. - - Two real shapes seen in the corpus: - - OC-style: ``item`` is a Union directly (no Record wrapper) -- saverecord/ - printrecord. - - LAK-style: ``item`` is a Record with exactly one field, itself a Union - (no separate index column -- each arm carries its own fk index, e.g. - LAK's ``lakeno``/``outletno``). - - LKE/LKT/SFR-style: ``item`` is a Record with an index column *and* a - sibling Union field (e.g. gwe-lke's ``{lakeno: Integer, laksetting: - Union}`` -- the index is a plain field here, not embedded per-arm). - - Returns None for ordinary (non-keystring) list blocks -- packagedata, - connectiondata, standard period blocks (CHD/WEL/DRN-style) -- where - ``item`` is a Record with no Union field anywhere among its top-level - fields. - """ - item = list_field.item - if isinstance(item, UnionField): - return item - if isinstance(item, Record): - for f in item.fields.values(): - if isinstance(f, UnionField) and not is_column_union(f): - return f - return None +def needs_class(arm: FieldV3) -> bool: + """Whether a union arm types as a class of its own: a record, or a + tagged value (OC's ``FREQUENCY ``), whose keyword picks the arm. A + keyword arm is a literal word, and an untagged scalar or array its own + type (LAK's bedleak: NONE or a number).""" + return isinstance(arm, Record) or (not isinstance(arm, KeywordField) and arm.tagged) def _fields_to_columns(fields: "list[tuple[str, FieldV3]]") -> list[ColumnSpec]: """Build ColumnSpecs from an ordered (name, field) sequence -- the shared core of list_columns (a List[Record]'s own item fields) and - make.py's keystring-union arm processing (a Union arm's fields, once its - own leading keyword, if any, is split off as the arm's _keyword). + make.py's union arm processing (a Union arm's fields, once its own + leading keyword, if any, is split off as the arm's _keyword). ``safe_name`` sanitizes the column name (e.g. LKT/LKE's hyphenated "ext-inflow" arm -> "ext_inflow") since it becomes a Python attribute @@ -1194,13 +1170,8 @@ def _fields_to_columns(fields: "list[tuple[str, FieldV3]]") -> list[ColumnSpec]: def list_columns(f: ListField) -> list[ColumnSpec]: - """Return the leaf column specs of a dev3 List[Record] field, in order. - - Returns [] for keystring-shaped lists (see find_keystring_union) -- those - are handled separately (see make.py's keystring period handling). - """ - if find_keystring_union(f) is not None: - return [] + """Return the column specs of a dev3 List[Record] field, in order; [] + for a list of unions (see make.py's _build_list_item_specs).""" item = f.item if not isinstance(item, Record): return [] @@ -1230,17 +1201,11 @@ def list_dim_bound(f: ListField) -> str | None: return split_bound(shape[0])[0] if len(shape) == 1 else None -def collision_names( - block_schemas: dict[str, list[ColumnSpec]], - reserved: frozenset[str] = frozenset(), -) -> set[str]: - """Column names that require block-prefixed Python attr names. - - A name is a collision when it appears in more than one static list block, - OR when it appears in any block AND is reserved by a period field. The - latter ensures that static block attrs never shadow bare period field names. - Prefix columns are excluded since they produce no attr. +def collision_names(block_schemas: dict[str, list[ColumnSpec]]) -> set[str]: + """Column names that require block-prefixed Python attr names: those + in more than one static list block. Prefix columns are excluded since + they produce no attr. """ names = [col.name for cols in block_schemas.values() for col in cols if not col.is_prefix] counts = Counter(names) - return {name for name, count in counts.items() if count > 1 or name in reserved} + return {name for name, count in counts.items() if count > 1} diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index 50f2dc57..1b0d24c6 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -105,9 +105,9 @@ class ItemClassSpec: tagged list's record), or one arm of a union (LAK's Stage/Rate/Status, OC's Saverecord/Printrecord). - ``top_level`` is False for an arm built by recursing into another - arm's own union-typed field (e.g. OC's ocsetting) -- still emitted as - a flat sibling class, but excluded from the outer union alias. + ``top_level`` is False for an arm of a union field (OC's ocsetting, + LKT's laksetting) -- still emitted as a flat sibling class, but + excluded from any list's union alias. """ class_name: str @@ -184,8 +184,8 @@ class ComponentSpec: # Column-schema-dict builders # -# Both static list blocks and standard (non-keystring) period blocks are -# List[Record] under dev3 -- including a real cellid Array field for period +# Static list blocks and period blocks of records are List[Record] under +# dev3 -- including a real cellid Array field for period # blocks, which the legacy schema had to synthesize. One function builds the # list[dict] "schema" (the intermediate format item_class/schema_class render) # for both cases; only the surrounding FieldSpec (type annotation, block= @@ -194,7 +194,7 @@ class ComponentSpec: def _item_columns( columns: list[ColumnSpec], - nested_arm_classes: "dict[str, list[str]] | None" = None, + unions: "Mapping[str, tuple[list[str], list[str]]] | None" = None, children: "Mapping[str, list[str]] | None" = None, ) -> list[ItemColumn]: """Build the rendered columns of an Item class from ColumnSpecs. @@ -206,12 +206,11 @@ def _item_columns( e.g. MIXED) are tagged bools. An array column's shape expression is emitted as-is, for the runtime to evaluate. - ``nested_arm_classes``, when given, maps a column name to sibling arm - class names already built for it (see ``_build_arm_specs_from_union``) - -- such a column is a nested union instead of the generic untyped-union - array below. + ``unions`` maps a union column's name to the arm classes already built + for it and its words (see ``_union_arm_specs``); any other union column + is one value, of any of its arms. """ - nested_arm_classes = nested_arm_classes or {} + unions = unions or {} children = children or {} items: list[ItemColumn] = [] pending_prefix: list[str] = [] @@ -279,7 +278,11 @@ class names already built for it (see ``_build_arm_specs_from_union``) item = filters.attr_column( name, "bool", {"tagged": True}, optional=True, default="False" ) - elif filters.is_column_union(f): + elif isinstance(f, UnionField) and name in unions: + # Words and arm classes, the rest of the row (OC's ocsetting) + classes, words = unions[name] + item = filters.class_union_column(name, classes, words, optional=optional) + elif isinstance(f, UnionField): # One column, any of the arms (OBS's id: a cellid, an index or # a boundname), told apart when read (see Item.from_tokens). item = filters.union_column( @@ -289,11 +292,7 @@ class names already built for it (see ``_build_arm_specs_from_union``) optional=optional, time_series=ts, ) - elif isinstance(f, UnionField) and name in nested_arm_classes: - item = filters.nested_union_column(name, nested_arm_classes[name], optional=optional) - elif isinstance(f, UnionField) or ( - isinstance(f, Array) and not filters.is_fixed_length_array(f) - ): + elif isinstance(f, Array) and not filters.is_fixed_length_array(f): item = filters.array_column(name, None, dfn_type) else: item = filters.value_column( @@ -488,29 +487,25 @@ def _build_list_item_specs( children: "Mapping[str, list[str]] | None" = None, ) -> tuple[list[ItemClassSpec], str, ItemUnionSpec | None]: """Build the Item class(es) for a list's elements, wherever the list is - (any block, repeating or not). Returns (classes, element type, union - alias or None): - - - a union item (OC's output, LAK's period settings): one class per arm, - dispatched by keyword, and a ``_Item`` alias for the union - - a tagged (keyword-led) record item: one class whose leading keyword is - its ``_keyword``, built like a union arm - - an untagged record item (packagedata, CHD's stress_period_data): one - class of positional columns + (any block, repeating or not), mirroring the DFN's element. Returns + (classes, element type, union alias or None): + + - a union element (OC's output, LAK's period settings): one class per + arm, dispatched by keyword, and a ``_Item`` alias for the + union + - a tagged (keyword-led) record element: one class whose leading + keyword is its ``_keyword``, built like a union arm + - an untagged record element (packagedata, CHD's stress_period_data, + LKT's ``ifno`` and ``laksetting``): one class of positional columns, + a union among them a field of its own (see _union_arm_specs) ``children`` maps the rows' child columns, by name, to the components they name (see _item_columns). """ - union = filters.find_keystring_union(list_field) - if union is not None: - item = list_field.item - shared_cols: list[tuple[str, FieldV3]] = ( - [(n, f) for n, f in item.fields.items() if f is not union] - if isinstance(item, Record) - else [] - ) + item = list_field.item + if isinstance(item, UnionField): specs = _build_arm_specs_from_union( - union, used_names, {}, shared_cols, name_hint=list_field.name, children=children + item, used_names, {}, name_hint=list_field.name, children=children ) alias = f"_{class_name}Item" members = [s.class_name for s in specs if s.top_level] @@ -518,47 +513,84 @@ class of positional columns if list_field.tagged: specs = _build_arm_spec( list_field.name, - list_field.item, + item, used_names, {}, - [], class_name=class_name, ) return specs, specs[-1].class_name, None - schema = _item_columns(filters.list_columns(list_field), children=children) + fields = list(item.fields.items()) if isinstance(item, Record) else [] + specs, unions = _union_arm_specs(fields, used_names, {}, children) + schema = _item_columns(filters.list_columns(list_field), unions, children) used_names.add(class_name) - spec = ItemClassSpec(class_name=class_name, keyword="", schema=schema) - return [spec], class_name, None + specs.append(ItemClassSpec(class_name=class_name, keyword="", schema=schema)) + return specs, class_name, None + + +def _union_arm_specs( + fields: "list[tuple[str, FieldV3]]", + used_names: set[str], + nested_union_cache: "dict[tuple[str, ...], list[str]]", + children: "Mapping[str, list[str]] | None" = None, +) -> "tuple[list[ItemClassSpec], dict[str, tuple[list[str], list[str]]]]": + """Build the classes for the union fields among ``fields`` whose arms + need them (see filters.needs_class). Returns them, and each such + field's (arm classes, words) by name: OC's ocsetting is the words ALL, + FIRST and LAST, or a Frequency or Steps. A union of words and values + alone (LAK's bedleak) needs no classes and is left out. + + `nested_union_cache` is keyed by a union's arm-name set, since DFN + parsing builds each Record's fields independently -- OC's + `saverecord.ocsetting` and `printrecord.ocsetting` are distinct `Union` + objects with identical arms, and without the cache the second would + rebuild and rename a duplicate set of classes instead of reusing the + first's. + """ + specs: list[ItemClassSpec] = [] + unions: dict[str, tuple[list[str], list[str]]] = {} + for field_name, fld in fields: + if not isinstance(fld, UnionField): + continue + arms = list(fld.arms.items()) + if not any(filters.needs_class(a) for _, a in arms): + continue # a value column (see _item_columns) + words = [a.name for _, a in arms if isinstance(a, KeywordField)] + values = [n for n, a in arms if not (isinstance(a, KeywordField) or filters.needs_class(a))] + if values: + raise ValueError(f"union {field_name!r}: untagged arms {values} beside record arms") + cache_key = tuple(sorted(fld.arms.keys())) + classes = nested_union_cache.get(cache_key) + if classes is None: + classes = [] + for arm_name, arm in arms: + if filters.needs_class(arm): + arm_specs = _build_arm_spec( + arm_name, + arm, + used_names, + nested_union_cache, + name_hint=field_name, + top_level=False, + children=children, + ) + specs.extend(arm_specs) + classes.append(arm_specs[-1].class_name) + nested_union_cache[cache_key] = classes + unions[field_name] = (classes, words) + return specs, unions def _build_arm_specs_from_union( union: UnionField, used_names: set[str], - nested_union_cache: "dict[tuple[str, ...], list[ItemClassSpec]]", - shared_cols: "list[tuple[str, FieldV3]]" = [], + nested_union_cache: "dict[tuple[str, ...], list[str]]", *, name_hint: str = "", - top_level: bool = True, children: "Mapping[str, list[str]] | None" = None, ) -> list[ItemClassSpec]: - """Build one ItemClassSpec per arm of `union` (see _build_arm_spec). - - Handles all three index shapes seen in the corpus generically, via a - shared prefix of columns prepended to every arm: - - OC-style: the List's item IS the union directly (no index at all). - - LAK-style: each arm embeds its own fk index (lakeno/outletno) as one - of its own fields -- no shared prefix needed, it falls out of the - arm's own fields. - - SFR/MAW-style: the union is a sibling of an outer index field (item - Record = {ifno, ...setting: Union}) -- shared by every arm. - - `nested_union_cache` (shared across the whole call tree) is keyed by a - nested union's arm-name set, since DFN parsing builds each Record - arm's fields independently -- OC's `saverecord.ocsetting` and - `printrecord.ocsetting` are distinct `Union` objects with identical - arms, and without the cache the second occurrence would rebuild and - rename a duplicate set of classes instead of reusing the first's. - """ + """Build one ItemClassSpec per arm of a list's union element (see + _build_arm_spec): OC's output, PRP's period settings, or LAK's, each + arm of which leads with its own lake or outlet number.""" specs: list[ItemClassSpec] = [] for arm_name, arm in union.arms.items(): specs.extend( @@ -567,9 +599,7 @@ def _build_arm_specs_from_union( arm, used_names, nested_union_cache, - shared_cols, name_hint=name_hint, - top_level=top_level, children=children, ) ) @@ -580,8 +610,7 @@ def _build_arm_spec( arm_name: str, arm: FieldV3, used_names: set[str], - nested_union_cache: "dict[tuple[str, ...], list[ItemClassSpec]]", - shared_cols: "list[tuple[str, FieldV3]]", + nested_union_cache: "dict[tuple[str, ...], list[str]]", *, name_hint: str = "", top_level: bool = True, @@ -591,11 +620,9 @@ def _build_arm_spec( """Build the keyword-led Item class for one union arm, or for a tagged list's record item -- each line starts with (or, LAK-style, contains) a keyword naming it. Returns any classes built for unions nested in it - (OC's ocsetting, recursively exploded into its own typed sub-arms with - `top_level=False` -- the DFN schema doesn't cap union nesting depth), - then this one. + (OC's ocsetting, see _union_arm_specs, with `top_level=False`), then + this one. """ - specs: list[ItemClassSpec] = [] if isinstance(arm, Record): # The discriminating keyword isn't always the arm's first field -- # LAK's auxiliaryrecord is (lakeno, auxiliary(kw), auxname, auxval), @@ -630,26 +657,8 @@ def _build_arm_spec( keyword = "_".join(_strip_record_words(arm_name)) rest = [(arm_name, arm)] - nested_arm_classes: dict[str, list[str]] = {} - for field_name, fld in rest: - if not isinstance(fld, UnionField) or filters.is_column_union(fld): - continue # a column union is read as a column (see _item_columns) - cache_key = tuple(sorted(fld.arms.keys())) - cached = nested_union_cache.get(cache_key) - if cached is None: - cached = _build_arm_specs_from_union( - fld, - used_names, - nested_union_cache, - name_hint=field_name, - top_level=False, - ) - nested_union_cache[cache_key] = cached - specs.extend(cached) - nested_arm_classes[field_name] = [s.class_name for s in cached] - - cols = filters._fields_to_columns(list(shared_cols) + rest) - schema = _item_columns(cols, nested_arm_classes, children) + specs, unions = _union_arm_specs(rest, used_names, nested_union_cache, children) + schema = _item_columns(filters._fields_to_columns(rest), unions, children) if not class_name: class_name = pascal_name("_".join(_strip_record_words(arm_name))) if class_name in used_names: @@ -813,34 +822,9 @@ def _process_child(child: FieldV3) -> None: return nested_specs + [this_spec] -def _period_keystring_names(component: Component) -> frozenset[str]: - """Names reserved by the period block's keystring keywords, if any. - - LAK-style keystring period settings (STATUS, STAGE, RATE, INVERT, ...) - are consolidated into a single generic _stress_period_data field (see - build_component_spec) -- no Python attribute is literally named e.g. - "invert" for period data. But the same word is also a real static-block - column name in some packages (LAK's outlets.invert), and a bare "invert" - attr there would read ambiguously against the period keyword string of - the same name. Static columns colliding with a period keystring keyword - take a block-prefixed attr name instead (collision_names' `reserved` - param) -- reproduces the legacy behavior, now sourced from the real - Union.arms names instead of the extra_period_fields TOML override table - (removed: no longer needed now that arms are schema-native). - """ - for block_name in filters.fill_forward_blocks(component): - for f in component.blocks[block_name].fields.values(): - if filters.is_list_field(f): - union = filters.find_keystring_union(f) - if union is not None: - return frozenset(name.lower() for name in union.arms) - return frozenset() - - def _build_block_property_specs( component: Component, *, - reserved_names: frozenset[str] = frozenset(), skip: frozenset[str] = frozenset(), ) -> tuple[list[BlockPropertySpec], set[str]]: """Compute BlockPropertySpec for all static (non-period) list blocks. @@ -864,7 +848,7 @@ def _build_block_property_specs( break col_schemas = {block: filters.list_columns(f) for block, f in list_fields_map.items()} - collisions = filters.collision_names(col_schemas, reserved=reserved_names) + collisions = filters.collision_names(col_schemas) # Resolve which DFN dimension scalar each block maps to: only through the # list's explicit shape (see filters.list_col_dim). @@ -1164,6 +1148,17 @@ def _child_link( return link, targets +def _element_unions(f: ListField) -> list[UnionField]: + """The unions in a list's element: the element itself (LAK's period + settings), or its fields (SFR's sfrsetting).""" + item = f.item + if isinstance(item, UnionField): + return [item] + if isinstance(item, Record): + return [u for u in item.fields.values() if isinstance(u, UnionField)] + return [] + + def _column_children( component: Component, f: FieldV3, dfns: Mapping[str, Component] | None ) -> dict[str, list[str]]: @@ -1173,8 +1168,10 @@ def _column_children( ``CROSS_SECTION TAB6 FILEIN ``, a utl-sfrtab), or nothing.""" if not isinstance(f, ListField): return {} - if (union := filters.find_keystring_union(f)) is not None: - return _arm_column_children(component, f, union, dfns) + if unions := _element_unions(f): + return { + k: v for u in unions for k, v in _arm_column_children(component, f, u, dfns).items() + } if (found := _find_link(f)) is None: return {} path, link, file_only = found @@ -1189,7 +1186,7 @@ def _column_children( def _arm_column_children( component: Component, f: ListField, union: UnionField, dfns: Mapping[str, Component] | None ) -> dict[str, list[str]]: - """A keystring list's file columns naming components, from its arms.""" + """A list's file columns naming components, from a union's arms.""" children = {} for arm in union.arms.values(): for name, m in (arm.fields or {}).items() if isinstance(arm, Record) else (): @@ -1419,11 +1416,7 @@ def build_component_spec( # Linked fields, by name, each replaced by a child field. links = {f.name: found for _, f in all_fields if (found := _child_link(component, f, dfns))} child_fields = frozenset(links) - block_properties, _bp_block_names = _build_block_property_specs( - component, - reserved_names=_period_keystring_names(component), - skip=child_fields, - ) + block_properties, _bp_block_names = _build_block_property_specs(component, skip=child_fields) # DIMENSIONS fields counting a list's rows _linked_dims = {bp.dim_attr for bp in block_properties if bp.dim_is_dfn_declared} @@ -1490,9 +1483,7 @@ def _add_list_items(lf: ListField, class_name: str) -> str: # but still needs the same per-period, fill-forward dict[int, ...] # treatment as any other period field (the whole point of a period # block is that its contents can differ/repeat across BEGIN PERIOD - # blocks). Same single-column keystring shape as the LAK-style case - # above, just with exactly one column since there's nothing else in - # the block to key against. + # blocks). One column, since there's nothing else in the block. if block_name in _fill_forward_blocks and filters.is_scalar(f): item_classes.append( ItemClassSpec( diff --git a/flopy4/mf6/utl/obs.py b/flopy4/mf6/utl/obs.py index 779dc206..da0c6acf 100644 --- a/flopy4/mf6/utl/obs.py +++ b/flopy4/mf6/utl/obs.py @@ -21,7 +21,7 @@ class Continuous(Item): id_: Union[tuple[int, ...], int, str] = field(union=("cellid", "index", "string")) id2: Optional[Union[tuple[int, ...], int, str]] = field( default=None, union=("cellid", "index", "string"), optional=True - ) + ) # noqa: E501 @attrs.define class Output(Item): diff --git a/flopy4/mf6/utl/spc.py b/flopy4/mf6/utl/spc.py index d9f9eaf1..a5354f09 100644 --- a/flopy4/mf6/utl/spc.py +++ b/flopy4/mf6/utl/spc.py @@ -19,16 +19,17 @@ class Spc(Package): @attrs.define class Concentration(Item): _keyword: ClassVar[str] = "concentration" - bndno: int = field(index=True, pk=True) concentration: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Temperature(Item): _keyword: ClassVar[str] = "temperature" - bndno: int = field(index=True, pk=True) temperature: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = Concentration | Temperature + @attrs.define + class StressPeriodData(Item): + bndno: int = field(index=True, pk=True) + spcsetting: "Spc.Concentration | Spc.Temperature" = field() print_input: bool = field( default=False, @@ -39,7 +40,7 @@ class Temperature(Item): ts: list[Ts] = child( block="options", keyword="ts6", direction="in", default=attrs.Factory(list) ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -61,3 +62,4 @@ def maxbound(self) -> int: SpcConcentration = Spc.Concentration SpcTemperature = Spc.Temperature +SpcStressPeriodData = Spc.StressPeriodData diff --git a/flopy4/mf6/utl/tvk.py b/flopy4/mf6/utl/tvk.py index ce6a1c19..45dc8169 100644 --- a/flopy4/mf6/utl/tvk.py +++ b/flopy4/mf6/utl/tvk.py @@ -17,22 +17,22 @@ class Tvk(Package): @attrs.define class K(Item): _keyword: ClassVar[str] = "k" - cellid: tuple = field(cellid=True) k: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class K22(Item): _keyword: ClassVar[str] = "k22" - cellid: tuple = field(cellid=True) k22: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class K33(Item): _keyword: ClassVar[str] = "k33" - cellid: tuple = field(cellid=True) k33: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = K | K22 | K33 + @attrs.define + class StressPeriodData(Item): + cellid: tuple = field(cellid=True) + tvksetting: "Tvk.K | Tvk.K22 | Tvk.K33" = field() print_input: bool = field( default=False, @@ -43,7 +43,7 @@ class K33(Item): ts: list[Ts] = child( block="options", keyword="ts6", direction="in", default=attrs.Factory(list) ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -55,3 +55,4 @@ class K33(Item): TvkK = Tvk.K TvkK22 = Tvk.K22 TvkK33 = Tvk.K33 +TvkStressPeriodData = Tvk.StressPeriodData diff --git a/flopy4/mf6/utl/tvs.py b/flopy4/mf6/utl/tvs.py index 36eaf1fb..daca523b 100644 --- a/flopy4/mf6/utl/tvs.py +++ b/flopy4/mf6/utl/tvs.py @@ -17,16 +17,17 @@ class Tvs(Package): @attrs.define class Ss(Item): _keyword: ClassVar[str] = "ss" - cellid: tuple = field(cellid=True) ss: Union[float, TimeSeriesRef] = field(time_series=True) @attrs.define class Sy(Item): _keyword: ClassVar[str] = "sy" - cellid: tuple = field(cellid=True) sy: Union[float, TimeSeriesRef] = field(time_series=True) - _StressPeriodDataItem = Ss | Sy + @attrs.define + class StressPeriodData(Item): + cellid: tuple = field(cellid=True) + tvssetting: "Tvs.Ss | Tvs.Sy" = field() disable_storage_change_integration: bool = field( default=False, @@ -43,7 +44,7 @@ class Sy(Item): ts: list[Ts] = child( block="options", keyword="ts6", direction="in", default=attrs.Factory(list) ) - _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( + _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( alias="stress_period_data", default=None, repr=False, @@ -54,3 +55,4 @@ class Sy(Item): TvsSs = Tvs.Ss TvsSy = Tvs.Sy +TvsStressPeriodData = Tvs.StressPeriodData diff --git a/test/mf6/test_mf6_codec.py b/test/mf6/test_mf6_codec.py index 52f88108..416ea640 100644 --- a/test/mf6/test_mf6_codec.py +++ b/test/mf6/test_mf6_codec.py @@ -499,8 +499,8 @@ def test_ts_names_mismatch(): def test_ts_reference_must_name_a_series(function_tmpdir, monkeypatch): """Writing checks that each name in a time-series column, aux and - keystring rows included, is a series the package's TS6 files define, - in any case, as MF6 needs.""" + union arms included, is a series the package's TS6 files define, in + any case, as MF6 needs.""" from flopy4.mf6.gwf import Chd, Maw from flopy4.mf6.utl import Ts @@ -532,7 +532,7 @@ def ts(name): maw = Maw( packagedata=[(0, 0.1, -10.0, 5.0, "THIEM", 1)], connectiondata=[(0, 0, (0, 0, 0), 0.0, -10.0, 1.0, 0.2)], - stress_period_data={0: [Maw.Rate(ifno=0, rate="pumping")]}, + stress_period_data={0: [Maw.StressPeriodData(ifno=0, mawsetting=Maw.Rate(rate="pumping"))]}, filename=function_tmpdir / "gwf.maw", ) with pytest.raises(ValueError, match=r"'pumping'.*none"): @@ -712,7 +712,10 @@ def test_maw_round_trip(): (1, 0, (0, 0, 1), 0.0, -10.0, 1.0, 0.2), ], stress_period_data={ - 0: [Maw.Rate(ifno=0, rate=-100.0), Maw.Status(ifno=1, status="INACTIVE")] + 0: [ + Maw.StressPeriodData(ifno=0, mawsetting=Maw.Rate(rate=-100.0)), + Maw.StressPeriodData(ifno=1, mawsetting=Maw.Status(status="INACTIVE")), + ] }, ) raw = loads(dumps(unstructure_component(maw))) @@ -804,13 +807,13 @@ def test_sfr_signed_index_zero(): Sfr.Connectiondata(ifno=0, ic=(0,)) -def test_sfr_diversion_keyword_after_row_key(): +def test_sfr_diversion_row(): from flopy4.mf6.gwf import Sfr - # the keyword follows the row key (ifno) but precedes other index columns - div = Sfr.Diversion(ifno=0, idv=1, divflow=0.5) - assert div.to_tokens() == (1, "DIVERSION", 2, 0.5) - assert Sfr.Diversion.from_tokens([1, "diversion", 2, 0.5]) == div + # the row's ifno, then the setting: its keyword, then its own index + row = Sfr.StressPeriodData(ifno=0, sfrsetting=Sfr.Diversion(idv=1, divflow=0.5)) + assert row.to_tokens() == (1, "DIVERSION", 2, 0.5) + assert Sfr.StressPeriodData.from_tokens([1, "diversion", 2, 0.5]) == row def test_sfr_unconnected_reach_round_trip(): @@ -1651,7 +1654,7 @@ def test_dumps_gwt_oc_wildcard(): def test_dumps_prt_prp_release_setting(): """prt-prp period release settings write correct MF6 keywords via stress_period_data. - PRP's `releasesetting` keystring union has no per-row index (unlike LAK/ + PRP's `releasesetting` union has no per-row index (unlike LAK/ LKE/SFR) -- the v1 DFN declares it a bare `recarray releasesetting` with no feature-id column, and dev3 confirms no arm carries a pk/fk field, so rows are typed per-arm classes: `Frequency` carries an Integer payload; @@ -1704,8 +1707,8 @@ def test_prt_prp_period_roundtrip(): def test_oc_ocsetting_typed_dispatch(): - """OC's nested ocsetting union (Save/Print's own All/First/Last/ - Frequency/Steps arms) dispatches to real typed instances, not a raw + """OC's ocsetting union (Save/Print's ALL/FIRST/LAST words and + Frequency/Steps arms) reads as a word or a typed instance, not a raw tuple, and survives a dump->load->structure_component cycle -- same coverage as test_prt_prp_period_roundtrip for the top-level case.""" from flopy4.mf6.converter.egress.unstructure import unstructure_component @@ -1735,7 +1738,7 @@ def test_oc_ocsetting_typed_dispatch(): assert save_row.ocsetting.steps == (1.0, 3.0, 5.0) print_row = next(r for r in rows if isinstance(r, Oc.Print)) - assert isinstance(print_row.ocsetting, Oc.All) + assert print_row.ocsetting == "ALL" def test_oc_ocsetting_construct_item_positional(): @@ -1744,10 +1747,12 @@ def test_oc_ocsetting_construct_item_positional(): from_tokens does.""" from flopy4.mf6.gwf import Oc - oc = Oc(stress_period_data={0: [("SAVE", "HEAD", "ALL")]}) - row = oc.stress_period_data[0][0] - assert isinstance(row, Oc.Save) - assert isinstance(row.ocsetting, Oc.All) + oc = Oc(stress_period_data={0: [("SAVE", "HEAD", "all"), ("PRINT", "BUDGET", "FREQUENCY", 2)]}) + save, print_ = oc.stress_period_data[0] + assert isinstance(save, Oc.Save) + assert save.ocsetting == "ALL" + assert isinstance(print_.ocsetting, Oc.Frequency) + assert print_.ocsetting.frequency == 2 # --------------------------------------------------------------------------- @@ -2371,7 +2376,7 @@ def test_ssm_fileinput_children(tmp_path): assert isinstance(wel.spc, Spc) and wel.mixed assert isinstance(rch.spc, Spca) assert wel.spc.filename == Path("wel.spc") - assert [r.concentration for r in wel.spc.stress_period_data[0]] == [100.0] + assert [r.spcsetting.concentration for r in wel.spc.stress_period_data[0]] == [100.0] assert sorted(ssm._children) == ["fileinput0", "fileinput1"] assert wel.spc.parent is ssm @@ -2402,8 +2407,8 @@ def test_sfr_period_cross_section_children(tmp_path, monkeypatch): ) sfr = Sfr.load(tmp_path / "gwf.sfr", dims={"nlay": 1, "nrow": 1, "ncol": 1}) a = sfr.crosssections[0].sfrtab - (b_row,) = sfr.stress_period_data[1] - (a_row,) = sfr.stress_period_data[2] + (b_row,) = (r.sfrsetting for r in sfr.stress_period_data[1]) + (a_row,) = (r.sfrsetting for r in sfr.stress_period_data[2]) assert isinstance(b_row.sfrtab, Sfrtab) assert b_row.sfrtab.filename == Path("b.tab") assert b_row.sfrtab.table[0].height == 2.0 @@ -2796,8 +2801,8 @@ def test_lkt_packagedata_double_aux_roundtrip(): assert [r.boundname for r in pd] == ["mylake"] -def test_lak_keystring_period_roundtrip(): - """LAK keystring period data round-trips through dump→load→structure.""" +def test_lak_period_roundtrip(): + """LAK period data round-trips through dump→load→structure.""" from flopy4.mf6.codec.writer import dumps from flopy4.mf6.converter.egress.unstructure import unstructure_component from flopy4.mf6.converter.ingress.structure import structure_component @@ -2838,28 +2843,30 @@ def test_lak_keystring_period_roundtrip(): # Period 0: 4 rows p0 = spd[0] assert len(p0) == 4 - assert isinstance(p0[0], Lak.Status) and p0[0].lakeno == 0 # 0-based feature id - assert p0[0].status == "ACTIVE" - assert isinstance(p0[1], Lak.Rainfall) and p0[1].lakeno == 0 - assert float(p0[1].rainfall) == pytest.approx(0.1) - assert isinstance(p0[2], Lak.Status) and p0[2].lakeno == 1 - assert p0[2].status == "CONSTANT" - assert isinstance(p0[3], Lak.Stage) and p0[3].lakeno == 1 - assert float(p0[3].stage) == pytest.approx(5.0) + assert ( + isinstance(p0[0].laksetting, Lak.Status) and p0[0].laksetting.lakeno == 0 + ) # 0-based feature id + assert p0[0].laksetting.status == "ACTIVE" + assert isinstance(p0[1].laksetting, Lak.Rainfall) and p0[1].laksetting.lakeno == 0 + assert float(p0[1].laksetting.rainfall) == pytest.approx(0.1) + assert isinstance(p0[2].laksetting, Lak.Status) and p0[2].laksetting.lakeno == 1 + assert p0[2].laksetting.status == "CONSTANT" + assert isinstance(p0[3].laksetting, Lak.Stage) and p0[3].laksetting.lakeno == 1 + assert float(p0[3].laksetting.stage) == pytest.approx(5.0) # Period 1: 1 row p1 = spd[1] assert len(p1) == 1 - assert isinstance(p1[0], Lak.Status) and p1[0].lakeno == 0 - assert p1[0].status == "INACTIVE" + assert isinstance(p1[0].laksetting, Lak.Status) and p1[0].laksetting.lakeno == 0 + assert p1[0].laksetting.status == "INACTIVE" # Period 2: 2 rows p2 = spd[2] assert len(p2) == 2 - assert isinstance(p2[0], Lak.Status) and p2[0].lakeno == 0 - assert p2[0].status == "ACTIVE" - assert isinstance(p2[1], Lak.Withdrawal) and p2[1].lakeno == 1 - assert float(p2[1].withdrawal) == pytest.approx(100.0) + assert isinstance(p2[0].laksetting, Lak.Status) and p2[0].laksetting.lakeno == 0 + assert p2[0].laksetting.status == "ACTIVE" + assert isinstance(p2[1].laksetting, Lak.Withdrawal) and p2[1].laksetting.lakeno == 1 + assert float(p2[1].laksetting.withdrawal) == pytest.approx(100.0) # --------------------------------------------------------------------------- @@ -3027,7 +3034,7 @@ def test_lkt_period_dumps(): def test_lkt_period_roundtrip(): - """LKT keystring period data round-trips through dump→load→structure.""" + """LKT period data round-trips through dump→load→structure.""" from flopy4.mf6.converter.egress.unstructure import unstructure_component from flopy4.mf6.converter.ingress.structure import structure_component from flopy4.mf6.gwt.lkt import Lkt @@ -3054,17 +3061,17 @@ def test_lkt_period_roundtrip(): p0 = spd[0] assert len(p0) == 3 - assert isinstance(p0[0], Lkt.Status) and p0[0].ifno == 0 - assert p0[0].status == "ACTIVE" - assert isinstance(p0[1], Lkt.Status) and p0[1].ifno == 1 - assert p0[1].status == "CONSTANT" - assert isinstance(p0[2], Lkt.Concentration) and p0[2].ifno == 0 - assert float(p0[2].concentration) == pytest.approx(10.0) + assert p0[0].ifno == 0 and isinstance(p0[0].laksetting, Lkt.Status) + assert p0[0].laksetting.status == "ACTIVE" + assert p0[1].ifno == 1 and isinstance(p0[1].laksetting, Lkt.Status) + assert p0[1].laksetting.status == "CONSTANT" + assert p0[2].ifno == 0 and isinstance(p0[2].laksetting, Lkt.Concentration) + assert float(p0[2].laksetting.concentration) == pytest.approx(10.0) p1 = spd[1] assert len(p1) == 1 - assert isinstance(p1[0], Lkt.Status) and p1[0].ifno == 0 - assert p1[0].status == "INACTIVE" + assert p1[0].ifno == 0 and isinstance(p1[0].laksetting, Lkt.Status) + assert p1[0].laksetting.status == "INACTIVE" def test_lkt_packagedata_roundtrip(): @@ -3138,7 +3145,7 @@ def test_lke_period_dumps(): def test_lke_period_roundtrip(): - """LKE keystring period data round-trips through dump→load→structure.""" + """LKE period data round-trips through dump→load→structure.""" from flopy4.mf6.converter.egress.unstructure import unstructure_component from flopy4.mf6.converter.ingress.structure import structure_component from flopy4.mf6.gwe.lke import Lke @@ -3165,17 +3172,17 @@ def test_lke_period_roundtrip(): p0 = spd[0] assert len(p0) == 3 - assert isinstance(p0[0], Lke.Status) and p0[0].lakeno == 0 - assert p0[0].status == "ACTIVE" - assert isinstance(p0[1], Lke.Status) and p0[1].lakeno == 1 - assert p0[1].status == "CONSTANT" - assert isinstance(p0[2], Lke.Temperature) and p0[2].lakeno == 0 - assert float(p0[2].temperature) == pytest.approx(18.5) + assert p0[0].lakeno == 0 and isinstance(p0[0].laksetting, Lke.Status) + assert p0[0].laksetting.status == "ACTIVE" + assert p0[1].lakeno == 1 and isinstance(p0[1].laksetting, Lke.Status) + assert p0[1].laksetting.status == "CONSTANT" + assert p0[2].lakeno == 0 and isinstance(p0[2].laksetting, Lke.Temperature) + assert float(p0[2].laksetting.temperature) == pytest.approx(18.5) p1 = spd[1] assert len(p1) == 1 - assert isinstance(p1[0], Lke.Status) and p1[0].lakeno == 0 - assert p1[0].status == "INACTIVE" + assert p1[0].lakeno == 0 and isinstance(p1[0].laksetting, Lke.Status) + assert p1[0].laksetting.status == "INACTIVE" def test_lke_packagedata_roundtrip(): @@ -3854,8 +3861,8 @@ def test_spc_round_trip(tmp_path): ) spc = Spc.load(path) rows = spc.stress_period_data - assert [(r.bndno, r.concentration) for r in rows[0]] == [(0, 100.0), (1, TimeSeriesRef("myts"))] - assert [(r.bndno, r.concentration) for r in rows[2]] == [(0, 0.0)] + values = [[(r.bndno, r.spcsetting.concentration) for r in rows[k]] for k in (0, 2)] + assert values == [[(0, 100.0), (1, TimeSeriesRef("myts"))], [(0, 0.0)]] assert spc.maxbound == 2 dumped = dumps(COMPONENT_CONVERTER.unstructure(spc)) diff --git a/test/mf6/test_mf6_codegen.py b/test/mf6/test_mf6_codegen.py index 50033cba..e1c26ec6 100644 --- a/test/mf6/test_mf6_codegen.py +++ b/test/mf6/test_mf6_codegen.py @@ -31,11 +31,11 @@ can_expand_record, class_name, feature_id_column, - is_column_union, is_generatable, item_class, model_abbr, module_name, + needs_class, output_path, py_type, safe_name, @@ -101,9 +101,9 @@ def all_dfns(dfn_path): "gwt-ist": ("Ist", "Package", "gwt"), } -# Tier 1a: OC period keystring union — saverecord/printrecord arms become -# real typed Save/Print classes composed into stress_period_data, the same -# generic mechanism LAK/SFR/MAW use for their own period keystring settings. +# Tier 1a: OC period union — saverecord/printrecord arms become real typed +# Save/Print classes composed into stress_period_data, the same generic +# mechanism LAK/SFR/MAW use for their own period settings' union arms. # Each tuple is (class_name, base_class, model_prefix). OC_TIER = { "gwt-oc": ("Oc", "Package", "gwt"), @@ -594,21 +594,13 @@ def test_lak_ifno_collision_prefixed(self, lak_spec): f"ifno in {block} should be prefixed as {block}_ifno" ) - def test_lak_outlets_period_collision_prefixed(self, lak_spec): - # invert/width/slope/rough appear as both outlets packagedata columns and - # period field keywords — static columns must take outlets_ prefix so period - # fields can always use the bare keyword name. + def test_lak_outlets_bare_names(self, lak_spec): + # outlets' columns keep their names, even those that are also period + # setting keywords (invert/width/slope/rough): period settings + # aren't package attributes, so nothing collides bp = next(b for b in lak_spec.block_properties if b.block_name == "outlets") - for col_name in ("invert", "width", "slope", "rough"): - assert bp.attr_name_map.get(col_name) == f"outlets_{col_name}", ( - f"outlets.{col_name} should be prefixed as outlets_{col_name} " - "(collides with period field keyword)" - ) - # outletno, lakein, lakeout, couttype are not period field names — bare names - for col_name in ("outletno", "lakein", "lakeout", "couttype"): - assert bp.attr_name_map.get(col_name) == col_name, ( - f"outlets.{col_name} should use bare name (no period field conflict)" - ) + for col_name in ("outletno", "lakein", "lakeout", "couttype", "invert", "width"): + assert bp.attr_name_map.get(col_name) == col_name def test_lak_connectiondata_cellid(self, lak_spec): bp = next(b for b in lak_spec.block_properties if b.block_name == "connectiondata") @@ -1186,7 +1178,7 @@ def test_union_list_outside_period_roundtrip(tmp_path): """A list of unions gets the same arm classes and keyword dispatch in any block, not just the period block. No real DFN has one outside the period block yet, so a synthetic one.""" - from modflow_devtools.dfns.schema import Block, Package, Union + from modflow_devtools.dfns.schema import Block, Package from modflow_devtools.dfns.schema import List as ListField from flopy4.mf6.codec.reader import loads @@ -1365,19 +1357,22 @@ def test_oc_tier_generates_importable_files(tmp_path, all_dfns): # Verify the OC period arms (Save/Print, real typed classes) were generated arm_names = [ic.class_name for ic in spec.item_classes] assert {"Save", "Print"} <= set(arm_names), f"{dfn_name} should have Save/Print arm classes" - # ocsetting's own arms (All/First/Last/Frequency/Steps) must be built - # exactly once (shared by Save.ocsetting and Print.ocsetting, not - # duplicated as e.g. SaverecordAll/PrintrecordAll -- see - # make.py's _build_arm_specs_from_union nested_union_cache) and must - # NOT be folded into the top-level Save|Print dispatch union. - assert arm_names.count("All") == 1, f"{dfn_name}: ocsetting arms must not be duplicated" + # ocsetting's arm classes (Frequency/Steps; ALL/FIRST/LAST are words) + # must be built exactly once (shared by Save.ocsetting and + # Print.ocsetting, not duplicated as e.g. SaverecordFrequency -- see + # make.py's _union_arm_specs nested_union_cache) and must NOT be + # folded into the top-level Save|Print dispatch union. + assert "All" not in arm_names, f"{dfn_name}: ALL is a word, not a class" + assert arm_names.count("Frequency") == 1, ( + f"{dfn_name}: ocsetting arms must not be duplicated" + ) (period_union,) = [u for u in spec.item_unions if u.alias == "_StressPeriodDataItem"] top_level_names = set(period_union.members) assert top_level_names == {"Save", "Print"}, ( f"{dfn_name}: only Save/Print may be top_level (dispatch union) arms" ) nested_names = {ic.class_name for ic in spec.item_classes if not ic.top_level} - assert nested_names == {"All", "First", "Last", "Frequency", "Steps"} + assert nested_names == {"Frequency", "Steps"} def test_utl_tier_generates_importable_files(tmp_path, all_dfns): @@ -1499,33 +1494,20 @@ def test_counted_array_column(): assert disv.cell2d[0].icvert == (0, 1, 2, 3) -def test_is_column_union(): - """A union is one column if each arm is one value (OBS's id, LAK's - bedleak), and a keystring if its arms are line forms (MAW's mawsetting, - LAK's laksetting, whose arms lead with a lake number).""" +def test_needs_class(): + """A union arm types as a class if its keyword picks it: a record (LAK's + stage, led by a lake number) or a tagged value (MAW's status). A + keyword arm is a word, and an untagged value its own type (OBS's id, + LAK's bedleak).""" value = Double(name="value", tagged=False) - assert is_column_union( - Union(name="bedleak", arms={"none": Keyword(name="none"), "value": value}) - ) - obs_id = Union( - name="id", - arms={ - "cellid": Array(name="cellid", dtype="integer", shape=["ncelldim"], tagged=False), - "boundname": String(name="boundname", tagged=False), - }, - ) - assert is_column_union(obs_id) - assert not is_column_union( - Union( - name="mawsetting", - arms={"status": String(name="status"), "bottom": Double(name="bottom")}, - ) - ) + assert not needs_class(Keyword(name="none")) + assert not needs_class(value) + assert not needs_class(Array(name="cellid", dtype="integer", shape=["ncelldim"], tagged=False)) + assert needs_class(String(name="status")) lakeno = Integer(name="lakeno", tagged=False) - stage = Record( - name="stage", fields={"lakeno": lakeno, "stage": Keyword(name="stage"), "v": value} + assert needs_class( + Record(name="stage", fields={"lakeno": lakeno, "stage": Keyword(name="stage"), "v": value}) ) - assert not is_column_union(Union(name="laksetting", arms={"stage": stage})) def test_number_or_word_column(): @@ -1553,14 +1535,14 @@ def test_number_or_word_from_tokens(bedleak): assert lower.bedleak == bedleak -def test_number_or_word_in_keystring_arm(): +def test_number_or_word_in_union_arm(): """MAW's HEAD_LIMIT takes OFF or a number.""" from flopy4.mf6.gwf import Maw - off = Maw.HeadLimit.from_tokens([1, "head_limit", "off"]) - assert (off.ifno, off.head_limit) == (0, "OFF") - limit = Maw.HeadLimit.from_tokens([1, "HEAD_LIMIT", 2.5]) - assert limit.head_limit == 2.5 + off = Maw.StressPeriodData.from_tokens([1, "head_limit", "off"]) + assert (off.ifno, off.mawsetting) == (0, Maw.HeadLimit(head_limit="OFF")) + limit = Maw.StressPeriodData.from_tokens([1, "HEAD_LIMIT", 2.5]) + assert limit.mawsetting.head_limit == 2.5 assert limit.to_tokens() == (1, "HEAD_LIMIT", 2.5) From ee0debd0bdf82bcd341d32fe4deea281f94f91fb Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 15:53:11 -0700 Subject: [PATCH 05/10] bump modflow-devtools for LAK's period union devtools now makes LAK's period element its laksetting union (#391), so LAK's rows are its arms again, as before. Also put `# noqa: E501` only on long union annotations, not on every column ruff wraps. Co-Authored-By: Claude Opus 5.5 --- .../reader/grammar/generated/gwf-lak.lark | 43 +++++++++++++++- flopy4/mf6/gwe/ctp.py | 2 +- flopy4/mf6/gwe/disu.py | 2 +- flopy4/mf6/gwe/disv.py | 2 +- flopy4/mf6/gwe/esl.py | 2 +- flopy4/mf6/gwe/lke.py | 2 +- flopy4/mf6/gwf/chd.py | 2 +- flopy4/mf6/gwf/disu.py | 2 +- flopy4/mf6/gwf/disv.py | 2 +- flopy4/mf6/gwf/drn.py | 2 +- flopy4/mf6/gwf/evt.py | 8 +-- flopy4/mf6/gwf/ghb.py | 2 +- flopy4/mf6/gwf/lak.py | 23 ++++++--- flopy4/mf6/gwf/maw.py | 2 +- flopy4/mf6/gwf/rch.py | 2 +- flopy4/mf6/gwf/riv.py | 2 +- flopy4/mf6/gwf/sfr.py | 4 +- flopy4/mf6/gwf/uzf.py | 2 +- flopy4/mf6/gwf/wel.py | 2 +- flopy4/mf6/gwt/cnc.py | 2 +- flopy4/mf6/gwt/disu.py | 2 +- flopy4/mf6/gwt/disv.py | 2 +- flopy4/mf6/gwt/lkt.py | 2 +- flopy4/mf6/gwt/src.py | 2 +- flopy4/mf6/prt/disv.py | 2 +- flopy4/mf6/utils/codegen/filters.py | 2 +- flopy4/mf6/utl/obs.py | 2 +- pixi.lock | 50 +++++++++---------- test/mf6/test_mf6_codec.py | 30 ++++++----- 29 files changed, 126 insertions(+), 78 deletions(-) diff --git a/flopy4/mf6/codec/reader/grammar/generated/gwf-lak.lark b/flopy4/mf6/codec/reader/grammar/generated/gwf-lak.lark index c1c21d1e..dba367b6 100644 --- a/flopy4/mf6/codec/reader/grammar/generated/gwf-lak.lark +++ b/flopy4/mf6/codec/reader/grammar/generated/gwf-lak.lark @@ -37,7 +37,7 @@ packagedata__fields: ((packagedatadata | open_close_redirect) _NL*)* connectiondata__fields: ((connectiondatadata | open_close_redirect) _NL*)* tables__fields: ((tablesdata | open_close_redirect) _NL*)* outlets__fields: ((outletsdata | open_close_redirect) _NL*)* -period__fields: ((stress_period_data | open_close_redirect) _NL*)* +period__fields: ((status__rec__elem | stage__rec__elem | rainfall__rec__elem | evaporation__rec__elem | runoff__rec__elem | inflow__rec__elem | withdrawal__rec__elem | rate__rec__elem | invert__rec__elem | width__rec__elem | slope__rec__elem | rough__rec__elem | auxiliaryrecord__rec__elem | open_close_redirect) _NL*)* auxiliary__val: "auxiliary"i word+ [_remark] _NL boundnames__kw: "boundnames"i [_remark] _NL print_input__kw: "print_input"i [_remark] _NL @@ -71,8 +71,47 @@ dev_no_final_check__kw: "dev_no_final_check"i [_remark] _NL nlakes__val: "nlakes"i integer [_remark] _NL noutlets__val: "noutlets"i integer [_remark] _NL ntables__val: "ntables"i integer [_remark] _NL +status__rec__elem: status__lakeno__val status__status__val [_remark] _NL +status__lakeno__val: integer +status__status__val: "status"i word +stage__rec__elem: stage__lakeno__val stage__stage__val [_remark] _NL +stage__lakeno__val: integer +stage__stage__val: "stage"i double +rainfall__rec__elem: rainfall__lakeno__val rainfall__rainfall__val [_remark] _NL +rainfall__lakeno__val: integer +rainfall__rainfall__val: "rainfall"i double +evaporation__rec__elem: evaporation__lakeno__val evaporation__evaporation__val [_remark] _NL +evaporation__lakeno__val: integer +evaporation__evaporation__val: "evaporation"i double +runoff__rec__elem: runoff__lakeno__val runoff__runoff__val [_remark] _NL +runoff__lakeno__val: integer +runoff__runoff__val: "runoff"i double +inflow__rec__elem: inflow__lakeno__val inflow__inflow__val [_remark] _NL +inflow__lakeno__val: integer +inflow__inflow__val: "inflow"i double +withdrawal__rec__elem: withdrawal__lakeno__val withdrawal__withdrawal__val [_remark] _NL +withdrawal__lakeno__val: integer +withdrawal__withdrawal__val: "withdrawal"i double +rate__rec__elem: rate__outletno__val rate__rate__val [_remark] _NL +rate__outletno__val: integer +rate__rate__val: "rate"i double +invert__rec__elem: invert__outletno__val invert__invert__val [_remark] _NL +invert__outletno__val: integer +invert__invert__val: "invert"i double +width__rec__elem: width__outletno__val width__width__val [_remark] _NL +width__outletno__val: integer +width__width__val: "width"i double +slope__rec__elem: slope__outletno__val slope__slope__val [_remark] _NL +slope__outletno__val: integer +slope__slope__val: "slope"i double +rough__rec__elem: rough__outletno__val rough__rough__val [_remark] _NL +rough__outletno__val: integer +rough__rough__val: "rough"i double +auxiliaryrecord__rec__elem: auxiliaryrecord__lakeno__val "auxiliary"i auxiliaryrecord__auxname__val auxiliaryrecord__auxval__val [_remark] _NL +auxiliaryrecord__lakeno__val: integer +auxiliaryrecord__auxname__val: word +auxiliaryrecord__auxval__val: double packagedatadata: record+ connectiondatadata: record+ tablesdata: record+ outletsdata: record+ -stress_period_data: record+ diff --git a/flopy4/mf6/gwe/ctp.py b/flopy4/mf6/gwe/ctp.py index 2e264f25..1073ee07 100644 --- a/flopy4/mf6/gwe/ctp.py +++ b/flopy4/mf6/gwe/ctp.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): temp: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwe/disu.py b/flopy4/mf6/gwe/disu.py index f6c68e8e..f079649d 100644 --- a/flopy4/mf6/gwe/disu.py +++ b/flopy4/mf6/gwe/disu.py @@ -33,7 +33,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwe/disv.py b/flopy4/mf6/gwe/disv.py index fba71e02..6c9c2a3a 100644 --- a/flopy4/mf6/gwe/disv.py +++ b/flopy4/mf6/gwe/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwe/esl.py b/flopy4/mf6/gwe/esl.py index 4ef59df3..142f1c6b 100644 --- a/flopy4/mf6/gwe/esl.py +++ b/flopy4/mf6/gwe/esl.py @@ -30,7 +30,7 @@ class StressPeriodData(Item): senerrate: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwe/lke.py b/flopy4/mf6/gwe/lke.py index e4518857..95990938 100644 --- a/flopy4/mf6/gwe/lke.py +++ b/flopy4/mf6/gwe/lke.py @@ -44,7 +44,7 @@ class Packagedata(Item): rbthcnd: float = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) @attrs.define diff --git a/flopy4/mf6/gwf/chd.py b/flopy4/mf6/gwf/chd.py index 09507d3a..e8a097d0 100644 --- a/flopy4/mf6/gwf/chd.py +++ b/flopy4/mf6/gwf/chd.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): head: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/disu.py b/flopy4/mf6/gwf/disu.py index 5f170b20..df6a092b 100644 --- a/flopy4/mf6/gwf/disu.py +++ b/flopy4/mf6/gwf/disu.py @@ -33,7 +33,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwf/disv.py b/flopy4/mf6/gwf/disv.py index b7366eb4..3062693a 100644 --- a/flopy4/mf6/gwf/disv.py +++ b/flopy4/mf6/gwf/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwf/drn.py b/flopy4/mf6/gwf/drn.py index 159b89d7..82c97b38 100644 --- a/flopy4/mf6/gwf/drn.py +++ b/flopy4/mf6/gwf/drn.py @@ -31,7 +31,7 @@ class StressPeriodData(Item): cond: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/evt.py b/flopy4/mf6/gwf/evt.py index 048f0b18..2250df0f 100644 --- a/flopy4/mf6/gwf/evt.py +++ b/flopy4/mf6/gwf/evt.py @@ -31,16 +31,16 @@ class StressPeriodData(Item): depth: Union[float, TimeSeriesRef] = field(time_series=True) pxdp: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("nseg-1",), time_series=True, optional=True - ) # noqa: E501 + ) petm: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("nseg-1",), time_series=True, optional=True - ) # noqa: E501 + ) petm0: Optional[Union[float, TimeSeriesRef]] = field( default=None, time_series=True, optional=True - ) # noqa: E501 + ) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) fixed_cell: bool = field( diff --git a/flopy4/mf6/gwf/ghb.py b/flopy4/mf6/gwf/ghb.py index 389483b7..159efc6e 100644 --- a/flopy4/mf6/gwf/ghb.py +++ b/flopy4/mf6/gwf/ghb.py @@ -31,7 +31,7 @@ class StressPeriodData(Item): cond: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/lak.py b/flopy4/mf6/gwf/lak.py index bbeea4e6..da359eab 100644 --- a/flopy4/mf6/gwf/lak.py +++ b/flopy4/mf6/gwf/lak.py @@ -50,7 +50,7 @@ class Packagedata(Item): nlakeconn: int = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) @attrs.define @@ -160,9 +160,21 @@ class Auxiliary(Item): auxname: str = field() auxval: Union[float, TimeSeriesRef] = field(time_series=True) - @attrs.define - class StressPeriodData(Item): - laksetting: "Lak.Status | Lak.Stage | Lak.Rainfall | Lak.Evaporation | Lak.Runoff | Lak.Inflow | Lak.Withdrawal | Lak.Rate | Lak.Invert | Lak.Width | Lak.Slope | Lak.Rough | Lak.Auxiliary" = field() # noqa: E501 + _StressPeriodDataItem = ( + Status + | Stage + | Rainfall + | Evaporation + | Runoff + | Inflow + | Withdrawal + | Rate + | Invert + | Width + | Slope + | Rough + | Auxiliary + ) auxiliary: Optional[NDArray[np.str_]] = field( default=None, @@ -313,7 +325,7 @@ class StressPeriodData(Item): block="outlets", dim="noutlets", ) - _stress_period_data: Optional[dict[int, list[StressPeriodData]]] = field( + _stress_period_data: Optional[dict[int, list[_StressPeriodDataItem]]] = field( alias="stress_period_data", default=None, repr=False, @@ -339,4 +351,3 @@ class StressPeriodData(Item): LakSlope = Lak.Slope LakRough = Lak.Rough LakAuxiliary = Lak.Auxiliary -LakStressPeriodData = Lak.StressPeriodData diff --git a/flopy4/mf6/gwf/maw.py b/flopy4/mf6/gwf/maw.py index 34dfcd7a..d67d1372 100644 --- a/flopy4/mf6/gwf/maw.py +++ b/flopy4/mf6/gwf/maw.py @@ -44,7 +44,7 @@ class Packagedata(Item): ngwfnodes: int = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) @attrs.define diff --git a/flopy4/mf6/gwf/rch.py b/flopy4/mf6/gwf/rch.py index 413f9648..ee3e5558 100644 --- a/flopy4/mf6/gwf/rch.py +++ b/flopy4/mf6/gwf/rch.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): recharge: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) fixed_cell: bool = field( diff --git a/flopy4/mf6/gwf/riv.py b/flopy4/mf6/gwf/riv.py index 10f5a2f6..9a20ab7a 100644 --- a/flopy4/mf6/gwf/riv.py +++ b/flopy4/mf6/gwf/riv.py @@ -32,7 +32,7 @@ class StressPeriodData(Item): rbot: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwf/sfr.py b/flopy4/mf6/gwf/sfr.py index e28b850d..d7c76d2b 100644 --- a/flopy4/mf6/gwf/sfr.py +++ b/flopy4/mf6/gwf/sfr.py @@ -57,7 +57,7 @@ class Packagedata(Item): ndv: int = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) @attrs.define @@ -75,7 +75,7 @@ class Connectiondata(Item): shape=("packagedata.ncon(ifno)",), signed=True, optional=True, - ) # noqa: E501 + ) @attrs.define class Diversions(Item): diff --git a/flopy4/mf6/gwf/uzf.py b/flopy4/mf6/gwf/uzf.py index 993135a7..9476e2ad 100644 --- a/flopy4/mf6/gwf/uzf.py +++ b/flopy4/mf6/gwf/uzf.py @@ -61,7 +61,7 @@ class StressPeriodData(Item): rootact: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) auxiliary: Optional[NDArray[np.str_]] = field( default=None, diff --git a/flopy4/mf6/gwf/wel.py b/flopy4/mf6/gwf/wel.py index dfdd6c2f..6a8d3281 100644 --- a/flopy4/mf6/gwf/wel.py +++ b/flopy4/mf6/gwf/wel.py @@ -32,7 +32,7 @@ class StressPeriodData(Item): q: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwt/cnc.py b/flopy4/mf6/gwt/cnc.py index 2062b4fd..4729ceeb 100644 --- a/flopy4/mf6/gwt/cnc.py +++ b/flopy4/mf6/gwt/cnc.py @@ -29,7 +29,7 @@ class StressPeriodData(Item): conc: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/gwt/disu.py b/flopy4/mf6/gwt/disu.py index d12489df..46f4ed17 100644 --- a/flopy4/mf6/gwt/disu.py +++ b/flopy4/mf6/gwt/disu.py @@ -33,7 +33,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwt/disv.py b/flopy4/mf6/gwt/disv.py index 812c52af..57aa9c2c 100644 --- a/flopy4/mf6/gwt/disv.py +++ b/flopy4/mf6/gwt/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwt/lkt.py b/flopy4/mf6/gwt/lkt.py index 624e1386..17134dd3 100644 --- a/flopy4/mf6/gwt/lkt.py +++ b/flopy4/mf6/gwt/lkt.py @@ -42,7 +42,7 @@ class Packagedata(Item): strt: float = field() aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) @attrs.define diff --git a/flopy4/mf6/gwt/src.py b/flopy4/mf6/gwt/src.py index 8a9ce753..dd3729d8 100644 --- a/flopy4/mf6/gwt/src.py +++ b/flopy4/mf6/gwt/src.py @@ -30,7 +30,7 @@ class StressPeriodData(Item): smassrate: Union[float, TimeSeriesRef] = field(time_series=True) aux: tuple[Union[float, TimeSeriesRef], ...] = field( default=(), array=True, shape=("auxiliary",), time_series=True, optional=True - ) # noqa: E501 + ) boundname: Optional[str] = field(default=None, optional=True) auxiliary: Optional[NDArray[np.str_]] = field( diff --git a/flopy4/mf6/prt/disv.py b/flopy4/mf6/prt/disv.py index b9882c27..5f760523 100644 --- a/flopy4/mf6/prt/disv.py +++ b/flopy4/mf6/prt/disv.py @@ -34,7 +34,7 @@ class Cell2d(Item): ncvert: Optional[int] = field(default=None, optional=True) icvert: tuple[int, ...] = field( default=(), array=True, index=True, shape=("ncvert",), optional=True - ) # noqa: E501 + ) length_units: Optional[str] = field( default=None, diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index 7f0359d5..c270ba78 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -805,7 +805,7 @@ def line(self, package_class_name: str = "") -> str: annotation = f'"{" | ".join(arms)}"' line = f" {self.name}: {annotation} = {self.rhs}" # a string annotation can't wrap - return line + " # noqa: E501" if len(line) > 100 else line + return line + " # noqa: E501" if self.arm_classes and len(line) > 100 else line def _margs(meta: dict) -> str: diff --git a/flopy4/mf6/utl/obs.py b/flopy4/mf6/utl/obs.py index da0c6acf..779dc206 100644 --- a/flopy4/mf6/utl/obs.py +++ b/flopy4/mf6/utl/obs.py @@ -21,7 +21,7 @@ class Continuous(Item): id_: Union[tuple[int, ...], int, str] = field(union=("cellid", "index", "string")) id2: Optional[Union[tuple[int, ...], int, str]] = field( default=None, union=("cellid", "index", "string"), optional=True - ) # noqa: E501 + ) @attrs.define class Output(Item): diff --git a/pixi.lock b/pixi.lock index df6ff02b..12fdf154 100644 --- a/pixi.lock +++ b/pixi.lock @@ -106,7 +106,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -255,7 +255,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -404,7 +404,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -547,7 +547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -709,7 +709,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -987,7 +987,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1263,7 +1263,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1533,7 +1533,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1822,7 +1822,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2089,7 +2089,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2357,7 +2357,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2618,7 +2618,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2898,7 +2898,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3143,7 +3143,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3389,7 +3389,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz @@ -3629,7 +3629,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3887,7 +3887,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -4129,7 +4129,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -4372,7 +4372,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl @@ -4609,7 +4609,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl @@ -4863,7 +4863,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5107,7 +5107,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl @@ -5352,7 +5352,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5591,7 +5591,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - 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 @@ -10852,7 +10852,7 @@ packages: - build ; extra == 'build' - twine ; extra == 'build' requires_python: '>=3.11,<3.14' -- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#7df4ae9713ab1050879c65008cfc7f38215695ed +- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 name: modflow-devtools version: 1.10.0.dev1 requires_dist: diff --git a/test/mf6/test_mf6_codec.py b/test/mf6/test_mf6_codec.py index 416ea640..8148e8da 100644 --- a/test/mf6/test_mf6_codec.py +++ b/test/mf6/test_mf6_codec.py @@ -2843,30 +2843,28 @@ def test_lak_period_roundtrip(): # Period 0: 4 rows p0 = spd[0] assert len(p0) == 4 - assert ( - isinstance(p0[0].laksetting, Lak.Status) and p0[0].laksetting.lakeno == 0 - ) # 0-based feature id - assert p0[0].laksetting.status == "ACTIVE" - assert isinstance(p0[1].laksetting, Lak.Rainfall) and p0[1].laksetting.lakeno == 0 - assert float(p0[1].laksetting.rainfall) == pytest.approx(0.1) - assert isinstance(p0[2].laksetting, Lak.Status) and p0[2].laksetting.lakeno == 1 - assert p0[2].laksetting.status == "CONSTANT" - assert isinstance(p0[3].laksetting, Lak.Stage) and p0[3].laksetting.lakeno == 1 - assert float(p0[3].laksetting.stage) == pytest.approx(5.0) + assert isinstance(p0[0], Lak.Status) and p0[0].lakeno == 0 # 0-based feature id + assert p0[0].status == "ACTIVE" + assert isinstance(p0[1], Lak.Rainfall) and p0[1].lakeno == 0 + assert float(p0[1].rainfall) == pytest.approx(0.1) + assert isinstance(p0[2], Lak.Status) and p0[2].lakeno == 1 + assert p0[2].status == "CONSTANT" + assert isinstance(p0[3], Lak.Stage) and p0[3].lakeno == 1 + assert float(p0[3].stage) == pytest.approx(5.0) # Period 1: 1 row p1 = spd[1] assert len(p1) == 1 - assert isinstance(p1[0].laksetting, Lak.Status) and p1[0].laksetting.lakeno == 0 - assert p1[0].laksetting.status == "INACTIVE" + assert isinstance(p1[0], Lak.Status) and p1[0].lakeno == 0 + assert p1[0].status == "INACTIVE" # Period 2: 2 rows p2 = spd[2] assert len(p2) == 2 - assert isinstance(p2[0].laksetting, Lak.Status) and p2[0].laksetting.lakeno == 0 - assert p2[0].laksetting.status == "ACTIVE" - assert isinstance(p2[1].laksetting, Lak.Withdrawal) and p2[1].laksetting.lakeno == 1 - assert float(p2[1].laksetting.withdrawal) == pytest.approx(100.0) + assert isinstance(p2[0], Lak.Status) and p2[0].lakeno == 0 + assert p2[0].status == "ACTIVE" + assert isinstance(p2[1], Lak.Withdrawal) and p2[1].lakeno == 1 + assert float(p2[1].withdrawal) == pytest.approx(100.0) # --------------------------------------------------------------------------- From 8213e56b9d6cdb6b8063ebe11235d0e7580a4224 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 16:26:36 -0700 Subject: [PATCH 06/10] build a union field's arm from its values `Maw.StressPeriodData(ifno=0, mawsetting=("status", "active"))` was accepted and written as a raw tuple. A union field with arm classes now converts, on init and on assignment, a tuple to the arm its keyword picks, or a word to the word, and raises if no arm takes the value. Co-Authored-By: Claude Opus 5.5 --- flopy4/mf6/gwe/lke.py | 4 +++- flopy4/mf6/gwe/oc.py | 4 ++-- flopy4/mf6/gwf/maw.py | 4 +++- flopy4/mf6/gwf/oc.py | 4 ++-- flopy4/mf6/gwf/sfr.py | 4 +++- flopy4/mf6/gwt/lkt.py | 4 +++- flopy4/mf6/gwt/oc.py | 4 ++-- flopy4/mf6/item.py | 12 ++++++++++++ flopy4/mf6/prt/oc.py | 4 ++-- flopy4/mf6/spec.py | 8 +++++++- flopy4/mf6/utils/codegen/filters.py | 11 ++++++++--- flopy4/mf6/utl/spc.py | 2 +- flopy4/mf6/utl/tvk.py | 2 +- flopy4/mf6/utl/tvs.py | 2 +- test/mf6/test_mf6_codec.py | 16 ++++++++++++++++ 15 files changed, 66 insertions(+), 19 deletions(-) diff --git a/flopy4/mf6/gwe/lke.py b/flopy4/mf6/gwe/lke.py index 95990938..e49be774 100644 --- a/flopy4/mf6/gwe/lke.py +++ b/flopy4/mf6/gwe/lke.py @@ -86,7 +86,9 @@ class Auxiliary(Item): @attrs.define class StressPeriodData(Item): lakeno: int = field(index=True, fk="packagedata.lakeno") - laksetting: "Lke.Status | Lke.Temperature | Lke.Rainfall | Lke.Evaporation | Lke.Runoff | Lke.ExtInflow | Lke.Auxiliary" = field() # noqa: E501 + laksetting: "Lke.Status | Lke.Temperature | Lke.Rainfall | Lke.Evaporation | Lke.Runoff | Lke.ExtInflow | Lke.Auxiliary" = field( # noqa: E501 + arms=True + ) flow_package_name: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwe/oc.py b/flopy4/mf6/gwe/oc.py index 40182364..75be1d71 100644 --- a/flopy4/mf6/gwe/oc.py +++ b/flopy4/mf6/gwe/oc.py @@ -42,13 +42,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) _StressPeriodDataItem = Save | Print diff --git a/flopy4/mf6/gwf/maw.py b/flopy4/mf6/gwf/maw.py index d67d1372..8d59399d 100644 --- a/flopy4/mf6/gwf/maw.py +++ b/flopy4/mf6/gwf/maw.py @@ -123,7 +123,9 @@ class Auxiliary(Item): @attrs.define class StressPeriodData(Item): ifno: int = field(index=True, fk="packagedata.ifno") - mawsetting: "Maw.Status | Maw.ConnectionStatus | Maw.Bottom | Maw.FlowingWell | Maw.Rate | Maw.WellHead | Maw.HeadLimit | Maw.Shutoff | Maw.RateScaling | Maw.Auxiliary" = field() # noqa: E501 + mawsetting: "Maw.Status | Maw.ConnectionStatus | Maw.Bottom | Maw.FlowingWell | Maw.Rate | Maw.WellHead | Maw.HeadLimit | Maw.Shutoff | Maw.RateScaling | Maw.Auxiliary" = field( # noqa: E501 + arms=True + ) auxiliary: Optional[NDArray[np.str_]] = field( default=None, diff --git a/flopy4/mf6/gwf/oc.py b/flopy4/mf6/gwf/oc.py index e46a14ba..f21d2213 100644 --- a/flopy4/mf6/gwf/oc.py +++ b/flopy4/mf6/gwf/oc.py @@ -42,13 +42,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) _StressPeriodDataItem = Save | Print diff --git a/flopy4/mf6/gwf/sfr.py b/flopy4/mf6/gwf/sfr.py index d7c76d2b..9a226620 100644 --- a/flopy4/mf6/gwf/sfr.py +++ b/flopy4/mf6/gwf/sfr.py @@ -154,7 +154,9 @@ class Auxiliary(Item): @attrs.define class StressPeriodData(Item): ifno: int = field(index=True, fk="packagedata.ifno") - sfrsetting: "Sfr.Status | Sfr.Bedk | Sfr.Manning | Sfr.Stage | Sfr.Inflow | Sfr.Rainfall | Sfr.Evaporation | Sfr.Runoff | Sfr.Diversion | Sfr.UpstreamFraction | Sfr.CrossSection | Sfr.Auxiliary" = field() # noqa: E501 + sfrsetting: "Sfr.Status | Sfr.Bedk | Sfr.Manning | Sfr.Stage | Sfr.Inflow | Sfr.Rainfall | Sfr.Evaporation | Sfr.Runoff | Sfr.Diversion | Sfr.UpstreamFraction | Sfr.CrossSection | Sfr.Auxiliary" = field( # noqa: E501 + arms=True + ) storage: bool = field( default=False, diff --git a/flopy4/mf6/gwt/lkt.py b/flopy4/mf6/gwt/lkt.py index 17134dd3..c1b5c376 100644 --- a/flopy4/mf6/gwt/lkt.py +++ b/flopy4/mf6/gwt/lkt.py @@ -84,7 +84,9 @@ class Auxiliary(Item): @attrs.define class StressPeriodData(Item): ifno: int = field(index=True, fk="packagedata.ifno") - laksetting: "Lkt.Status | Lkt.Concentration | Lkt.Rainfall | Lkt.Evaporation | Lkt.Runoff | Lkt.ExtInflow | Lkt.Auxiliary" = field() # noqa: E501 + laksetting: "Lkt.Status | Lkt.Concentration | Lkt.Rainfall | Lkt.Evaporation | Lkt.Runoff | Lkt.ExtInflow | Lkt.Auxiliary" = field( # noqa: E501 + arms=True + ) flow_package_name: Optional[str] = field( default=None, diff --git a/flopy4/mf6/gwt/oc.py b/flopy4/mf6/gwt/oc.py index 877fc678..4eb4400c 100644 --- a/flopy4/mf6/gwt/oc.py +++ b/flopy4/mf6/gwt/oc.py @@ -42,13 +42,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) _StressPeriodDataItem = Save | Print diff --git a/flopy4/mf6/item.py b/flopy4/mf6/item.py index 34c52d7d..8dc5a06b 100644 --- a/flopy4/mf6/item.py +++ b/flopy4/mf6/item.py @@ -170,6 +170,18 @@ def _union_field_arms( return _union_arms(cls, t) if t is not None else None # type: ignore[arg-type] +def to_union_arm(value: Any, self: Any, f: attrs.Attribute) -> Any: + """Convert a union field's value (OC's ocsetting, LKT's laksetting) to + one of its words or arm instances: an arm's values build it by its + keyword, ``("STATUS", "ACTIVE")`` a Status. Raises if no arm takes it.""" + if value is None or (arms := _union_field_arms(type(self), f)) is None: + return value + values = list(value) if isinstance(value, (list, tuple)) else [value] + if (converted := _union_value(values, arms)) is None: + raise ValueError(f"{type(self).__name__}.{f.name}: no arm of {f.type} takes {value!r}") + return converted + + def _union_value(values: list, arms: "tuple[tuple[str, ...], tuple[type[Item], ...]]") -> Any: """A union field's value from the user-supplied values it spans: one of its words, an arm instance given as is, or an arm built from them by diff --git a/flopy4/mf6/prt/oc.py b/flopy4/mf6/prt/oc.py index 38092a3b..deee084e 100644 --- a/flopy4/mf6/prt/oc.py +++ b/flopy4/mf6/prt/oc.py @@ -31,13 +31,13 @@ class Steps(Item): class Save(Item): _keyword: ClassVar[str] = "save" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) @attrs.define class Print(Item): _keyword: ClassVar[str] = "print" rtype: str = field() - ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field() + ocsetting: "Literal['ALL', 'FIRST', 'LAST'] | Oc.Frequency | Oc.Steps" = field(arms=True) _StressPeriodDataItem = Save | Print diff --git a/flopy4/mf6/spec.py b/flopy4/mf6/spec.py index f3f5c004..a91729d8 100644 --- a/flopy4/mf6/spec.py +++ b/flopy4/mf6/spec.py @@ -21,6 +21,7 @@ to_ts_value, to_ts_values, ) +from flopy4.mf6.item import to_union_arm from flopy4.spec import fields_dict as flopy_fields_dict FieldType = Literal["keyword", "integer", "double", "string", "list", "record"] @@ -82,13 +83,16 @@ def field( array: bool = False, signed: bool = False, union: tuple[str, ...] | None = None, + arms: bool = False, ): """Define a field: always a plain ``attrs.field()``. A field with a ``shape`` holds an array, so unless ``eq`` is given it compares with `array_eq` (numpy's elementwise ``==`` isn't a valid ``__eq__`` result). An ``array`` column of a table row (`Item`) is a - tuple, compared as such; its ``shape`` is the dimension sizing it. + tuple, compared as such; its ``shape`` is the dimension sizing it. A + row's union of words and arm classes (``arms``) builds its arm from + the values given for it (see `item.to_union_arm`). """ if eq is None: eq = ARRAY_EQ if shape and not array else True @@ -148,6 +152,8 @@ def field( if signed: metadata["signed"] = True converter = converter or to_signed_indexes + if arms: + converter = converter or attrs.Converter(to_union_arm, takes_self=True, takes_field=True) return attrs.field( default=default, validator=validator, diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index c270ba78..9ae6c38e 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -804,8 +804,13 @@ def line(self, package_class_name: str = "") -> str: arms.insert(0, f"Literal[{', '.join(repr(w.upper()) for w in self.words)}]") annotation = f'"{" | ".join(arms)}"' line = f" {self.name}: {annotation} = {self.rhs}" - # a string annotation can't wrap - return line + " # noqa: E501" if self.arm_classes and len(line) > 100 else line + if self.arm_classes and len(line) > 100: + # a string annotation can't wrap, so wrap the call as ruff would, + # keeping the comment on the long line + args = self.rhs.removeprefix("field(").removesuffix(")") + first = f" {self.name}: {annotation} = field( # noqa: E501" + return f"{first}\n {args}\n )" + return line def _margs(meta: dict) -> str: @@ -1033,7 +1038,7 @@ def class_union_column( return ItemColumn( name=name, annotation="", - rhs="field()", + rhs="field(arms=True)", optional=optional, arm_classes=tuple(arm_classes), words=tuple(words), diff --git a/flopy4/mf6/utl/spc.py b/flopy4/mf6/utl/spc.py index a5354f09..abe85e4e 100644 --- a/flopy4/mf6/utl/spc.py +++ b/flopy4/mf6/utl/spc.py @@ -29,7 +29,7 @@ class Temperature(Item): @attrs.define class StressPeriodData(Item): bndno: int = field(index=True, pk=True) - spcsetting: "Spc.Concentration | Spc.Temperature" = field() + spcsetting: "Spc.Concentration | Spc.Temperature" = field(arms=True) print_input: bool = field( default=False, diff --git a/flopy4/mf6/utl/tvk.py b/flopy4/mf6/utl/tvk.py index 45dc8169..70f17e03 100644 --- a/flopy4/mf6/utl/tvk.py +++ b/flopy4/mf6/utl/tvk.py @@ -32,7 +32,7 @@ class K33(Item): @attrs.define class StressPeriodData(Item): cellid: tuple = field(cellid=True) - tvksetting: "Tvk.K | Tvk.K22 | Tvk.K33" = field() + tvksetting: "Tvk.K | Tvk.K22 | Tvk.K33" = field(arms=True) print_input: bool = field( default=False, diff --git a/flopy4/mf6/utl/tvs.py b/flopy4/mf6/utl/tvs.py index daca523b..82ea9dc1 100644 --- a/flopy4/mf6/utl/tvs.py +++ b/flopy4/mf6/utl/tvs.py @@ -27,7 +27,7 @@ class Sy(Item): @attrs.define class StressPeriodData(Item): cellid: tuple = field(cellid=True) - tvssetting: "Tvs.Ss | Tvs.Sy" = field() + tvssetting: "Tvs.Ss | Tvs.Sy" = field(arms=True) disable_storage_change_integration: bool = field( default=False, diff --git a/test/mf6/test_mf6_codec.py b/test/mf6/test_mf6_codec.py index 8148e8da..f6e80bb2 100644 --- a/test/mf6/test_mf6_codec.py +++ b/test/mf6/test_mf6_codec.py @@ -699,6 +699,22 @@ def test_record_field_set_after_construction(): unstructure_component(npf) +def test_union_field_takes_arm_values(): + """A row's union field builds its arm from the arm's values, by keyword, + or takes one of its words; anything else raises.""" + from flopy4.mf6.gwf import Maw, Oc + + row = Maw.StressPeriodData(ifno=0, mawsetting=("status", "active")) + assert row.mawsetting == Maw.Status(status="active") + assert row.to_tokens() == (1, "STATUS", "active") + row.mawsetting = ("RATE", -1.0) + assert row.mawsetting == Maw.Rate(rate=-1.0) + assert Oc.Save(rtype="HEAD", ocsetting="all").ocsetting == "ALL" + assert Oc.Save(rtype="HEAD", ocsetting=("frequency", 2)).ocsetting == Oc.Frequency(2) + with pytest.raises(ValueError, match="no arm"): + Maw.StressPeriodData(ifno=0, mawsetting=("bogus", 1)) + + def test_maw_round_trip(): from flopy4.mf6.converter.egress.unstructure import unstructure_component from flopy4.mf6.converter.ingress.structure import structure_component From 87d4032a911c406eada38eb659ed35a9791ff3a1 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 16:37:48 -0700 Subject: [PATCH 07/10] sync DFNs and models separately in CI devtools' `mf sync` now fails if any registry does, and the mf6/example models registry is missing its models.toml. Fail on a DFN sync failure, but only warn on a models one for now. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df453f2e..3fc730c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,9 +102,13 @@ jobs: environments: test${{ matrix.python }} activate-environment: true - # temporary. devtools 2.x will autosync, but 1.x needs opt-in. + # temporary. devtools 2.x will autosync, but 1.x needs opt-in. A DFN + # sync failure fails the job; a models one doesn't yet, since the + # mf6/example registry is missing its models.toml. - name: Sync devtools - run: pixi run mf sync + run: | + pixi run mf dfns sync + pixi run mf models sync || echo "::warning::models registry sync failed" - name: Show MF6 sync status run: pixi run flopy4 mf6 status @@ -132,9 +136,13 @@ jobs: environments: test313 activate-environment: true - # temporary. devtools 2.x will autosync, but 1.x needs opt-in. + # temporary. devtools 2.x will autosync, but 1.x needs opt-in. A DFN + # sync failure fails the job; a models one doesn't yet, since the + # mf6/example registry is missing its models.toml. - name: Sync devtools - run: pixi run mf sync + run: | + pixi run mf dfns sync + pixi run mf models sync || echo "::warning::models registry sync failed" - name: Sync classes and binary to the latest MF6 release env: From 3530e661937fd203cd66e6f227aa1f043510c1aa Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 16:41:25 -0700 Subject: [PATCH 08/10] run CI's devtools sync in bash, with a token pwsh exits with the last native command's code, so Windows still failed on the models sync. Pass GITHUB_TOKEN for the DFN sync's API call, which devtools will send once it authenticates that lookup. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fc730c1..286cfa83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,10 @@ jobs: # sync failure fails the job; a models one doesn't yet, since the # mf6/example registry is missing its models.toml. - name: Sync devtools + # bash on Windows too: pwsh exits with the models sync's code + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} run: | pixi run mf dfns sync pixi run mf models sync || echo "::warning::models registry sync failed" @@ -140,6 +144,10 @@ jobs: # sync failure fails the job; a models one doesn't yet, since the # mf6/example registry is missing its models.toml. - name: Sync devtools + # bash on Windows too: pwsh exits with the models sync's code + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} run: | pixi run mf dfns sync pixi run mf models sync || echo "::warning::models registry sync failed" From 907aaec64089ad126517f266e14f1a5bcc18e13c Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 17:12:56 -0700 Subject: [PATCH 09/10] bump modflow-devtools for authenticated DFN sync Co-Authored-By: Claude Opus 5.5 --- pixi.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pixi.lock b/pixi.lock index 12fdf154..cf78b132 100644 --- a/pixi.lock +++ b/pixi.lock @@ -106,7 +106,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -255,7 +255,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -404,7 +404,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl @@ -547,7 +547,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/70/746ab7140121dcf04bade9ab7d2e65b6d5eb5b23bb7dc04b7ad22742ca0f/xugrid-0.15.3-py3-none-any.whl @@ -709,7 +709,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -987,7 +987,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1263,7 +1263,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1533,7 +1533,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl @@ -1822,7 +1822,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2089,7 +2089,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2357,7 +2357,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2618,7 +2618,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - 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 @@ -2898,7 +2898,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-9_cp311.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3143,7 +3143,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3389,7 +3389,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz @@ -3629,7 +3629,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -3887,7 +3887,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -4129,7 +4129,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -4372,7 +4372,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl @@ -4609,7 +4609,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl @@ -4863,7 +4863,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5107,7 +5107,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl @@ -5352,7 +5352,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - 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 - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl @@ -5591,7 +5591,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda - pypi: ./ - - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 + - pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 - pypi: https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl - 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 @@ -10852,7 +10852,7 @@ packages: - build ; extra == 'build' - twine ; extra == 'build' requires_python: '>=3.11,<3.14' -- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#2d7fa61be917aba82d76a0b950cb6fee368e3ec1 +- pypi: git+https://github.com/MODFLOW-ORG/modflow-devtools.git#9d76a59ee914599f0db2501ebd9083ad850d6564 name: modflow-devtools version: 1.10.0.dev1 requires_dist: From 9305de9f26100da91ec8ff11df2af34b0438f2ec Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 12 Oct 2026 18:05:04 -0700 Subject: [PATCH 10/10] read TVK and TVS settings off the row's union field in round-trip tests Co-Authored-By: Claude Opus 5.5 --- test/mf6/test_mf6_io_roundtrip.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/mf6/test_mf6_io_roundtrip.py b/test/mf6/test_mf6_io_roundtrip.py index dfa80c9e..4c390ef0 100644 --- a/test/mf6/test_mf6_io_roundtrip.py +++ b/test/mf6/test_mf6_io_roundtrip.py @@ -184,8 +184,9 @@ def test_tvk_child_kept(tmp_path): sim = Simulation.load(workspace / "mfsim.nam") tvk = sim["flow15"].npf.tvk rows = tvk.stress_period_data[0] - assert [(type(r).__name__, r.cellid) for r in rows] == [("K", (0, 0, 2)), ("K33", (0, 0, 3))] - assert (rows[0].k, rows[1].k33) == (5.0, 2.5) + settings = [(type(r.tvksetting).__name__, r.cellid) for r in rows] + assert settings == [("K", (0, 0, 2)), ("K33", (0, 0, 3))] + assert (rows[0].tvksetting.k, rows[1].tvksetting.k33) == (5.0, 2.5) out = tmp_path / "written" out.mkdir() @@ -208,8 +209,9 @@ def test_tvs_child_kept(tmp_path): ) sim = Simulation.load(workspace / "mfsim.nam") rows = sim["GWF_1"].sto.tvs.stress_period_data[1] - assert [(type(r).__name__, r.cellid) for r in rows] == [("Ss", (0, 0, 0)), ("Sy", (0, 0, 1))] - assert (rows[0].ss, rows[1].sy) == (2.0e-5, 0.1) + settings = [(type(r.tvssetting).__name__, r.cellid) for r in rows] + assert settings == [("Ss", (0, 0, 0)), ("Sy", (0, 0, 1))] + assert (rows[0].tvssetting.ss, rows[1].tvssetting.sy) == (2.0e-5, 0.1) out = tmp_path / "written" out.mkdir()