Skip to content

Commit

Permalink
Merge pull request #10 from wpbonelli/fixes
Browse files Browse the repository at this point in the history
*__str__() on param, block or package shows what will be written to file
* __repr__() on param, block or package shows unambiguous representation
    * value or dict of values for instance
    * spec of dict of specs for class
* attach block attrs to package class
* remove unnecessary params properties
  • Loading branch information
wpbonelli authored Jul 24, 2024
2 parents 8f82fb9 + 43d21fe commit 91b528e
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 59 deletions.
14 changes: 13 additions & 1 deletion flopy4/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,17 @@ def __init__(
repeating=repeating,
tagged=tagged,
reader=reader,
shape=shape,
default_value=default_value,
)
self._value = array
self._shape = shape
self._how = how
self._factor = factor

def __get__(self, obj, type=None):
return self if self.value is None else self.value

def __getitem__(self, item):
return self.raw[item]

Expand Down Expand Up @@ -269,10 +273,13 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
return self

@property
def value(self) -> np.ndarray:
def value(self) -> Optional[np.ndarray]:
"""
Return the array.
"""
if self._value is None:
return None

if self.layered:
arr = []
for mfa in self._value:
Expand All @@ -284,6 +291,11 @@ def value(self) -> np.ndarray:
else:
return self._value.reshape(self._shape) * self.factor

@value.setter
def value(self, value: np.ndarray):
assert value.shape == self.shape
self._value = value

@property
def raw(self):
"""
Expand Down
30 changes: 19 additions & 11 deletions flopy4/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import UserDict
from dataclasses import asdict
from io import StringIO
from pprint import pformat
from typing import Any

from flopy4.array import MFArray
Expand Down Expand Up @@ -39,7 +40,8 @@ def __new__(cls, clsname, bases, attrs):
.lower()
)

# add parameter specification class attributes
# add parameter specification as class attribute.
# dynamically set the parameters' name and block.
params = {
k: v.with_name(k).with_block(block_name)
for k, v in attrs.items()
Expand Down Expand Up @@ -70,7 +72,7 @@ class MFBlock(MFParams, metaclass=MFBlockMappingMeta):
Supports dictionary and attribute access. The class
attributes specify the block's parameters. Instance
attributes contain both the specification and value.
attributes expose the parameter value.
The block's name and index are discovered upon load.
"""
Expand All @@ -81,21 +83,24 @@ def __init__(self, name=None, index=None, params=None):
super().__init__(params)

def __getattribute__(self, name: str) -> Any:
# shortcut to parameter value for instance attribute.
# the class attribute is the full parameter instance.
attr = super().__getattribute__(name)
return attr.value if isinstance(attr, MFParam) else attr
if name == "data":
return super().__getattribute__(name)

param = self.data.get(name)
return (
param.value
if param is not None
else super().__getattribute__(name)
)

def __repr__(self):
return pformat({k: v for k, v in self.data.items()})

def __str__(self):
buffer = StringIO()
self.write(buffer)
return buffer.getvalue()

@property
def params(self) -> MFParams:
"""Block parameters."""
return self.data

@classmethod
def load(cls, f, **kwargs):
"""Load the block from file."""
Expand Down Expand Up @@ -158,3 +163,6 @@ def __init__(self, blocks=None):
super().__init__(blocks)
for key, block in self.items():
setattr(self, key, block)

def __repr__(self):
return pformat({k: repr(v) for k, v in self.data.items()})
20 changes: 11 additions & 9 deletions flopy4/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections.abc import Mapping
from dataclasses import asdict
from io import StringIO
from pprint import pformat
from typing import Any, Dict

from flopy4.param import MFParam, MFParams, MFReader
Expand Down Expand Up @@ -54,6 +55,12 @@ def __init__(
default_value,
)

def __get__(self, obj, type=None):
return self

def __repr__(self):
return pformat(self.data)

@property
def params(self) -> MFParams:
"""Component parameters."""
Expand All @@ -67,14 +74,10 @@ def value(self) -> Mapping[str, Any]:
}

@value.setter
def value(self, **kwargs):
def value(self, value):
"""Set component names/values by keyword arguments."""
val_len = len(kwargs)
exp_len = len(self.data)
if exp_len != val_len:
raise ValueError(f"Expected {exp_len} values, got {val_len}")
for key in self.data.keys():
self.data[key].value = kwargs[key]
for key, val in value.items():
self.data[key].value = val


class MFRecord(MFCompound):
Expand Down Expand Up @@ -224,5 +227,4 @@ def load(cls, f, params, **kwargs) -> "MFKeystring":

def write(self, f):
"""Write the keystring to file."""
for param in self.data:
param.write(f)
super().write(f)
53 changes: 30 additions & 23 deletions flopy4/package.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABCMeta
from collections import UserDict
from io import StringIO
from itertools import groupby
from pprint import pformat
from typing import Any

from flopy4.block import MFBlock, MFBlockMeta, MFBlocks
Expand All @@ -17,23 +17,23 @@ def get_block(pkg_name, block_name, params):

class MFPackageMeta(type):
def __new__(cls, clsname, bases, attrs):
new = super().__new__(cls, clsname, bases, attrs)
if clsname == "MFPackage":
return new
return super().__new__(cls, clsname, bases, attrs)

# detect package name
pkg_name = clsname.replace("Package", "")

# add parameter and block specification as class
# attributes. subclass mfblock dynamically based
# on each block parameter specification.
pkg_name = clsname.replace("Package", "")
params = MFParams(
{
k: v.with_name(k)
for k, v in attrs.items()
if issubclass(type(v), MFParam)
}
)
new.params = params
new.blocks = MFBlocks(
blocks = MFBlocks(
{
block_name: get_block(
pkg_name=pkg_name,
Expand All @@ -45,51 +45,58 @@ def __new__(cls, clsname, bases, attrs):
)
}
)
return new

attrs["params"] = params
attrs["blocks"] = blocks
for block_name, block in blocks.items():
attrs[block_name] = block

return super().__new__(cls, clsname, bases, attrs)


class MFPackageMappingMeta(MFPackageMeta, ABCMeta):
# http://www.phyast.pitt.edu/~micheles/python/metatype.html
pass


class MFPackage(UserDict, metaclass=MFPackageMappingMeta):
class MFPackage(MFBlocks, metaclass=MFPackageMappingMeta):
"""
MF6 model or simulation component package.
TODO: reimplement with `ChainMap`?
"""

def __init__(self, blocks=None):
super().__init__(blocks)

def __repr__(self):
return pformat(self.data)

def __str__(self):
buffer = StringIO()
self.write(buffer)
return buffer.getvalue()

def __getattribute__(self, name: str) -> Any:
value = super().__getattribute__(name)
if name == "data":
return value
return super().__getattribute__(name)

block = self.data.get(name)
if block is not None:
return block

# shortcut to parameter value for instance attribute.
# the class attribute is the full parameter instance.
# the class attribute is the parameter specification.
params = {
param_name: param
for block in self.data.values()
for param_name, param in block.items()
}
param = params.get(name)
return params[name].value if param is not None else value

@property
def params(self) -> MFParams:
"""Package parameters."""
return MFParams(
{
name: param
for block in self.data
for name, param in block.items()
}
return (
param.value
if param is not None
else super().__getattribute__(name)
)

@classmethod
Expand Down
6 changes: 6 additions & 0 deletions flopy4/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import UserDict
from dataclasses import dataclass, fields
from io import StringIO
from pprint import pformat
from typing import Any, Optional, Tuple

from flopy4.constants import MFReader
Expand Down Expand Up @@ -148,6 +149,11 @@ def __init__(
default_value=default_value,
)

def __repr__(self):
return (
super().__repr__() if self.value is None else pformat(self.value)
)

def __str__(self):
buffer = StringIO()
self.write(buffer)
Expand Down
3 changes: 3 additions & 0 deletions flopy4/scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def __init__(
default_value,
)

def __get__(self, obj, type=None):
return self if self.value is None else self.value

@property
def value(self):
return self._value
Expand Down
4 changes: 4 additions & 0 deletions flopy4/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ def strip(line):
line = line.split(comment_flag)[0]
line = line.strip()
return line.replace(",", " ")


class hybridproperty:
pass
21 changes: 9 additions & 12 deletions test/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class TestBlock(MFBlock):
d = MFDouble(description="double")
s = MFString(description="string", optional=False)
f = MFFilename(description="filename", optional=False)
a = MFArray(description="array", shape=(3))
a = MFArray(description="array", shape=(3,))
r = MFRecord(
params={
"rk": MFKeyword(),
Expand Down Expand Up @@ -84,29 +84,26 @@ def test_load_write(tmp_path):
with open(fpth, "r") as f:
block = TestBlock.load(f)

# class attribute as param specification
# check parameter specification
assert isinstance(TestBlock.k, MFKeyword)
assert TestBlock.k.name == "k"
assert TestBlock.k.block == "options"
assert TestBlock.k.description == "keyword"

# instance attribute as shortcut to param valu
assert isinstance(block.k, bool)
assert block.k
assert block.i == 1
assert block.d == 1.0
assert block.s == "value"
assert block.f == fpth
assert np.allclose(block.a, np.array([1.0, 2.0, 3.0]))

assert isinstance(TestBlock.r, MFRecord)
assert TestBlock.r.name == "r"
assert len(TestBlock.r.params) == 3
assert isinstance(TestBlock.r.params["rk"], MFKeyword)
assert isinstance(TestBlock.r.params["ri"], MFInteger)
assert isinstance(TestBlock.r.params["rd"], MFDouble)

assert isinstance(block.r, dict)
# check parameter values (via descriptors)
assert block.k
assert block.i == 1
assert block.d == 1.0
assert block.s == "value"
assert block.f == fpth
assert np.allclose(block.a, np.array([1.0, 2.0, 3.0]))
assert block.r == {"rd": 2.0, "ri": 2, "rk": True}

# test block write
Expand Down
5 changes: 2 additions & 3 deletions test/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,15 @@ def test_load_write(tmp_path):
with open(fpth, "r") as f:
package = TestPackage.load(f)

# check block and parameter specifications
assert len(package.blocks) == 2
assert len(package.params) == 6

# class attribute as param specification
assert isinstance(TestPackage.k, MFKeyword)
assert TestPackage.k.name == "k"
assert TestPackage.k.block == "options"
assert TestPackage.k.description == "keyword"

# instance attribute as shortcut to param value
# check parameter values
assert isinstance(package.k, bool)
assert package.k
assert package.i == 1
Expand Down

0 comments on commit 91b528e

Please sign in to comment.