foamlib 0.2.9__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foamlib
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: A Python interface for interacting with OpenFOAM
5
5
  Author-email: "Gabriel S. Gerlero" <ggerlero@cimec.unl.edu.ar>
6
6
  Project-URL: Homepage, https://github.com/gerlero/foamlib
@@ -1,9 +1,10 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.2.9"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  from ._cases import AsyncFoamCase, FoamCase, FoamCaseBase
6
- from ._dictionaries import FoamDictionaryBase, FoamFieldFile, FoamFile
6
+ from ._files import FoamDict, FoamFieldFile, FoamFile
7
+ from ._util import CalledProcessError
7
8
 
8
9
  __all__ = [
9
10
  "FoamCase",
@@ -11,5 +12,6 @@ __all__ = [
11
12
  "FoamCaseBase",
12
13
  "FoamFile",
13
14
  "FoamFieldFile",
14
- "FoamDictionaryBase",
15
+ "FoamDict",
16
+ "CalledProcessError",
15
17
  ]
@@ -27,8 +27,8 @@ else:
27
27
 
28
28
  import aioshutil
29
29
 
30
- from ._dictionaries import FoamFieldFile, FoamFile
31
- from ._util import CalledProcessError, is_sequence, run_process, run_process_async
30
+ from ._files import FoamFieldFile, FoamFile
31
+ from ._util import is_sequence, run_process, run_process_async
32
32
 
33
33
 
34
34
  class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
@@ -325,7 +325,7 @@ class FoamCase(FoamCaseBase):
325
325
  Clean this case.
326
326
 
327
327
  :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
328
- :param check: If True, raise a RuntimeError if the clean script returns a non-zero exit code.
328
+ :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
329
329
  """
330
330
  script_path = self._clean_script() if script else None
331
331
 
@@ -349,24 +349,18 @@ class FoamCase(FoamCaseBase):
349
349
  :param cmd: The command to run. If None, run the case. If a sequence, the first element is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
350
350
  :param script: If True and `cmd` is None, use an (All)run(-parallel) script if it exists for running the case. If False or no run script is found, autodetermine the command(s) needed to run the case.
351
351
  :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
352
- :param check: If True, raise a RuntimeError if any command returns a non-zero exit code.
352
+ :param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
353
353
  """
354
354
  if cmd is not None:
355
355
  if parallel:
356
356
  cmd = self._parallel_cmd(cmd)
357
357
 
358
- try:
359
- run_process(
360
- cmd,
361
- check=check,
362
- cwd=self.path,
363
- env=self._env(),
364
- )
365
- except CalledProcessError as e:
366
- raise RuntimeError(
367
- f"{e.cmd} failed with return code {e.returncode}\n{e.stderr}"
368
- ) from None
369
-
358
+ run_process(
359
+ cmd,
360
+ check=check,
361
+ cwd=self.path,
362
+ env=self._env(),
363
+ )
370
364
  else:
371
365
  script_path = self._run_script(parallel=parallel) if script else None
372
366
 
@@ -485,7 +479,7 @@ class AsyncFoamCase(FoamCaseBase):
485
479
  Clean this case.
486
480
 
487
481
  :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
488
- :param check: If True, raise a RuntimeError if the clean script returns a non-zero exit code.
482
+ :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
489
483
  """
490
484
  script_path = self._clean_script() if script else None
491
485
 
@@ -505,13 +499,13 @@ class AsyncFoamCase(FoamCaseBase):
505
499
  check: bool = True,
506
500
  ) -> None:
507
501
  """
508
- Run this case.
502
+ Run this case, or a specified command in the context of this case.
509
503
 
510
504
  :param cmd: The command to run. If None, run the case. If a sequence, the first element is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
511
505
  :param script: If True and `cmd` is None, use an (All)run(-parallel) script if it exists for running the case. If False or no run script is found, autodetermine the command(s) needed to run the case.
512
506
  :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
513
507
  :param cpus: The number of CPUs to reserve for the run. The run will wait until the requested number of CPUs is available. If None, autodetect the number of CPUs to reserve.
514
- :param check: If True, raise a RuntimeError if a command returns a non-zero exit code.
508
+ :param check: If True, raise a CalledProcessError if a command returns a non-zero exit code.
515
509
  """
516
510
  if cmd is not None:
517
511
  if cpus is None:
@@ -523,19 +517,13 @@ class AsyncFoamCase(FoamCaseBase):
523
517
  if parallel:
524
518
  cmd = self._parallel_cmd(cmd)
525
519
 
526
- try:
527
- async with self._cpus(cpus):
528
- await run_process_async(
529
- cmd,
530
- check=check,
531
- cwd=self.path,
532
- env=self._env(),
533
- )
534
- except CalledProcessError as e:
535
- raise RuntimeError(
536
- f"{e.cmd} failed with return code {e.returncode}\n{e.stderr}"
537
- ) from None
538
-
520
+ async with self._cpus(cpus):
521
+ await run_process_async(
522
+ cmd,
523
+ check=check,
524
+ cwd=self.path,
525
+ env=self._env(),
526
+ )
539
527
  else:
540
528
  script_path = self._run_script(parallel=parallel) if script else None
541
529
 
@@ -0,0 +1,9 @@
1
+ from ._base import FoamDict
2
+ from ._fields import FoamFieldFile
3
+ from ._files import FoamFile
4
+
5
+ __all__ = [
6
+ "FoamFile",
7
+ "FoamFieldFile",
8
+ "FoamDict",
9
+ ]
@@ -15,7 +15,7 @@ except ModuleNotFoundError:
15
15
  pass
16
16
 
17
17
 
18
- class FoamDictionaryBase:
18
+ class FoamDict:
19
19
  class DimensionSet(NamedTuple):
20
20
  mass: Union[int, float] = 0
21
21
  length: Union[int, float] = 0
@@ -31,34 +31,32 @@ class FoamDictionaryBase:
31
31
  @dataclass
32
32
  class Dimensioned:
33
33
  value: Union[int, float, Sequence[Union[int, float]]] = 0
34
- dimensions: Union[
35
- "FoamDictionaryBase.DimensionSet", Sequence[Union[int, float]]
36
- ] = ()
34
+ dimensions: Union["FoamDict.DimensionSet", Sequence[Union[int, float]]] = ()
37
35
  name: Optional[str] = None
38
36
 
39
37
  def __post_init__(self) -> None:
40
- if not isinstance(self.dimensions, FoamDictionaryBase.DimensionSet):
41
- self.dimensions = FoamDictionaryBase.DimensionSet(*self.dimensions)
38
+ if not isinstance(self.dimensions, FoamDict.DimensionSet):
39
+ self.dimensions = FoamDict.DimensionSet(*self.dimensions)
42
40
 
43
- Value = Union[
41
+ Data = Union[
44
42
  str,
45
43
  int,
46
44
  float,
47
45
  bool,
48
46
  Dimensioned,
49
47
  DimensionSet,
50
- Sequence["Value"],
51
- Mapping[str, "Value"],
48
+ Sequence["Data"],
49
+ Mapping[str, "Data"],
52
50
  ]
53
51
  """
54
52
  A value that can be stored in an OpenFOAM dictionary.
55
53
  """
56
54
 
57
- _Dict = Dict[str, Union["Value", "_Dict"]]
55
+ _Dict = Dict[str, Union["Data", "_Dict"]]
58
56
 
59
57
  @abstractmethod
60
58
  def as_dict(self) -> _Dict:
61
59
  """Return a nested dict representation of the dictionary."""
62
60
  raise NotImplementedError
63
61
 
64
- _SetValue = Union[Value, "NDArray[np.generic]"]
62
+ _SetData = Union[Data, "NDArray[np.generic]"]
@@ -0,0 +1,158 @@
1
+ import sys
2
+ from typing import Any, Tuple, Union, cast
3
+
4
+ if sys.version_info >= (3, 9):
5
+ from collections.abc import Sequence
6
+ else:
7
+ from typing import Sequence
8
+
9
+ from ._files import FoamFile
10
+
11
+ try:
12
+ import numpy as np
13
+ from numpy.typing import NDArray
14
+ except ModuleNotFoundError:
15
+ pass
16
+
17
+
18
+ class FoamFieldFile(FoamFile):
19
+ """An OpenFOAM dictionary file representing a field as a mutable mapping."""
20
+
21
+ class BoundariesSubDict(FoamFile.SubDict):
22
+ def __getitem__(self, keyword: str) -> "FoamFieldFile.BoundarySubDict":
23
+ value = super().__getitem__(keyword)
24
+ if not isinstance(value, FoamFieldFile.BoundarySubDict):
25
+ assert not isinstance(value, FoamFile.SubDict)
26
+ raise TypeError(f"boundary {keyword} is not a dictionary")
27
+ return value
28
+
29
+ class BoundarySubDict(FoamFile.SubDict):
30
+ """An OpenFOAM dictionary representing a boundary condition as a mutable mapping."""
31
+
32
+ def __setitem__(
33
+ self,
34
+ keyword: str,
35
+ data: FoamFile._SetData,
36
+ ) -> None:
37
+ if keyword == "value":
38
+ self._setitem(keyword, data, assume_field=True)
39
+ else:
40
+ self._setitem(keyword, data)
41
+
42
+ @property
43
+ def type(self) -> str:
44
+ """Alias of `self["type"]`."""
45
+ ret = self["type"]
46
+ if not isinstance(ret, str):
47
+ raise TypeError("type is not a string")
48
+ return ret
49
+
50
+ @type.setter
51
+ def type(self, data: str) -> None:
52
+ self["type"] = data
53
+
54
+ @property
55
+ def value(
56
+ self,
57
+ ) -> Union[
58
+ int,
59
+ float,
60
+ Sequence[Union[int, float, Sequence[Union[int, float]]]],
61
+ "NDArray[np.generic]",
62
+ ]:
63
+ """Alias of `self["value"]`."""
64
+ ret = self["value"]
65
+ if not isinstance(ret, (int, float, Sequence)):
66
+ raise TypeError("value is not a field")
67
+ return cast(Union[int, float, Sequence[Union[int, float]]], ret)
68
+
69
+ @value.setter
70
+ def value(
71
+ self,
72
+ value: Union[
73
+ int,
74
+ float,
75
+ Sequence[Union[int, float, Sequence[Union[int, float]]]],
76
+ "NDArray[np.generic]",
77
+ ],
78
+ ) -> None:
79
+ self["value"] = value
80
+
81
+ @value.deleter
82
+ def value(self) -> None:
83
+ del self["value"]
84
+
85
+ def __getitem__(
86
+ self, keywords: Union[str, Tuple[str, ...]]
87
+ ) -> Union[FoamFile.Data, FoamFile.SubDict]:
88
+ if not isinstance(keywords, tuple):
89
+ keywords = (keywords,)
90
+
91
+ ret = super().__getitem__(keywords)
92
+ if keywords[0] == "boundaryField" and isinstance(ret, FoamFile.SubDict):
93
+ if len(keywords) == 1:
94
+ ret = FoamFieldFile.BoundariesSubDict(self, keywords)
95
+ elif len(keywords) == 2:
96
+ ret = FoamFieldFile.BoundarySubDict(self, keywords)
97
+ return ret
98
+
99
+ def __setitem__(self, keywords: Union[str, Tuple[str, ...]], value: Any) -> None:
100
+ if not isinstance(keywords, tuple):
101
+ keywords = (keywords,)
102
+
103
+ if keywords == ("internalField",):
104
+ self._setitem(keywords, value, assume_field=True)
105
+ elif keywords == ("dimensions",):
106
+ self._setitem(keywords, value, assume_dimensions=True)
107
+ else:
108
+ self._setitem(keywords, value)
109
+
110
+ @property
111
+ def dimensions(self) -> FoamFile.DimensionSet:
112
+ """Alias of `self["dimensions"]`."""
113
+ ret = self["dimensions"]
114
+ if not isinstance(ret, FoamFile.DimensionSet):
115
+ raise TypeError("dimensions is not a DimensionSet")
116
+ return ret
117
+
118
+ @dimensions.setter
119
+ def dimensions(
120
+ self, value: Union[FoamFile.DimensionSet, Sequence[Union[int, float]]]
121
+ ) -> None:
122
+ self["dimensions"] = value
123
+
124
+ @property
125
+ def internal_field(
126
+ self,
127
+ ) -> Union[
128
+ int,
129
+ float,
130
+ Sequence[Union[int, float, Sequence[Union[int, float]]]],
131
+ "NDArray[np.generic]",
132
+ ]:
133
+ """Alias of `self["internalField"]`."""
134
+ ret = self["internalField"]
135
+ if not isinstance(ret, (int, float, Sequence)):
136
+ raise TypeError("internalField is not a field")
137
+ return cast(Union[int, float, Sequence[Union[int, float]]], ret)
138
+
139
+ @internal_field.setter
140
+ def internal_field(
141
+ self,
142
+ value: Union[
143
+ int,
144
+ float,
145
+ Sequence[Union[int, float, Sequence[Union[int, float]]]],
146
+ "NDArray[np.generic]",
147
+ ],
148
+ ) -> None:
149
+ self["internalField"] = value
150
+
151
+ @property
152
+ def boundary_field(self) -> "FoamFieldFile.BoundariesSubDict":
153
+ """Alias of `self["boundaryField"]`."""
154
+ ret = self["boundaryField"]
155
+ if not isinstance(ret, FoamFieldFile.BoundariesSubDict):
156
+ assert not isinstance(ret, FoamFile.SubDict)
157
+ raise TypeError("boundaryField is not a dictionary")
158
+ return ret
@@ -0,0 +1,203 @@
1
+ import sys
2
+ from typing import (
3
+ Any,
4
+ Tuple,
5
+ Union,
6
+ )
7
+
8
+ if sys.version_info >= (3, 9):
9
+ from collections.abc import Iterator, Mapping, MutableMapping
10
+ else:
11
+ from typing import Iterator, Mapping, MutableMapping
12
+
13
+ from ._base import FoamDict
14
+ from ._io import FoamFileIO
15
+ from ._serialization import serialize_keyword_entry
16
+
17
+
18
+ class FoamFile(
19
+ FoamDict,
20
+ MutableMapping[
21
+ Union[str, Tuple[str, ...]], Union["FoamFile.Data", "FoamFile.SubDict"]
22
+ ],
23
+ FoamFileIO,
24
+ ):
25
+ """
26
+ An OpenFOAM dictionary file.
27
+
28
+ Use as a mutable mapping (i.e., like a dict) to access and modify entries.
29
+
30
+ Use as a context manager to make multiple changes to the file while saving all changes only once at the end.
31
+ """
32
+
33
+ class SubDict(
34
+ FoamDict,
35
+ MutableMapping[str, Union["FoamFile.Data", "FoamFile.SubDict"]],
36
+ ):
37
+ """An OpenFOAM dictionary within a file as a mutable mapping."""
38
+
39
+ def __init__(self, _file: "FoamFile", _keywords: Tuple[str, ...]) -> None:
40
+ self._file = _file
41
+ self._keywords = _keywords
42
+
43
+ def __getitem__(
44
+ self, keyword: str
45
+ ) -> Union["FoamFile.Data", "FoamFile.SubDict"]:
46
+ return self._file[(*self._keywords, keyword)]
47
+
48
+ def _setitem(
49
+ self,
50
+ keyword: str,
51
+ data: Any,
52
+ *,
53
+ assume_field: bool = False,
54
+ assume_dimensions: bool = False,
55
+ ) -> None:
56
+ self._file._setitem(
57
+ (*self._keywords, keyword),
58
+ data,
59
+ assume_field=assume_field,
60
+ assume_dimensions=assume_dimensions,
61
+ )
62
+
63
+ def __setitem__(self, keyword: str, value: "FoamFile._SetData") -> None:
64
+ self._setitem(keyword, value)
65
+
66
+ def __delitem__(self, keyword: str) -> None:
67
+ del self._file[(*self._keywords, keyword)]
68
+
69
+ def __iter__(self) -> Iterator[str]:
70
+ return self._file._iter(self._keywords)
71
+
72
+ def __contains__(self, keyword: object) -> bool:
73
+ return (*self._keywords, keyword) in self._file
74
+
75
+ def __len__(self) -> int:
76
+ return len(list(iter(self)))
77
+
78
+ def update(self, *args: Any, **kwargs: Any) -> None:
79
+ with self._file:
80
+ super().update(*args, **kwargs)
81
+
82
+ def clear(self) -> None:
83
+ with self._file:
84
+ super().clear()
85
+
86
+ def __repr__(self) -> str:
87
+ return f"{type(self).__qualname__}({self._file}, {self._keywords})"
88
+
89
+ def as_dict(self) -> FoamDict._Dict:
90
+ """Return a nested dict representation of the dictionary."""
91
+ ret = self._file.as_dict()
92
+
93
+ for k in self._keywords:
94
+ assert isinstance(ret, dict)
95
+ v = ret[k]
96
+ assert isinstance(v, dict)
97
+ ret = v
98
+
99
+ return ret
100
+
101
+ def __getitem__(
102
+ self, keywords: Union[str, Tuple[str, ...]]
103
+ ) -> Union["FoamFile.Data", "FoamFile.SubDict"]:
104
+ if not isinstance(keywords, tuple):
105
+ keywords = (keywords,)
106
+
107
+ _, parsed = self._read()
108
+
109
+ value = parsed[keywords]
110
+
111
+ if value is ...:
112
+ return FoamFile.SubDict(self, keywords)
113
+ else:
114
+ return value # type: ignore [return-value]
115
+
116
+ def _setitem(
117
+ self,
118
+ keywords: Union[str, Tuple[str, ...]],
119
+ data: "FoamFile._SetData",
120
+ *,
121
+ assume_field: bool = False,
122
+ assume_dimensions: bool = False,
123
+ ) -> None:
124
+ if not isinstance(keywords, tuple):
125
+ keywords = (keywords,)
126
+
127
+ contents, parsed = self._read()
128
+
129
+ if isinstance(data, Mapping):
130
+ with self:
131
+ if isinstance(data, FoamDict):
132
+ data = data.as_dict()
133
+
134
+ start, end = parsed.entry_location(keywords, missing_ok=True)
135
+
136
+ self._write(
137
+ f"{contents[:start]}\n{serialize_keyword_entry(keywords[-1], {})}\n{contents[end:]}"
138
+ )
139
+
140
+ for k, v in data.items():
141
+ self[(*keywords, k)] = v
142
+ else:
143
+ start, end = parsed.entry_location(keywords, missing_ok=True)
144
+
145
+ self._write(
146
+ f"{contents[:start]}\n{serialize_keyword_entry(keywords[-1], data, assume_field=assume_field, assume_dimensions=assume_dimensions)}\n{contents[end:]}"
147
+ )
148
+
149
+ def __setitem__(
150
+ self,
151
+ keywords: Union[str, Tuple[str, ...]],
152
+ data: "FoamFile._SetData",
153
+ ) -> None:
154
+ self._setitem(keywords, data)
155
+
156
+ def __delitem__(self, keywords: Union[str, Tuple[str, ...]]) -> None:
157
+ if not isinstance(keywords, tuple):
158
+ keywords = (keywords,)
159
+
160
+ contents, parsed = self._read()
161
+
162
+ start, end = parsed.entry_location(keywords)
163
+
164
+ self._write(contents[:start] + contents[end:])
165
+
166
+ def _iter(self, keywords: Union[str, Tuple[str, ...]] = ()) -> Iterator[str]:
167
+ if not isinstance(keywords, tuple):
168
+ keywords = (keywords,)
169
+
170
+ _, parsed = self._read()
171
+
172
+ yield from (k[-1] for k in parsed if k[:-1] == keywords)
173
+
174
+ def __iter__(self) -> Iterator[str]:
175
+ return self._iter()
176
+
177
+ def __contains__(self, keywords: object) -> bool:
178
+ if not isinstance(keywords, tuple):
179
+ keywords = (keywords,)
180
+ _, parsed = self._read()
181
+ return keywords in parsed
182
+
183
+ def __len__(self) -> int:
184
+ return len(list(iter(self)))
185
+
186
+ def update(self, *args: Any, **kwargs: Any) -> None:
187
+ with self:
188
+ super().update(*args, **kwargs)
189
+
190
+ def clear(self) -> None:
191
+ with self:
192
+ super().clear()
193
+
194
+ def __fspath__(self) -> str:
195
+ return str(self.path)
196
+
197
+ def __repr__(self) -> str:
198
+ return f"{type(self).__name__}({self.path})"
199
+
200
+ def as_dict(self) -> FoamDict._Dict:
201
+ """Return a nested dict representation of the file."""
202
+ _, parsed = self._read()
203
+ return parsed.as_dict()
@@ -0,0 +1,73 @@
1
+ import sys
2
+ from copy import deepcopy
3
+ from pathlib import Path
4
+ from types import TracebackType
5
+ from typing import (
6
+ Optional,
7
+ Tuple,
8
+ Type,
9
+ Union,
10
+ )
11
+
12
+ if sys.version_info >= (3, 11):
13
+ from typing import Self
14
+ else:
15
+ from typing_extensions import Self
16
+
17
+ from ._parsing import Parsed
18
+
19
+
20
+ class FoamFileIO:
21
+ def __init__(self, path: Union[str, Path]) -> None:
22
+ self.path = Path(path).absolute()
23
+ if self.path.is_dir():
24
+ raise IsADirectoryError(self.path)
25
+ elif not self.path.is_file():
26
+ raise FileNotFoundError(self.path)
27
+
28
+ self.__contents: Optional[str] = None
29
+ self.__parsed: Optional[Parsed] = None
30
+ self.__defer_io = 0
31
+ self.__dirty = False
32
+
33
+ def __enter__(self) -> Self:
34
+ if self.__defer_io == 0:
35
+ self._read()
36
+ self.__defer_io += 1
37
+ return self
38
+
39
+ def __exit__(
40
+ self,
41
+ exc_type: Optional[Type[BaseException]],
42
+ exc_val: Optional[BaseException],
43
+ exc_tb: Optional[TracebackType],
44
+ ) -> None:
45
+ self.__defer_io -= 1
46
+ if self.__defer_io == 0 and self.__dirty:
47
+ assert self.__contents is not None
48
+ self._write(self.__contents)
49
+ assert not self.__dirty
50
+
51
+ def _read(self) -> Tuple[str, Parsed]:
52
+ if not self.__defer_io:
53
+ contents = self.path.read_text()
54
+ if contents != self.__contents:
55
+ self.__contents = contents
56
+ self.__parsed = None
57
+
58
+ assert self.__contents is not None
59
+
60
+ if self.__parsed is None:
61
+ parsed = Parsed(self.__contents)
62
+ self.__parsed = parsed
63
+
64
+ return self.__contents, deepcopy(self.__parsed)
65
+
66
+ def _write(self, contents: str) -> None:
67
+ self.__contents = contents
68
+ self.__parsed = None
69
+ if not self.__defer_io:
70
+ self.path.write_text(contents)
71
+ self.__dirty = False
72
+ else:
73
+ self.__dirty = True