foamlib 0.2.9__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- foamlib/__init__.py +5 -3
- foamlib/_cases.py +20 -32
- foamlib/_files/__init__.py +9 -0
- foamlib/{_dictionaries → _files}/_base.py +9 -11
- foamlib/_files/_fields.py +158 -0
- foamlib/_files/_files.py +203 -0
- foamlib/_files/_io.py +73 -0
- foamlib/_files/_parsing.py +195 -0
- foamlib/_files/_serialization.py +156 -0
- {foamlib-0.2.9.dist-info → foamlib-0.3.0.dist-info}/METADATA +1 -1
- foamlib-0.3.0.dist-info/RECORD +16 -0
- foamlib/_dictionaries/__init__.py +0 -8
- foamlib/_dictionaries/_files.py +0 -422
- foamlib/_dictionaries/_parsing.py +0 -198
- foamlib/_dictionaries/_serialization.py +0 -129
- foamlib-0.2.9.dist-info/RECORD +0 -14
- {foamlib-0.2.9.dist-info → foamlib-0.3.0.dist-info}/LICENSE.txt +0 -0
- {foamlib-0.2.9.dist-info → foamlib-0.3.0.dist-info}/WHEEL +0 -0
- {foamlib-0.2.9.dist-info → foamlib-0.3.0.dist-info}/top_level.txt +0 -0
foamlib/__init__.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
"""A Python interface for interacting with OpenFOAM."""
|
2
2
|
|
3
|
-
__version__ = "0.
|
3
|
+
__version__ = "0.3.0"
|
4
4
|
|
5
5
|
from ._cases import AsyncFoamCase, FoamCase, FoamCaseBase
|
6
|
-
from .
|
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
|
-
"
|
15
|
+
"FoamDict",
|
16
|
+
"CalledProcessError",
|
15
17
|
]
|
foamlib/_cases.py
CHANGED
@@ -27,8 +27,8 @@ else:
|
|
27
27
|
|
28
28
|
import aioshutil
|
29
29
|
|
30
|
-
from .
|
31
|
-
from ._util import
|
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
|
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
|
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
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
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
|
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
|
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
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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
|
|
@@ -15,7 +15,7 @@ except ModuleNotFoundError:
|
|
15
15
|
pass
|
16
16
|
|
17
17
|
|
18
|
-
class
|
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,
|
41
|
-
self.dimensions =
|
38
|
+
if not isinstance(self.dimensions, FoamDict.DimensionSet):
|
39
|
+
self.dimensions = FoamDict.DimensionSet(*self.dimensions)
|
42
40
|
|
43
|
-
|
41
|
+
Data = Union[
|
44
42
|
str,
|
45
43
|
int,
|
46
44
|
float,
|
47
45
|
bool,
|
48
46
|
Dimensioned,
|
49
47
|
DimensionSet,
|
50
|
-
Sequence["
|
51
|
-
Mapping[str, "
|
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["
|
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
|
-
|
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
|
foamlib/_files/_files.py
ADDED
@@ -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()
|
foamlib/_files/_io.py
ADDED
@@ -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
|