From 8839359c9922d291b30b97dcf7ec4ffdb4f7b7ed Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sat, 3 Oct 2026 10:48:19 -0700 Subject: [PATCH 1/2] feat(dfns): keep keyword aliases from other_names The v1 other_names attribute was dropped in migration, so utl-ts's NAMES keyword lost the NAME spelling MF6 also accepts. Add Keyword.aliases, migrate other_names into it, and count aliases as leading tags. Co-Authored-By: Claude Opus 5.5 --- autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.json | 5 ++++- autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.toml | 3 +++ autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.yaml | 2 ++ autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.json | 5 ++++- autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.toml | 3 +++ autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.yaml | 2 ++ autotest/dfns/test_migrate.py | 6 ++++++ autotest/dfns/test_schema.py | 6 ++++++ docs/md/dfnspec.md | 6 ++++++ modflow_devtools/dfn/schema.py | 1 + modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py | 1 + modflow_devtools/dfns/schema.json | 8 ++++++++ modflow_devtools/dfns/schema.py | 4 +++- 13 files changed, 49 insertions(+), 3 deletions(-) diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.json b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.json index b047bcc3..e9eb6ccb 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.json @@ -19,7 +19,10 @@ "fields": { "names": { "type": "keyword", - "description": "xxx" + "description": "xxx", + "aliases": [ + "name" + ] }, "time_series_names": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.toml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.toml index 7adc6a80..12a7f650 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.toml @@ -15,6 +15,9 @@ description = "xxx" [blocks.attributes.fields.time_series_namerecord.fields.names] type = "keyword" description = "xxx" +aliases = [ + "name", +] [blocks.attributes.fields.time_series_namerecord.fields.time_series_names] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.yaml index 0e75ae6e..60c82cd2 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev2/utl-ts.yaml @@ -17,6 +17,8 @@ blocks: names: type: keyword description: xxx + aliases: + - name time_series_names: type: array description: Name by which a package references a particular time-array series. The name must diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.json b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.json index b842504f..e71ffa1c 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.json +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.json @@ -19,7 +19,10 @@ "fields": { "names": { "type": "keyword", - "description": "xxx" + "description": "xxx", + "aliases": [ + "name" + ] }, "time_series_names": { "type": "array", diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.toml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.toml index 863160bc..4c2a07fc 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.toml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.toml @@ -15,6 +15,9 @@ description = "xxx" [blocks.attributes.fields.time_series_namerecord.fields.names] type = "keyword" description = "xxx" +aliases = [ + "name", +] [blocks.attributes.fields.time_series_namerecord.fields.time_series_names] type = "array" diff --git a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.yaml b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.yaml index 2f7217ae..587d91fe 100644 --- a/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.yaml +++ b/autotest/dfns/__snapshots__/v2.0.0.dev3/utl-ts.yaml @@ -17,6 +17,8 @@ blocks: names: type: keyword description: xxx + aliases: + - name time_series_names: type: array description: Name by which a package references a particular time-array series. The name must diff --git a/autotest/dfns/test_migrate.py b/autotest/dfns/test_migrate.py index 6f051121..f19eab36 100644 --- a/autotest/dfns/test_migrate.py +++ b/autotest/dfns/test_migrate.py @@ -400,3 +400,9 @@ def test_migrate_package_dims_are_component_scoped(dev3): name for name in spec.components if name.split("-")[1].startswith("dis") } assert ("sim-tdis", "nper") in shared + + +def test_migrate_keyword_aliases(dfn_dir): + component = _migrate_dev3(dfn_dir, "utl-ts") + record = component.blocks["attributes"].fields["time_series_namerecord"] + assert record.fields["names"].aliases == ["name"] diff --git a/autotest/dfns/test_schema.py b/autotest/dfns/test_schema.py index db2809c7..bef1f2f6 100644 --- a/autotest/dfns/test_schema.py +++ b/autotest/dfns/test_schema.py @@ -1069,3 +1069,9 @@ def test_index_rejects_unknown_kind(): def test_signed_index_requires_integer_array(): with pytest.raises(ValueError, match="requires dtype='integer'"): Array(name="ic", dtype="double", shape=["ncon"], index="signed") + + +def test_keyword_aliases_round_trip(): + field = Keyword(name="names", aliases=["name"]) + assert Keyword.model_validate(field.model_dump()).aliases == ["name"] + assert "aliases" not in Keyword(name="names").model_dump(exclude_defaults=True) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 386d148d..8be89d02 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -344,6 +344,12 @@ Scalar fields define a single value. Type `keyword`. Represents a boolean choice. In input files, the presence of a keyword indicates true, its absence false. +##### Type-specific attributes + +###### `aliases` + +`[string] (default: [])`. Other spellings MF6 accepts for the keyword, e.g. `name` for utl-ts's `names`. Writers use the field's `name`; readers accept the name or any alias. + #### String Type `string`. diff --git a/modflow_devtools/dfn/schema.py b/modflow_devtools/dfn/schema.py index 8f96c7c0..4a3e3507 100644 --- a/modflow_devtools/dfn/schema.py +++ b/modflow_devtools/dfn/schema.py @@ -78,6 +78,7 @@ class Field(TypedDict): preserve_case: NotRequired[bool] numeric_index: NotRequired[bool] support_negative_index: NotRequired[bool] + other_names: NotRequired[str] # Whether MF6 requires this block's header to appear in the input file # even when it has zero body lines (e.g. an empty required recarray # block). Set explicitly on the block's aggregate field -- see diff --git a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py index b651ad87..c909b849 100644 --- a/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py +++ b/modflow_devtools/dfns/migrate_to_v2_0_0_dev2.py @@ -1597,6 +1597,7 @@ def _to_scalar() -> v2.Scalar: netcdf=netcdf, removed=removed, deprecated=deprecated, + aliases=(f.get("other_names") or "").lower().split(), ) if _type == "string": return v2.String( diff --git a/modflow_devtools/dfns/schema.json b/modflow_devtools/dfns/schema.json index f0724fcc..34408a1e 100644 --- a/modflow_devtools/dfns/schema.json +++ b/modflow_devtools/dfns/schema.json @@ -780,6 +780,14 @@ "default": "keyword", "title": "Type", "type": "string" + }, + "aliases": { + "default": [], + "items": { + "type": "string" + }, + "title": "Aliases", + "type": "array" } }, "required": [ diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 3b812dbe..90dad9be 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -78,6 +78,8 @@ def render(self, *, inline: bool = False) -> str: class Keyword(InputFieldBase): type: Literal["keyword"] = PydanticField(default="keyword", frozen=True) + # Other spellings MF6 accepts for the keyword (e.g. utl-ts's NAME for NAMES). + aliases: list[str] = [] class String(InputFieldBase): @@ -221,7 +223,7 @@ def _leading_tags(field: "InputField", *, every_line: bool = False) -> list[str] (a Union with an arm that begins with a value instead doesn't qualify).""" match field: case Keyword(): - return [field.name] + return [field.name, *field.aliases] case String() | Integer() | Double() | Array() | File(): return [field.name] if field.tagged else [] case Record(): From 812f97f7883c76f97f20b083dc66388076b50b8c Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Sat, 3 Oct 2026 14:08:14 -0700 Subject: [PATCH 2/2] docs(dfns): trim keyword aliases docs Co-Authored-By: Claude Opus 5.5 --- docs/md/dfnspec.md | 2 +- modflow_devtools/dfns/schema.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/md/dfnspec.md b/docs/md/dfnspec.md index 8be89d02..a8f7d508 100644 --- a/docs/md/dfnspec.md +++ b/docs/md/dfnspec.md @@ -348,7 +348,7 @@ Type `keyword`. Represents a boolean choice. In input files, the presence of a k ###### `aliases` -`[string] (default: [])`. Other spellings MF6 accepts for the keyword, e.g. `name` for utl-ts's `names`. Writers use the field's `name`; readers accept the name or any alias. +`[string] (default: [])`. Other spellings MF6 accepts for the keyword. #### String diff --git a/modflow_devtools/dfns/schema.py b/modflow_devtools/dfns/schema.py index 90dad9be..bc5e9cdc 100644 --- a/modflow_devtools/dfns/schema.py +++ b/modflow_devtools/dfns/schema.py @@ -78,7 +78,6 @@ def render(self, *, inline: bool = False) -> str: class Keyword(InputFieldBase): type: Literal["keyword"] = PydanticField(default="keyword", frozen=True) - # Other spellings MF6 accepts for the keyword (e.g. utl-ts's NAME for NAMES). aliases: list[str] = []