diff --git a/flopy4/array.py b/flopy4/array.py index e6cb9f6..8a7a846 100644 --- a/flopy4/array.py +++ b/flopy4/array.py @@ -219,6 +219,7 @@ def __init__( repeating=repeating, tagged=tagged, reader=reader, + shape=shape, default_value=default_value, ) self._value = array @@ -226,6 +227,9 @@ def __init__( 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] @@ -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: @@ -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): """ diff --git a/flopy4/block.py b/flopy4/block.py index da4a9f6..b450ea8 100644 --- a/flopy4/block.py +++ b/flopy4/block.py @@ -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 @@ -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() @@ -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. """ @@ -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.""" @@ -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()}) diff --git a/flopy4/compound.py b/flopy4/compound.py index b80d3a0..294ef45 100644 --- a/flopy4/compound.py +++ b/flopy4/compound.py @@ -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 @@ -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.""" @@ -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): @@ -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) diff --git a/flopy4/package.py b/flopy4/package.py index 41bada2..c73596e 100644 --- a/flopy4/package.py +++ b/flopy4/package.py @@ -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 @@ -17,14 +17,15 @@ 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) @@ -32,8 +33,7 @@ def __new__(cls, clsname, bases, attrs): if issubclass(type(v), MFParam) } ) - new.params = params - new.blocks = MFBlocks( + blocks = MFBlocks( { block_name: get_block( pkg_name=pkg_name, @@ -45,7 +45,13 @@ 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): @@ -53,43 +59,44 @@ class MFPackageMappingMeta(MFPackageMeta, ABCMeta): 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 diff --git a/flopy4/param.py b/flopy4/param.py index 7d3d2d8..2c2e463 100644 --- a/flopy4/param.py +++ b/flopy4/param.py @@ -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 @@ -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) diff --git a/flopy4/scalar.py b/flopy4/scalar.py index cebc575..3d0528c 100644 --- a/flopy4/scalar.py +++ b/flopy4/scalar.py @@ -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 diff --git a/flopy4/utils.py b/flopy4/utils.py index c1cb167..278f2bc 100644 --- a/flopy4/utils.py +++ b/flopy4/utils.py @@ -22,3 +22,7 @@ def strip(line): line = line.split(comment_flag)[0] line = line.strip() return line.replace(",", " ") + + +class hybridproperty: + pass diff --git a/test/test_block.py b/test/test_block.py index c99df6d..2d5c293 100644 --- a/test/test_block.py +++ b/test/test_block.py @@ -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(), @@ -84,21 +84,12 @@ 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 @@ -106,7 +97,13 @@ def test_load_write(tmp_path): 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 diff --git a/test/test_package.py b/test/test_package.py index 810a0ca..b80dfc0 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -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