Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gwfoc demo cleanup #29

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 11 additions & 24 deletions flopy4/attrs.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand Down Expand Up @@ -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))
Expand All @@ -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


Expand All @@ -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}")
Empty file added flopy4/converter.py
Empty file.
7 changes: 0 additions & 7 deletions test/test_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -42,9 +39,6 @@ class Block:
)


# Keystrings are sum types: discriminated unions of records.


def test_spec():
spec = params(Record)
assert len(spec) == 4
Expand Down Expand Up @@ -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)
95 changes: 56 additions & 39 deletions test/test_gwfoc.py
Original file line number Diff line number Diff line change
@@ -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"]


Expand All @@ -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(
Expand Down Expand Up @@ -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]
Expand All @@ -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]

Expand Down Expand Up @@ -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": {
Expand Down
Loading