From f2191b74f00ff286f781bd060dabb0b1e851157a Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sun, 12 Jul 2026 05:12:01 -0700 Subject: [PATCH] pk/fk field metadata, path field fixes --- flopy4/mf6/gwe/fmi.py | 5 +++-- flopy4/mf6/gwe/ssm.py | 5 +++-- flopy4/mf6/gwf/lak.py | 2 +- flopy4/mf6/gwt/fmi.py | 5 +++-- flopy4/mf6/gwt/ssm.py | 5 +++-- flopy4/mf6/spec.py | 6 ++++++ flopy4/mf6/utils/codegen/filters.py | 27 +++++++++++++++++++++++++-- flopy4/mf6/utils/codegen/make.py | 13 +++++++++---- 8 files changed, 53 insertions(+), 15 deletions(-) diff --git a/flopy4/mf6/gwe/fmi.py b/flopy4/mf6/gwe/fmi.py index 5d4e615f..ce27977b 100644 --- a/flopy4/mf6/gwe/fmi.py +++ b/flopy4/mf6/gwe/fmi.py @@ -1,4 +1,5 @@ # autogenerated file, do not modify +from pathlib import Path from typing import ClassVar, Optional, Union import attrs @@ -6,7 +7,7 @@ from flopy4.mf6.package import Package from flopy4.mf6.schema import Column, Schema -from flopy4.mf6.spec import field +from flopy4.mf6.spec import field, path @attrs.define(kw_only=True, slots=False) @@ -42,7 +43,7 @@ class _PackagedataSchema(Schema): @attrs.define class PackagedataRow: flowtype: Union[float, str] - fname: Union[float, str] + fname: Path = path(converter=Path, inout="filein") def __iter__(self): yield self.flowtype diff --git a/flopy4/mf6/gwe/ssm.py b/flopy4/mf6/gwe/ssm.py index 15792f8e..d2feeb86 100644 --- a/flopy4/mf6/gwe/ssm.py +++ b/flopy4/mf6/gwe/ssm.py @@ -1,4 +1,5 @@ # autogenerated file, do not modify +from pathlib import Path from typing import ClassVar, Optional, Union import attrs @@ -6,7 +7,7 @@ from flopy4.mf6.package import Package from flopy4.mf6.schema import Column, Schema -from flopy4.mf6.spec import field +from flopy4.mf6.spec import field, path @attrs.define(kw_only=True, slots=False) @@ -67,7 +68,7 @@ class _FileinputSchema(Schema): @attrs.define class FileinputRow: pname: Union[float, str] - spc6_filename: Union[float, str] + spc6_filename: Path = path(converter=Path, inout="filein") mixed: Optional[str] = None def __iter__(self): diff --git a/flopy4/mf6/gwf/lak.py b/flopy4/mf6/gwf/lak.py index 10f5e424..6150029f 100644 --- a/flopy4/mf6/gwf/lak.py +++ b/flopy4/mf6/gwf/lak.py @@ -235,7 +235,7 @@ class _TablesSchema(Schema): @attrs.define class TablesRow: ifno: int - tab6_filename: Union[float, str] + tab6_filename: Path = path(converter=Path, inout="filein") def __iter__(self): yield self.ifno diff --git a/flopy4/mf6/gwt/fmi.py b/flopy4/mf6/gwt/fmi.py index 5d4e615f..ce27977b 100644 --- a/flopy4/mf6/gwt/fmi.py +++ b/flopy4/mf6/gwt/fmi.py @@ -1,4 +1,5 @@ # autogenerated file, do not modify +from pathlib import Path from typing import ClassVar, Optional, Union import attrs @@ -6,7 +7,7 @@ from flopy4.mf6.package import Package from flopy4.mf6.schema import Column, Schema -from flopy4.mf6.spec import field +from flopy4.mf6.spec import field, path @attrs.define(kw_only=True, slots=False) @@ -42,7 +43,7 @@ class _PackagedataSchema(Schema): @attrs.define class PackagedataRow: flowtype: Union[float, str] - fname: Union[float, str] + fname: Path = path(converter=Path, inout="filein") def __iter__(self): yield self.flowtype diff --git a/flopy4/mf6/gwt/ssm.py b/flopy4/mf6/gwt/ssm.py index 15792f8e..d2feeb86 100644 --- a/flopy4/mf6/gwt/ssm.py +++ b/flopy4/mf6/gwt/ssm.py @@ -1,4 +1,5 @@ # autogenerated file, do not modify +from pathlib import Path from typing import ClassVar, Optional, Union import attrs @@ -6,7 +7,7 @@ from flopy4.mf6.package import Package from flopy4.mf6.schema import Column, Schema -from flopy4.mf6.spec import field +from flopy4.mf6.spec import field, path @attrs.define(kw_only=True, slots=False) @@ -67,7 +68,7 @@ class _FileinputSchema(Schema): @attrs.define class FileinputRow: pname: Union[float, str] - spc6_filename: Union[float, str] + spc6_filename: Path = path(converter=Path, inout="filein") mixed: Optional[str] = None def __iter__(self): diff --git a/flopy4/mf6/spec.py b/flopy4/mf6/spec.py index 98486908..89ee8da8 100644 --- a/flopy4/mf6/spec.py +++ b/flopy4/mf6/spec.py @@ -46,6 +46,8 @@ def field( oc_action: str | None = None, oc_rtype: str | None = None, time_series: bool = False, + pk: bool = False, + fk: str | None = None, ): """Define a codegen-v2 field: always a plain ``attrs.field()``. @@ -92,6 +94,10 @@ def field( metadata["oc_rtype"] = oc_rtype if time_series: metadata["time_series"] = True + if pk: + metadata["pk"] = True + if fk: + metadata["fk"] = fk return attrs.field( default=default, validator=validator, diff --git a/flopy4/mf6/utils/codegen/filters.py b/flopy4/mf6/utils/codegen/filters.py index 1bc7c8fb..f3a5011a 100644 --- a/flopy4/mf6/utils/codegen/filters.py +++ b/flopy4/mf6/utils/codegen/filters.py @@ -676,6 +676,29 @@ def _py_type(col: dict) -> str: def _is_optional(col: dict) -> bool: return bool(col.get("optional")) or col["role"] in ("boundname", "inline_keyword") + def _prefix_inout(col: dict) -> str: + """MF6 inout direction implied by a row column's prefix tokens.""" + return "fileout" if "FILEOUT" in (col.get("prefix") or "").upper().split() else "filein" + + def _field_line(col: dict, *, optional: bool) -> str: + # File-reference columns (a fixed MF6 token or two before a filename, + # e.g. LAK tables' "TAB6 FILEIN ") are Path fields built via the + # same path() convention used for Package-level file fields, not the + # generic dtype-based Union[float, str] fallback below. + if col.get("prefix"): + inout = _prefix_inout(col) + if optional: + return ( + f" {col['name']}: Optional[Path] = path(\n" + f' default=None, converter=_optional_path, inout="{inout}"\n' + f" )" + ) + return f' {col["name"]}: Path = path(converter=Path, inout="{inout}")' + py_type = _py_type(col) + if optional: + return f" {col['name']}: Optional[{py_type}] = None" + return f" {col['name']}: {py_type}" + required = [col for col in schema_list if not _is_optional(col)] optional = [col for col in schema_list if _is_optional(col)] # Aux injection: only for period blocks. Standard stress packages (CHD, @@ -689,11 +712,11 @@ def _is_optional(col: dict) -> bool: lines = [" @attrs.define"] lines.append(f" class {class_name}:") for col in required: - lines.append(f" {col['name']}: {_py_type(col)}") + lines.append(_field_line(col, optional=False)) if has_positional_aux: lines.append(" aux: tuple = ()") for col in optional: - lines.append(f" {col['name']}: Optional[{_py_type(col)}] = None") + lines.append(_field_line(col, optional=True)) lines.append("") lines.append(" def __iter__(self):") for col in required: diff --git a/flopy4/mf6/utils/codegen/make.py b/flopy4/mf6/utils/codegen/make.py index baad5776..f5638856 100644 --- a/flopy4/mf6/utils/codegen/make.py +++ b/flopy4/mf6/utils/codegen/make.py @@ -608,11 +608,16 @@ def _new_codegen_imports( has_union = any( col.get("time_series") or col.get("dtype") == "np.object_" for col in _all_schema_cols - if col.get("role") not in ("keystring_value", "boundname") + if col.get("role") not in ("keystring_value", "boundname") and not col.get("prefix") ) + # prefix= row columns (file references, e.g. LAK tables' TAB6 FILEIN) + # become Path fields via path() in row_class(), not Union[float, str]. + _row_path_cols = [col for col in _all_schema_cols if col.get("prefix")] + has_row_path_cols = bool(_row_path_cols) + has_optional_row_path_cols = any(col.get("optional") for col in _row_path_cols) stdlib: list[str] = [] - if has_path or has_injected_paths or has_file_records: + if has_path or has_injected_paths or has_file_records or has_row_path_cols: stdlib.append("from pathlib import Path") typing_parts: list[str] = [] if has_classvar: @@ -643,7 +648,7 @@ def _new_codegen_imports( _spec_parts: list[str] = [] if has_field_call: _spec_parts.append("field") - if has_path_call: + if has_path_call or has_row_path_cols: _spec_parts.append("path") if _spec_parts: flopy4.append(f"from flopy4.mf6.spec import {', '.join(sorted(_spec_parts))}") @@ -652,7 +657,7 @@ def _new_codegen_imports( _types_parts.append("IntArrayLike") if needs_float_arraylike: _types_parts.append("FloatArrayLike") - if has_file_records or has_injected_paths: + if has_file_records or has_injected_paths or has_optional_row_path_cols: _types_parts.append("_optional_path") if _types_parts: flopy4.append(f"from flopy4.mf6._types import {', '.join(sorted(_types_parts))}")