From 6c794d1e651fe8c8e02826049370a5bc306051fe Mon Sep 17 00:00:00 2001 From: w-bonelli Date: Sat, 8 Mar 2025 21:09:05 -0500 Subject: [PATCH 1/4] feat: mf6 input file parser --- autotest/test_codec.py | 15 +++++++++++++++ docs/md/codec.md | 0 modflow_devtools/codec.py | 5 +++++ modflow_devtools/dfn.py | 12 +++++++----- modflow_devtools/mf6.lark | 15 +++++++++++++++ pyproject.toml | 7 +++++++ 6 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 autotest/test_codec.py create mode 100644 docs/md/codec.md create mode 100644 modflow_devtools/codec.py create mode 100644 modflow_devtools/mf6.lark diff --git a/autotest/test_codec.py b/autotest/test_codec.py new file mode 100644 index 00000000..c55ae216 --- /dev/null +++ b/autotest/test_codec.py @@ -0,0 +1,15 @@ +from modflow_devtools.codec import make_parser + + +def test_parser(): + parser = make_parser() + text = """ +BEGIN OPTIONS + AN OPTION + ANOTHER OPTION +END OPTIONS +BEGIN PACKAGEDATA +END PACKAGEDATA +""" + tree = parser.parse(text) + print(tree.pretty()) diff --git a/docs/md/codec.md b/docs/md/codec.md new file mode 100644 index 00000000..e69de29b diff --git a/modflow_devtools/codec.py b/modflow_devtools/codec.py new file mode 100644 index 00000000..9506fcb3 --- /dev/null +++ b/modflow_devtools/codec.py @@ -0,0 +1,5 @@ +from lark import Lark + + +def make_parser(**kwargs) -> Lark: + return Lark.open("mf6.lark", parser="lalr", rel_to=__file__, **kwargs) diff --git a/modflow_devtools/dfn.py b/modflow_devtools/dfn.py index b8aab064..5739b494 100644 --- a/modflow_devtools/dfn.py +++ b/modflow_devtools/dfn.py @@ -157,14 +157,16 @@ class Dfn(TypedDict): MODFLOW 6 input definition. An input definition specifies a component in an MF6 simulation, e.g. a model or package, containing input variables. + + # TODO: use dataclass, mypy says static methods in a typed dict are invalid """ name: str - advanced: bool = False - multi: bool = False - ref: Ref | None = None - sln: Sln | None = None - fkeys: Dfns | None = None + advanced: bool + multi: bool + ref: Optional[Ref] + sln: Optional[Sln] + fkeys: Optional[Dfns] @staticmethod def _load_v1_flat(f, common: dict | None = None) -> tuple[Mapping, list[str]]: diff --git a/modflow_devtools/mf6.lark b/modflow_devtools/mf6.lark new file mode 100644 index 00000000..3800ee45 --- /dev/null +++ b/modflow_devtools/mf6.lark @@ -0,0 +1,15 @@ +start: [WS] block+ [WS] + +block: "begin"i CNAME _NL _content "end"i CNAME _NL +_content: line* [WS] +line: [WS] value* _NL +value: WORD | NUMBER + + +%import common.NEWLINE -> _NL +%import common.WS +%import common.WS_INLINE +%import common.CNAME +%import common.WORD +%import common.NUMBER +%ignore WS_INLINE \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e8f637ae..b0c81a9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,9 @@ models = [ "tomli", "tomli-w" ] +codec = [ + "lark" +] dev = ["modflow-devtools[lint,test,docs,dfn,models]"] [dependency-groups] @@ -126,6 +129,9 @@ models = [ "tomli", "tomli-w" ] +codec = [ + "lark" +] dev = [ {include-group = "build"}, {include-group = "lint"}, @@ -133,6 +139,7 @@ dev = [ {include-group = "docs"}, {include-group = "dfn"}, {include-group = "models"}, + {include-group = "codec"}, ] [project.urls] From 955676a5173e0747f6ab5febedbecc9df830c4fe Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 28 Apr 2025 08:42:47 -0400 Subject: [PATCH 2/4] parametrize test --- autotest/test_codec.py | 19 ++++++++++--------- modflow_devtools/dfn.py | 6 +++--- modflow_devtools/mf6.lark | 6 +++--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/autotest/test_codec.py b/autotest/test_codec.py index c55ae216..90f7c195 100644 --- a/autotest/test_codec.py +++ b/autotest/test_codec.py @@ -1,15 +1,16 @@ +import pytest + +import modflow_devtools.models as models from modflow_devtools.codec import make_parser +MODELS = [name for name in models.get_models().keys() if name.startswith("mf6/")] + -def test_parser(): +@pytest.mark.parametrize("model_name", MODELS) +def test_parser(model_name, function_tmpdir): + workspace = models.copy_to(function_tmpdir, model_name) + nam_path = next(iter(workspace.glob("*.nam"))) + text = nam_path.open().read() parser = make_parser() - text = """ -BEGIN OPTIONS - AN OPTION - ANOTHER OPTION -END OPTIONS -BEGIN PACKAGEDATA -END PACKAGEDATA -""" tree = parser.parse(text) print(tree.pretty()) diff --git a/modflow_devtools/dfn.py b/modflow_devtools/dfn.py index 5739b494..9e0e4601 100644 --- a/modflow_devtools/dfn.py +++ b/modflow_devtools/dfn.py @@ -164,9 +164,9 @@ class Dfn(TypedDict): name: str advanced: bool multi: bool - ref: Optional[Ref] - sln: Optional[Sln] - fkeys: Optional[Dfns] + ref: Ref | None + sln: Sln | None + fkeys: Dfns | None @staticmethod def _load_v1_flat(f, common: dict | None = None) -> tuple[Mapping, list[str]]: diff --git a/modflow_devtools/mf6.lark b/modflow_devtools/mf6.lark index 3800ee45..efd8c052 100644 --- a/modflow_devtools/mf6.lark +++ b/modflow_devtools/mf6.lark @@ -1,15 +1,15 @@ start: [WS] block+ [WS] - block: "begin"i CNAME _NL _content "end"i CNAME _NL _content: line* [WS] line: [WS] value* _NL value: WORD | NUMBER - %import common.NEWLINE -> _NL %import common.WS %import common.WS_INLINE %import common.CNAME %import common.WORD %import common.NUMBER -%ignore WS_INLINE \ No newline at end of file +%import common.SH_COMMENT +%ignore WS_INLINE +%ignore SH_COMMENT \ No newline at end of file From 5b7077a5b7782dc3462e06205694b9ee1dbf8f89 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 28 Apr 2025 14:13:00 -0400 Subject: [PATCH 3/4] successfully parse all namefiles --- autotest/test_codec.py | 7 +++++-- modflow_devtools/mf6.lark | 12 ++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/autotest/test_codec.py b/autotest/test_codec.py index 90f7c195..1a04959a 100644 --- a/autotest/test_codec.py +++ b/autotest/test_codec.py @@ -1,9 +1,12 @@ +from pprint import pprint + import pytest import modflow_devtools.models as models from modflow_devtools.codec import make_parser MODELS = [name for name in models.get_models().keys() if name.startswith("mf6/")] +PARSER = make_parser() @pytest.mark.parametrize("model_name", MODELS) @@ -11,6 +14,6 @@ def test_parser(model_name, function_tmpdir): workspace = models.copy_to(function_tmpdir, model_name) nam_path = next(iter(workspace.glob("*.nam"))) text = nam_path.open().read() - parser = make_parser() - tree = parser.parse(text) + pprint(text) + tree = PARSER.parse(text) print(tree.pretty()) diff --git a/modflow_devtools/mf6.lark b/modflow_devtools/mf6.lark index efd8c052..cd64e2bb 100644 --- a/modflow_devtools/mf6.lark +++ b/modflow_devtools/mf6.lark @@ -1,8 +1,10 @@ -start: [WS] block+ [WS] -block: "begin"i CNAME _NL _content "end"i CNAME _NL +start: [WS] [_NL*] (block [[WS] _NL*])+ [WS] +block: "begin"i CNAME [_block_index] _NL _content "end"i CNAME [_block_index] _NL+ +_block_index: INT _content: line* [WS] -line: [WS] value* _NL -value: WORD | NUMBER +line: [WS] item* _NL+ +item: word | NUMBER +word: /[a-zA-Z0-9._'~,-\\(\\)]+/ %import common.NEWLINE -> _NL %import common.WS @@ -10,6 +12,8 @@ value: WORD | NUMBER %import common.CNAME %import common.WORD %import common.NUMBER +%import common.INT %import common.SH_COMMENT +%import common._STRING_INNER %ignore WS_INLINE %ignore SH_COMMENT \ No newline at end of file From 29d89869208a5069e61f1c4f4d42988e9d5e9c8d Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Mon, 28 Apr 2025 14:13:58 -0400 Subject: [PATCH 4/4] revert dfn.py --- modflow_devtools/dfn.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modflow_devtools/dfn.py b/modflow_devtools/dfn.py index 9e0e4601..b8aab064 100644 --- a/modflow_devtools/dfn.py +++ b/modflow_devtools/dfn.py @@ -157,16 +157,14 @@ class Dfn(TypedDict): MODFLOW 6 input definition. An input definition specifies a component in an MF6 simulation, e.g. a model or package, containing input variables. - - # TODO: use dataclass, mypy says static methods in a typed dict are invalid """ name: str - advanced: bool - multi: bool - ref: Ref | None - sln: Sln | None - fkeys: Dfns | None + advanced: bool = False + multi: bool = False + ref: Ref | None = None + sln: Sln | None = None + fkeys: Dfns | None = None @staticmethod def _load_v1_flat(f, common: dict | None = None) -> tuple[Mapping, list[str]]: