From fb1673794d783c1287a72e2582c4d4c4f911009c Mon Sep 17 00:00:00 2001 From: Gabriel Gerlero Date: Wed, 20 Mar 2024 09:42:27 -0300 Subject: [PATCH] Refactor dictionary manipulation --- docs/index.rst | 2 +- foamlib/__init__.py | 13 +++- foamlib/_cases.py | 8 +-- foamlib/_dictionaries.py | 137 ++++++++++++++++++++++++++++--------- tests/test_dictionaries.py | 33 ++++++--- 5 files changed, 145 insertions(+), 48 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e307550..ae3861d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ Documentation for `foamlib `_ .. automodule:: foamlib :members: :undoc-members: - :inherited-members: + :show-inheritance: Indices and tables ================== diff --git a/foamlib/__init__.py b/foamlib/__init__.py index 2032c68..df95c60 100644 --- a/foamlib/__init__.py +++ b/foamlib/__init__.py @@ -1,14 +1,25 @@ __version__ = "0.1.3" from ._cases import FoamCase, AsyncFoamCase, FoamTimeDirectory -from ._dictionaries import FoamFile, FoamDictionary, FoamDimensioned, FoamDimensionSet +from ._dictionaries import ( + FoamFile, + FoamFieldFile, + FoamDictionary, + FoamBoundariesDictionary, + FoamBoundaryDictionary, + FoamDimensioned, + FoamDimensionSet, +) __all__ = [ "FoamCase", "AsyncFoamCase", "FoamTimeDirectory", "FoamFile", + "FoamFieldFile", "FoamDictionary", + "FoamBoundariesDictionary", + "FoamBoundaryDictionary", "FoamDimensioned", "FoamDimensionSet", ] diff --git a/foamlib/_cases.py b/foamlib/_cases.py index 726cffb..2ca5cdb 100644 --- a/foamlib/_cases.py +++ b/foamlib/_cases.py @@ -22,7 +22,7 @@ import aioshutil from ._subprocesses import run_process, run_process_async, CalledProcessError -from ._dictionaries import FoamFile +from ._dictionaries import FoamFile, FoamFieldFile class _FoamCaseBase(Sequence["FoamTimeDirectory"]): @@ -601,7 +601,7 @@ async def clone(self, dest: Union[Path, str]) -> "AsyncFoamCase": return AsyncFoamCase(dest) -class FoamTimeDirectory(Mapping[str, FoamFile]): +class FoamTimeDirectory(Mapping[str, FoamFieldFile]): """ An OpenFOAM time directory in a case. @@ -629,9 +629,9 @@ def name(self) -> str: """ return self.path.name - def __getitem__(self, key: str) -> FoamFile: + def __getitem__(self, key: str) -> FoamFieldFile: try: - return FoamFile(self.path / key) + return FoamFieldFile(self.path / key) except FileNotFoundError as e: raise KeyError(key) from e diff --git a/foamlib/_dictionaries.py b/foamlib/_dictionaries.py index fd893cb..b1e53df 100644 --- a/foamlib/_dictionaries.py +++ b/foamlib/_dictionaries.py @@ -164,23 +164,6 @@ def _parse(value: str) -> FoamValue: return value -def _serialize_mapping(mapping: Any) -> str: - if isinstance(mapping, FoamDictionary): - return mapping._cmd(["-value"]) - elif isinstance(mapping, Mapping): - m = { - k: _serialize( - v, - assume_field=(k == "internalField" or k == "value"), - assume_dimensions=(k == "dimensions"), - ) - for k, v in mapping.items() - } - return f"{{{' '.join(f'{k} {v};' for k, v in m.items())}}}" - else: - raise TypeError(f"Not a mapping: {type(mapping)}") - - def _serialize_bool(value: Any) -> str: if value is True: return "yes" @@ -251,9 +234,6 @@ def _serialize_dimensioned(value: Any) -> str: def _serialize( value: Any, *, assume_field: bool = False, assume_dimensions: bool = False ) -> str: - with suppress(TypeError): - return _serialize_mapping(value) - if isinstance(value, FoamDimensionSet) or assume_dimensions: with suppress(TypeError): return _serialize_dimensions(value) @@ -316,12 +296,27 @@ def __getitem__(self, key: str) -> Union[FoamValue, "FoamDictionary"]: else: return _parse(value) - def __setitem__(self, key: str, value: Any) -> None: - value = _serialize( - value, - assume_field=(key == "internalField" or key == "value"), - assume_dimensions=(key == "dimensions"), - ) + def _setitem( + self, + key: str, + value: Any, + *, + assume_field: bool = False, + assume_dimensions: bool = False, + ) -> None: + if isinstance(value, FoamDictionary): + value = value._cmd(["-value"]) + elif isinstance(value, Mapping): + self._cmd(["-set", "{}"], key=key) + subdict = self[key] + assert isinstance(subdict, FoamDictionary) + for k, v in value.items(): + subdict[k] = v + return + else: + value = _serialize( + value, assume_field=assume_field, assume_dimensions=assume_dimensions + ) if len(value) < 1000: self._cmd(["-set", value], key=key) @@ -331,6 +326,9 @@ def __setitem__(self, key: str, value: Any) -> None: contents = contents.replace("_foamlib_value_", value, 1) self._file.path.write_text(contents) + def __setitem__(self, key: str, value: Any) -> None: + self._setitem(key, value) + def __delitem__(self, key: str) -> None: if key not in self: raise KeyError(key) @@ -348,6 +346,63 @@ def __repr__(self) -> str: return type(self).__name__ +class FoamBoundaryDictionary(FoamDictionary): + """An OpenFOAM dictionary representing a boundary condition as a mutable mapping.""" + + def __setitem__(self, key: str, value: Any) -> None: + if key == "value": + self._setitem(key, value, assume_field=True) + else: + self._setitem(key, value) + + @property + def type(self) -> str: + """ + Alias of `self["type"]`. + """ + ret = self["type"] + if not isinstance(ret, str): + raise TypeError("type is not a string") + return ret + + @type.setter + def type(self, value: str) -> None: + self["type"] = value + + @property + def value( + self, + ) -> Union[int, float, Sequence[Union[int, float, Sequence[Union[int, float]]]]]: + """ + Alias of `self["value"]`. + """ + ret = self["value"] + if not isinstance(ret, (int, float, Sequence)): + raise TypeError("value is not a field") + return cast(Union[int, float, Sequence[Union[int, float]]], ret) + + @value.setter + def value( + self, + value: Union[ + int, float, Sequence[Union[int, float, Sequence[Union[int, float]]]] + ], + ) -> None: + self["value"] = value + + @value.deleter + def value(self) -> None: + del self["value"] + + +class FoamBoundariesDictionary(FoamDictionary): + def __getitem__(self, key: str) -> Union[FoamValue, FoamBoundaryDictionary]: + ret = super().__getitem__(key) + if isinstance(ret, FoamDictionary): + ret = FoamBoundaryDictionary(self._file, [*self._keywords, key]) + return ret + + class FoamFile(FoamDictionary): """An OpenFOAM dictionary file as a mutable mapping.""" @@ -359,6 +414,30 @@ def __init__(self, path: Union[str, Path]) -> None: elif not self.path.is_file(): raise FileNotFoundError(self.path) + def __fspath__(self) -> str: + return str(self.path) + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.path})" + + +class FoamFieldFile(FoamFile): + """An OpenFOAM dictionary file representing a field as a mutable mapping.""" + + def __getitem__(self, key: str) -> Union[FoamValue, FoamDictionary]: + ret = super().__getitem__(key) + if key == "boundaryField" and isinstance(ret, FoamDictionary): + ret = FoamBoundariesDictionary(self, [key]) + return ret + + def __setitem__(self, key: str, value: Any) -> None: + if key == "internalField": + self._setitem(key, value, assume_field=True) + elif key == "dimensions": + self._setitem(key, value, assume_dimensions=True) + else: + self._setitem(key, value) + @property def dimensions(self) -> FoamDimensionSet: """ @@ -405,9 +484,3 @@ def boundary_field(self) -> FoamDictionary: if not isinstance(ret, FoamDictionary): raise TypeError("boundaryField is not a dictionary") return ret - - def __fspath__(self) -> str: - return str(self.path) - - def __repr__(self) -> str: - return f"{type(self).__name__}({self.path})" diff --git a/tests/test_dictionaries.py b/tests/test_dictionaries.py index 09159d2..e7d4dc5 100644 --- a/tests/test_dictionaries.py +++ b/tests/test_dictionaries.py @@ -97,7 +97,29 @@ def pitz(tmp_path: Path) -> FoamCase: return PITZ.clone(tmp_path / PITZ.name) -def test_field(pitz: FoamCase) -> None: +def test_dimensions(pitz: FoamCase) -> None: + assert pitz[0]["p"].dimensions == FoamDimensionSet(length=2, time=-2) + assert pitz[0]["U"].dimensions == FoamDimensionSet(length=1, time=-1) + + pitz[0]["p"].dimensions = FoamDimensionSet(mass=1, length=1, time=-2) + + assert pitz[0]["p"].dimensions == FoamDimensionSet(mass=1, length=1, time=-2) + + +def test_boundary_field(pitz: FoamCase) -> None: + outlet = pitz[0]["p"].boundary_field["outlet"] + assert isinstance(outlet, FoamBoundaryDictionary) + assert outlet.type == "fixedValue" + assert outlet.value == 0 + + outlet.type = "zeroGradient" + del outlet.value + + assert outlet.type == "zeroGradient" + assert "value" not in outlet + + +def test_internal_field(pitz: FoamCase) -> None: pitz[0]["p"].internal_field = 0.5 pitz[0]["U"].internal_field = [1.5, 2.0, 3] @@ -128,12 +150,3 @@ def test_field(pitz: FoamCase) -> None: assert u == pytest.approx(u_arr) pitz.run() - - -def test_dimensions(pitz: FoamCase) -> None: - assert pitz[0]["p"].dimensions == FoamDimensionSet(length=2, time=-2) - assert pitz[0]["U"].dimensions == FoamDimensionSet(length=1, time=-1) - - pitz[0]["p"].dimensions = FoamDimensionSet(mass=1, length=1, time=-2) - - assert pitz[0]["p"].dimensions == FoamDimensionSet(mass=1, length=1, time=-2)