From ee464830f6df9f84bb09e9358f18adca02d1bd13 Mon Sep 17 00:00:00 2001 From: wpbonelli Date: Wed, 4 Sep 2024 12:48:24 -0400 Subject: [PATCH] demo gwfoc --- flopy4/attrs.py | 35 ++++++----------- flopy4/converter.py | 0 test/test_attrs.py | 7 ---- test/test_gwfoc.py | 95 ++++++++++++++++++++++++++------------------- 4 files changed, 67 insertions(+), 70 deletions(-) create mode 100644 flopy4/converter.py diff --git a/flopy4/attrs.py b/flopy4/attrs.py index 43b2e99..1dffb1b 100644 --- a/flopy4/attrs.py +++ b/flopy4/attrs.py @@ -1,17 +1,20 @@ from pathlib import Path from typing import ( + Any, Optional, TypeVar, Union, ) import attr -from attrs import NOTHING, define, field, fields -from cattrs import structure, unstructure +from attrs import NOTHING, asdict, define, field, fields +from cattrs import structure from numpy.typing import ArrayLike from pandas import DataFrame # Enumerate the primitive types to support. +# This is just for reference, not meant to +# be definitive, exclusive, or exhaustive. Scalar = Union[bool, int, float, str, Path] """A scalar input parameter.""" @@ -109,7 +112,7 @@ def from_dict(cls, d: dict): def to_dict(self): """Convert the context to a dictionary.""" - return unstructure(self) + return asdict(self, recurse=True) def wrap(cls): setattr(cls, "from_dict", classmethod(from_dict)) @@ -128,23 +131,6 @@ def wrap(cls): return wrap(maybe_cls) -def choice( - maybe_cls: Optional[type[T]] = None, - *, - frozen: bool = False, -): - def wrap(cls): - return context( - cls, - frozen=frozen, - ) - - if maybe_cls is None: - return wrap - - return wrap(maybe_cls) - - # Utilities @@ -170,10 +156,11 @@ def is_frozen(cls: type) -> bool: return cls.__setattr__ == attr._make._frozen_setattrs -def to_path(val) -> Optional[Path]: - if val is None: +def to_path(value: Any) -> Optional[Path]: + """Try to convert the value to a `Path`.""" + if value is None: return None try: - return Path(val).expanduser() + return Path(value).expanduser() except: - raise ValueError(f"Cannot convert value to Path: {val}") + raise ValueError(f"Can't convert value to Path: {value}") diff --git a/flopy4/converter.py b/flopy4/converter.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_attrs.py b/test/test_attrs.py index f4349d9..80c8649 100644 --- a/test/test_attrs.py +++ b/test/test_attrs.py @@ -14,9 +14,6 @@ params, ) -# Records are product types: named, ordered tuples of scalars. -# Records are immutable: they can't be changed, only evolved. - @context(frozen=True) class Record: @@ -42,9 +39,6 @@ class Block: ) -# Keystrings are sum types: discriminated unions of records. - - def test_spec(): spec = params(Record) assert len(spec) == 4 @@ -90,5 +84,4 @@ def test_usage(): assert astuple(r) == (True, 42, math.pi, None) assert asdict(r) == {"rb": True, "ri": 42, "rf": math.pi, "rs": None} with pytest.raises(TypeError): - # non-optional members are required Record(rb=True) diff --git a/test/test_gwfoc.py b/test/test_gwfoc.py index b74d1b7..4d86f7c 100644 --- a/test/test_gwfoc.py +++ b/test/test_gwfoc.py @@ -1,14 +1,10 @@ from pathlib import Path from typing import Dict, List, Literal, Optional, Union -from cattrs import unstructure +import pytest from flopy4.attrs import context, is_frozen, param, params, to_path -# Define the package input specification. -# Some of this will be generic, and come -# from elsewhere, eventually. - ArrayFormat = Literal["exponential", "fixed", "general", "scientific"] @@ -32,6 +28,36 @@ class PrintFormat: ) +@context +class Options: + budget_file: Optional[Path] = param( + description=""" +name of the output file to write budget information""", + converter=to_path, + default=None, + ) + budget_csv_file: Optional[Path] = param( + description=""" +name of the comma-separated value (CSV) output +file to write budget summary information. +A budget summary record will be written to this +file for each time step of the simulation.""", + converter=to_path, + default=None, + ) + head_file: Optional[Path] = param( + description=""" +name of the output file to write head information.""", + converter=to_path, + default=None, + ) + print_format: Optional[PrintFormat] = param( + description=""" +specify format for printing to the listing file""", + default=None, + ) + + @context class All: all: bool = param( @@ -74,7 +100,7 @@ class Frequency: # It's awkward to have single-parameter contexts, but # it's the only way I got `cattrs` to distinguish the -# choices in the union. +# choices in the union. There is likely a better way. StepSelection = Union[All, First, Last, Steps, Frequency] @@ -89,36 +115,6 @@ class OutputControlData: ocsetting: StepSelection = param() -@context -class Options: - budget_file: Optional[Path] = param( - description=""" -name of the output file to write budget information""", - converter=to_path, - default=None, - ) - budget_csv_file: Optional[Path] = param( - description=""" -name of the comma-separated value (CSV) output -file to write budget summary information. -A budget summary record will be written to this -file for each time step of the simulation.""", - converter=to_path, - default=None, - ) - head_file: Optional[Path] = param( - description=""" -name of the output file to write head information.""", - converter=to_path, - default=None, - ) - print_format: Optional[PrintFormat] = param( - description=""" -specify format for printing to the listing file""", - default=None, - ) - - Period = List[OutputControlData] Periods = List[Period] @@ -148,15 +144,36 @@ def test_spec(): assert ocsetting.type is StepSelection -def test_options(): +def test_options_to_dict(): options = Options( budget_file="some/file/path.cbc", ) assert isinstance(options.budget_file, Path) - assert len(unstructure(options)) == 4 + assert len(options.to_dict()) == 4 + + +def test_output_control_data_from_dict(): + # from dict + ocdata = OutputControlData.from_dict( + { + "action": "print", + "variable": "budget", + "ocsetting": {"steps": [1, 3, 5]}, + } + ) + assert ocdata.action == "print" + + +@pytest.mark.xfail(reason="todo") +def test_output_control_data_from_tuple(): + ocdata = OutputControlData.from_tuple( + ("print", "budget", "steps", 1, 3, 5) + ) + assert ocdata.action == "print" + assert ocdata.variable == "budget" -def test_gwfoc_structure(): +def test_gwfoc_from_dict(): gwfoc = GwfOc.from_dict( { "options": {