foamlib 0.4.4__py3-none-any.whl → 0.5.1__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 +1 -3
- foamlib/_cases/__init__.py +1 -2
- foamlib/_cases/_async.py +112 -61
- foamlib/_cases/_base.py +3 -253
- foamlib/_cases/_recipes.py +299 -0
- foamlib/_cases/_subprocess.py +86 -0
- foamlib/_cases/_sync.py +100 -46
- foamlib/_cases/_util.py +38 -24
- foamlib/_files/_files.py +4 -8
- foamlib/_files/_serialization.py +1 -1
- {foamlib-0.4.4.dist-info → foamlib-0.5.1.dist-info}/METADATA +29 -1
- foamlib-0.5.1.dist-info/RECORD +21 -0
- foamlib-0.4.4.dist-info/RECORD +0 -19
- /foamlib/{_util.py → _files/_util.py} +0 -0
- {foamlib-0.4.4.dist-info → foamlib-0.5.1.dist-info}/LICENSE.txt +0 -0
- {foamlib-0.4.4.dist-info → foamlib-0.5.1.dist-info}/WHEEL +0 -0
- {foamlib-0.4.4.dist-info → foamlib-0.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
import os
|
2
|
+
import shlex
|
3
|
+
import shutil
|
4
|
+
import sys
|
5
|
+
from contextlib import contextmanager
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import (
|
8
|
+
IO,
|
9
|
+
Any,
|
10
|
+
Optional,
|
11
|
+
Tuple,
|
12
|
+
Union,
|
13
|
+
)
|
14
|
+
|
15
|
+
if sys.version_info >= (3, 9):
|
16
|
+
from collections.abc import (
|
17
|
+
Callable,
|
18
|
+
Collection,
|
19
|
+
Generator,
|
20
|
+
Mapping,
|
21
|
+
Sequence,
|
22
|
+
Set,
|
23
|
+
)
|
24
|
+
else:
|
25
|
+
from typing import AbstractSet as Set
|
26
|
+
from typing import (
|
27
|
+
Callable,
|
28
|
+
Collection,
|
29
|
+
Generator,
|
30
|
+
Mapping,
|
31
|
+
Sequence,
|
32
|
+
)
|
33
|
+
|
34
|
+
from ._base import FoamCaseBase
|
35
|
+
from ._subprocess import DEVNULL, STDOUT
|
36
|
+
|
37
|
+
|
38
|
+
class _FoamCaseRecipes(FoamCaseBase):
|
39
|
+
def _clean_paths(self) -> Set[Path]:
|
40
|
+
has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
|
41
|
+
has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
|
42
|
+
|
43
|
+
paths = set()
|
44
|
+
|
45
|
+
for p in self.path.iterdir():
|
46
|
+
if p.is_dir():
|
47
|
+
try:
|
48
|
+
t = float(p.name)
|
49
|
+
except ValueError:
|
50
|
+
pass
|
51
|
+
else:
|
52
|
+
if t != 0:
|
53
|
+
paths.add(p)
|
54
|
+
|
55
|
+
if has_decompose_par_dict and p.name.startswith("processor"):
|
56
|
+
paths.add(p)
|
57
|
+
|
58
|
+
if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
|
59
|
+
paths.add(self.path / "0")
|
60
|
+
|
61
|
+
if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
|
62
|
+
paths.add(self.path / "constant" / "polyMesh")
|
63
|
+
|
64
|
+
if self._run_script() is not None:
|
65
|
+
paths.update(self.path.glob("log.*"))
|
66
|
+
|
67
|
+
return paths
|
68
|
+
|
69
|
+
def __delitem__(self, key: Union[int, float, str]) -> None:
|
70
|
+
shutil.rmtree(self[key].path)
|
71
|
+
|
72
|
+
def _clone_ignore(
|
73
|
+
self,
|
74
|
+
) -> Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]:
|
75
|
+
clean_paths = self._clean_paths()
|
76
|
+
|
77
|
+
def ignore(
|
78
|
+
path: Union["os.PathLike[str]", str], names: Collection[str]
|
79
|
+
) -> Collection[str]:
|
80
|
+
paths = {Path(path) / name for name in names}
|
81
|
+
return {p.name for p in paths.intersection(clean_paths)}
|
82
|
+
|
83
|
+
return ignore
|
84
|
+
|
85
|
+
def _clean_script(self) -> Optional[Path]:
|
86
|
+
"""Return the path to the (All)clean script, or None if no clean script is found."""
|
87
|
+
clean = self.path / "clean"
|
88
|
+
all_clean = self.path / "Allclean"
|
89
|
+
|
90
|
+
if clean.is_file():
|
91
|
+
script = clean
|
92
|
+
elif all_clean.is_file():
|
93
|
+
script = all_clean
|
94
|
+
else:
|
95
|
+
return None
|
96
|
+
|
97
|
+
if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
|
98
|
+
return None
|
99
|
+
|
100
|
+
return script if Path(sys.argv[0]).absolute() != script.absolute() else None
|
101
|
+
|
102
|
+
def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
|
103
|
+
"""Return the path to the (All)run script, or None if no run script is found."""
|
104
|
+
run = self.path / "run"
|
105
|
+
run_parallel = self.path / "run-parallel"
|
106
|
+
all_run = self.path / "Allrun"
|
107
|
+
all_run_parallel = self.path / "Allrun-parallel"
|
108
|
+
|
109
|
+
if run.is_file() or all_run.is_file():
|
110
|
+
if run_parallel.is_file() or all_run_parallel.is_file():
|
111
|
+
if parallel:
|
112
|
+
script = (
|
113
|
+
run_parallel if run_parallel.is_file() else all_run_parallel
|
114
|
+
)
|
115
|
+
elif parallel is False:
|
116
|
+
script = run if run.is_file() else all_run
|
117
|
+
else:
|
118
|
+
raise ValueError(
|
119
|
+
"Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
|
120
|
+
)
|
121
|
+
else:
|
122
|
+
script = run if run.is_file() else all_run
|
123
|
+
elif parallel is not False and (
|
124
|
+
run_parallel.is_file() or all_run_parallel.is_file()
|
125
|
+
):
|
126
|
+
script = run_parallel if run_parallel.is_file() else all_run_parallel
|
127
|
+
else:
|
128
|
+
return None
|
129
|
+
|
130
|
+
if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
|
131
|
+
return None
|
132
|
+
|
133
|
+
return script
|
134
|
+
|
135
|
+
def _env(self, *, shell: bool) -> Optional[Mapping[str, str]]:
|
136
|
+
sip_workaround = os.environ.get(
|
137
|
+
"FOAM_LD_LIBRARY_PATH", ""
|
138
|
+
) and not os.environ.get("DYLD_LIBRARY_PATH", "")
|
139
|
+
|
140
|
+
if not shell or sip_workaround:
|
141
|
+
env = os.environ.copy()
|
142
|
+
|
143
|
+
if not shell:
|
144
|
+
env["PWD"] = str(self.path)
|
145
|
+
|
146
|
+
if sip_workaround:
|
147
|
+
env["DYLD_LIBRARY_PATH"] = env["FOAM_LD_LIBRARY_PATH"]
|
148
|
+
|
149
|
+
return env
|
150
|
+
else:
|
151
|
+
return None
|
152
|
+
|
153
|
+
@contextmanager
|
154
|
+
def _output(
|
155
|
+
self, cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str], *, log: bool
|
156
|
+
) -> Generator[Tuple[Union[int, IO[bytes]], Union[int, IO[bytes]]], None, None]:
|
157
|
+
if log:
|
158
|
+
if isinstance(cmd, str):
|
159
|
+
name = shlex.split(cmd)[0]
|
160
|
+
else:
|
161
|
+
if isinstance(cmd[0], os.PathLike):
|
162
|
+
name = Path(cmd[0]).name
|
163
|
+
else:
|
164
|
+
name = cmd[0]
|
165
|
+
|
166
|
+
with (self.path / f"log.{name}").open("ab") as stdout:
|
167
|
+
yield stdout, STDOUT
|
168
|
+
else:
|
169
|
+
yield DEVNULL, DEVNULL
|
170
|
+
|
171
|
+
def _copy_cmds(
|
172
|
+
self, dest: Union["os.PathLike[str]", str]
|
173
|
+
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
174
|
+
yield (
|
175
|
+
"_copytree",
|
176
|
+
(
|
177
|
+
self.path,
|
178
|
+
dest,
|
179
|
+
),
|
180
|
+
{"symlinks": True},
|
181
|
+
)
|
182
|
+
|
183
|
+
def _clean_cmds(
|
184
|
+
self, *, script: bool = True, check: bool = False
|
185
|
+
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
186
|
+
script_path = self._clean_script() if script else None
|
187
|
+
|
188
|
+
if script_path is not None:
|
189
|
+
yield ("_run", ([script_path],), {"cpus": 0, "check": check, "log": False})
|
190
|
+
else:
|
191
|
+
for p in self._clean_paths():
|
192
|
+
if p.is_dir():
|
193
|
+
yield ("_rmtree", (p,), {})
|
194
|
+
else:
|
195
|
+
p.unlink()
|
196
|
+
|
197
|
+
def _clone_cmds(
|
198
|
+
self, dest: Union["os.PathLike[str]", str]
|
199
|
+
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
200
|
+
if self._clean_script() is not None:
|
201
|
+
yield ("copy", (dest,), {})
|
202
|
+
yield ("clean", (), {})
|
203
|
+
else:
|
204
|
+
yield (
|
205
|
+
"_copytree",
|
206
|
+
(
|
207
|
+
self.path,
|
208
|
+
dest,
|
209
|
+
),
|
210
|
+
{"symlinks": True, "ignore": self._clone_ignore()},
|
211
|
+
)
|
212
|
+
|
213
|
+
def _restore_0_dir_cmds(
|
214
|
+
self,
|
215
|
+
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
216
|
+
yield ("_rmtree", (self.path / "0",), {"ignore_errors": True})
|
217
|
+
yield (
|
218
|
+
"_copytree",
|
219
|
+
(
|
220
|
+
self.path / "0.orig",
|
221
|
+
self.path / "0",
|
222
|
+
),
|
223
|
+
{"symlinks": True},
|
224
|
+
)
|
225
|
+
|
226
|
+
def _run_cmds(
|
227
|
+
self,
|
228
|
+
cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
|
229
|
+
*,
|
230
|
+
script: bool = True,
|
231
|
+
parallel: Optional[bool] = None,
|
232
|
+
cpus: Optional[int] = None,
|
233
|
+
check: bool = True,
|
234
|
+
) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
|
235
|
+
if cmd is not None:
|
236
|
+
if parallel:
|
237
|
+
if cpus is None:
|
238
|
+
cpus = max(self._nprocessors, 1)
|
239
|
+
else:
|
240
|
+
parallel = False
|
241
|
+
if cpus is None:
|
242
|
+
cpus = 1
|
243
|
+
|
244
|
+
yield ("_run", (cmd,), {"parallel": parallel, "cpus": cpus, "check": check})
|
245
|
+
|
246
|
+
else:
|
247
|
+
script_path = self._run_script(parallel=parallel) if script else None
|
248
|
+
|
249
|
+
if script_path is not None:
|
250
|
+
if parallel or parallel is None:
|
251
|
+
if cpus is None:
|
252
|
+
if self._nprocessors > 0:
|
253
|
+
cpus = self._nprocessors
|
254
|
+
elif (self.path / "system" / "decomposeParDict").is_file():
|
255
|
+
cpus = self._nsubdomains
|
256
|
+
else:
|
257
|
+
cpus = 1
|
258
|
+
else:
|
259
|
+
if cpus is None:
|
260
|
+
cpus = 1
|
261
|
+
|
262
|
+
yield (
|
263
|
+
"_run",
|
264
|
+
([script_path],),
|
265
|
+
{"parallel": False, "cpus": cpus, "check": check},
|
266
|
+
)
|
267
|
+
|
268
|
+
else:
|
269
|
+
if not self and (self.path / "0.orig").is_dir():
|
270
|
+
yield ("restore_0_dir", (), {})
|
271
|
+
|
272
|
+
if (self.path / "system" / "blockMeshDict").is_file():
|
273
|
+
yield ("block_mesh", (), {"check": check})
|
274
|
+
|
275
|
+
if parallel is None:
|
276
|
+
parallel = (
|
277
|
+
(cpus is not None and cpus > 1)
|
278
|
+
or self._nprocessors > 0
|
279
|
+
or (self.path / "system" / "decomposeParDict").is_file()
|
280
|
+
)
|
281
|
+
|
282
|
+
if parallel:
|
283
|
+
if (
|
284
|
+
self._nprocessors == 0
|
285
|
+
and (self.path / "system" / "decomposeParDict").is_file()
|
286
|
+
):
|
287
|
+
yield ("decompose_par", (), {"check": check})
|
288
|
+
|
289
|
+
if cpus is None:
|
290
|
+
cpus = max(self._nprocessors, 1)
|
291
|
+
else:
|
292
|
+
if cpus is None:
|
293
|
+
cpus = 1
|
294
|
+
|
295
|
+
yield (
|
296
|
+
"_run",
|
297
|
+
([self.application],),
|
298
|
+
{"parallel": parallel, "cpus": cpus, "check": check},
|
299
|
+
)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
from typing import IO, Optional, Union
|
6
|
+
|
7
|
+
if sys.version_info >= (3, 9):
|
8
|
+
from collections.abc import Mapping, Sequence
|
9
|
+
else:
|
10
|
+
from typing import Mapping, Sequence
|
11
|
+
|
12
|
+
CalledProcessError = subprocess.CalledProcessError
|
13
|
+
CompletedProcess = subprocess.CompletedProcess
|
14
|
+
|
15
|
+
DEVNULL = subprocess.DEVNULL
|
16
|
+
PIPE = subprocess.PIPE
|
17
|
+
STDOUT = subprocess.STDOUT
|
18
|
+
|
19
|
+
|
20
|
+
def run_sync(
|
21
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
22
|
+
*,
|
23
|
+
check: bool = True,
|
24
|
+
cwd: Optional["os.PathLike[str]"] = None,
|
25
|
+
env: Optional[Mapping[str, str]] = None,
|
26
|
+
stdout: Optional[Union[int, IO[bytes]]] = None,
|
27
|
+
stderr: Optional[Union[int, IO[bytes]]] = None,
|
28
|
+
) -> "CompletedProcess[bytes]":
|
29
|
+
if not isinstance(cmd, str) and sys.version_info < (3, 8):
|
30
|
+
cmd = [str(arg) for arg in cmd]
|
31
|
+
|
32
|
+
return subprocess.run(
|
33
|
+
cmd,
|
34
|
+
cwd=cwd,
|
35
|
+
env=env,
|
36
|
+
stdout=stdout,
|
37
|
+
stderr=stderr,
|
38
|
+
shell=isinstance(cmd, str),
|
39
|
+
check=check,
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
async def run_async(
|
44
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
45
|
+
*,
|
46
|
+
check: bool = True,
|
47
|
+
cwd: Optional["os.PathLike[str]"] = None,
|
48
|
+
env: Optional[Mapping[str, str]] = None,
|
49
|
+
stdout: Optional[Union[int, IO[bytes]]] = None,
|
50
|
+
stderr: Optional[Union[int, IO[bytes]]] = None,
|
51
|
+
) -> "CompletedProcess[bytes]":
|
52
|
+
if isinstance(cmd, str):
|
53
|
+
proc = await asyncio.create_subprocess_shell(
|
54
|
+
cmd,
|
55
|
+
cwd=cwd,
|
56
|
+
env=env,
|
57
|
+
stdout=stdout,
|
58
|
+
stderr=stderr,
|
59
|
+
)
|
60
|
+
|
61
|
+
else:
|
62
|
+
if sys.version_info < (3, 8):
|
63
|
+
cmd = [str(arg) for arg in cmd]
|
64
|
+
proc = await asyncio.create_subprocess_exec(
|
65
|
+
*cmd,
|
66
|
+
cwd=cwd,
|
67
|
+
env=env,
|
68
|
+
stdout=stdout,
|
69
|
+
stderr=stderr,
|
70
|
+
)
|
71
|
+
|
72
|
+
output, error = await proc.communicate()
|
73
|
+
|
74
|
+
assert proc.returncode is not None
|
75
|
+
|
76
|
+
if check and proc.returncode != 0:
|
77
|
+
raise CalledProcessError(
|
78
|
+
returncode=proc.returncode,
|
79
|
+
cmd=cmd,
|
80
|
+
output=output,
|
81
|
+
stderr=error,
|
82
|
+
)
|
83
|
+
|
84
|
+
return CompletedProcess(
|
85
|
+
cmd, returncode=proc.returncode, stdout=output, stderr=error
|
86
|
+
)
|
foamlib/_cases/_sync.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
import os
|
1
2
|
import shutil
|
2
|
-
import subprocess
|
3
3
|
import sys
|
4
|
+
import tempfile
|
4
5
|
from pathlib import Path
|
6
|
+
from types import TracebackType
|
5
7
|
from typing import (
|
6
8
|
Callable,
|
7
9
|
Optional,
|
10
|
+
Type,
|
8
11
|
Union,
|
9
12
|
)
|
10
13
|
|
@@ -13,12 +16,16 @@ if sys.version_info >= (3, 9):
|
|
13
16
|
else:
|
14
17
|
from typing import Collection, Sequence
|
15
18
|
|
16
|
-
|
17
|
-
from
|
18
|
-
|
19
|
+
if sys.version_info >= (3, 11):
|
20
|
+
from typing import Self
|
21
|
+
else:
|
22
|
+
from typing_extensions import Self
|
23
|
+
|
24
|
+
from ._recipes import _FoamCaseRecipes
|
25
|
+
from ._subprocess import run_sync
|
19
26
|
|
20
27
|
|
21
|
-
class FoamCase(
|
28
|
+
class FoamCase(_FoamCaseRecipes):
|
22
29
|
"""
|
23
30
|
An OpenFOAM case.
|
24
31
|
|
@@ -29,22 +36,51 @@ class FoamCase(FoamCaseBase):
|
|
29
36
|
:param path: The path to the case directory.
|
30
37
|
"""
|
31
38
|
|
39
|
+
def __init__(self, path: Union["os.PathLike[str]", str] = Path()):
|
40
|
+
super().__init__(path)
|
41
|
+
self._tmp: Optional[bool] = None
|
42
|
+
|
32
43
|
@staticmethod
|
33
|
-
def _rmtree(
|
44
|
+
def _rmtree(
|
45
|
+
path: Union["os.PathLike[str]", str], *, ignore_errors: bool = False
|
46
|
+
) -> None:
|
34
47
|
shutil.rmtree(path, ignore_errors=ignore_errors)
|
35
48
|
|
36
49
|
@staticmethod
|
37
50
|
def _copytree(
|
38
|
-
src:
|
39
|
-
dest:
|
51
|
+
src: Union["os.PathLike[str]", str],
|
52
|
+
dest: Union["os.PathLike[str]", str],
|
40
53
|
*,
|
41
54
|
symlinks: bool = False,
|
42
55
|
ignore: Optional[
|
43
|
-
Callable[[Union[
|
56
|
+
Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
|
44
57
|
] = None,
|
45
58
|
) -> None:
|
46
59
|
shutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
|
47
60
|
|
61
|
+
def __enter__(self) -> "FoamCase":
|
62
|
+
if self._tmp is None:
|
63
|
+
raise RuntimeError(
|
64
|
+
"Cannot use a non-copied/cloned case as a context manager"
|
65
|
+
)
|
66
|
+
return self
|
67
|
+
|
68
|
+
def __exit__(
|
69
|
+
self,
|
70
|
+
exc_type: Optional[Type[BaseException]],
|
71
|
+
exc_val: Optional[BaseException],
|
72
|
+
exc_tb: Optional[TracebackType],
|
73
|
+
) -> None:
|
74
|
+
if self._tmp is not None:
|
75
|
+
if self._tmp:
|
76
|
+
self._rmtree(self.path.parent)
|
77
|
+
else:
|
78
|
+
self._rmtree(self.path)
|
79
|
+
else:
|
80
|
+
raise RuntimeError(
|
81
|
+
"Cannot use a non-copied/cloned case as a context manager"
|
82
|
+
)
|
83
|
+
|
48
84
|
def clean(
|
49
85
|
self,
|
50
86
|
*,
|
@@ -62,43 +98,39 @@ class FoamCase(FoamCaseBase):
|
|
62
98
|
|
63
99
|
def _run(
|
64
100
|
self,
|
65
|
-
cmd: Union[Sequence[Union[str,
|
101
|
+
cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
|
66
102
|
*,
|
67
103
|
parallel: bool = False,
|
68
104
|
cpus: int = 1,
|
69
105
|
check: bool = True,
|
106
|
+
log: bool = True,
|
70
107
|
) -> None:
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
shell=shell,
|
94
|
-
)
|
95
|
-
|
96
|
-
if check:
|
97
|
-
check_returncode(proc.returncode, cmd, proc.stderr)
|
108
|
+
with self._output(cmd, log=log) as (stdout, stderr):
|
109
|
+
if parallel:
|
110
|
+
if isinstance(cmd, str):
|
111
|
+
cmd = [
|
112
|
+
"mpiexec",
|
113
|
+
"-n",
|
114
|
+
str(cpus),
|
115
|
+
"/bin/sh",
|
116
|
+
"-c",
|
117
|
+
f"{cmd} -parallel",
|
118
|
+
]
|
119
|
+
else:
|
120
|
+
cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
|
121
|
+
|
122
|
+
run_sync(
|
123
|
+
cmd,
|
124
|
+
check=check,
|
125
|
+
cwd=self.path,
|
126
|
+
env=self._env(shell=isinstance(cmd, str)),
|
127
|
+
stdout=stdout,
|
128
|
+
stderr=stderr,
|
129
|
+
)
|
98
130
|
|
99
131
|
def run(
|
100
132
|
self,
|
101
|
-
cmd: Optional[Union[Sequence[Union[str,
|
133
|
+
cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
|
102
134
|
*,
|
103
135
|
script: bool = True,
|
104
136
|
parallel: Optional[bool] = None,
|
@@ -134,24 +166,46 @@ class FoamCase(FoamCaseBase):
|
|
134
166
|
for name, args, kwargs in self._restore_0_dir_cmds():
|
135
167
|
getattr(self, name)(*args, **kwargs)
|
136
168
|
|
137
|
-
def copy(self,
|
169
|
+
def copy(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
|
138
170
|
"""
|
139
171
|
Make a copy of this case.
|
140
172
|
|
141
|
-
|
173
|
+
Use as a context manager to automatically delete the copy when done.
|
174
|
+
|
175
|
+
:param dst: The destination path. If None, copy to a temporary directory.
|
142
176
|
"""
|
143
|
-
|
177
|
+
if dst is None:
|
178
|
+
dst = Path(tempfile.mkdtemp(), self.name)
|
179
|
+
tmp = True
|
180
|
+
else:
|
181
|
+
tmp = False
|
182
|
+
|
183
|
+
for name, args, kwargs in self._copy_cmds(dst):
|
144
184
|
getattr(self, name)(*args, **kwargs)
|
145
185
|
|
146
|
-
|
186
|
+
ret = type(self)(dst)
|
187
|
+
ret._tmp = tmp
|
188
|
+
|
189
|
+
return ret
|
147
190
|
|
148
|
-
def clone(self,
|
191
|
+
def clone(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
|
149
192
|
"""
|
150
193
|
Clone this case (make a clean copy).
|
151
194
|
|
152
|
-
|
195
|
+
Use as a context manager to automatically delete the clone when done.
|
196
|
+
|
197
|
+
:param dst: The destination path. If None, clone to a temporary directory.
|
153
198
|
"""
|
154
|
-
|
199
|
+
if dst is None:
|
200
|
+
dst = Path(tempfile.mkdtemp(), self.name)
|
201
|
+
tmp = True
|
202
|
+
else:
|
203
|
+
tmp = False
|
204
|
+
|
205
|
+
for name, args, kwargs in self._clone_cmds(dst):
|
155
206
|
getattr(self, name)(*args, **kwargs)
|
156
207
|
|
157
|
-
|
208
|
+
ret = type(self)(dst)
|
209
|
+
ret._tmp = tmp
|
210
|
+
|
211
|
+
return ret
|
foamlib/_cases/_util.py
CHANGED
@@ -1,35 +1,49 @@
|
|
1
|
-
import
|
1
|
+
import functools
|
2
2
|
import sys
|
3
|
-
from
|
4
|
-
from typing import
|
5
|
-
|
3
|
+
from types import TracebackType
|
4
|
+
from typing import (
|
5
|
+
Any,
|
6
|
+
AsyncContextManager,
|
7
|
+
Callable,
|
8
|
+
Generic,
|
9
|
+
Optional,
|
10
|
+
Type,
|
11
|
+
TypeVar,
|
12
|
+
)
|
6
13
|
|
7
14
|
if sys.version_info >= (3, 9):
|
8
|
-
from collections.abc import
|
15
|
+
from collections.abc import Generator
|
9
16
|
else:
|
10
|
-
from typing import
|
17
|
+
from typing import Generator
|
11
18
|
|
12
19
|
|
13
|
-
|
14
|
-
"""Exception raised when a process fails and `check=True`."""
|
20
|
+
R = TypeVar("R")
|
15
21
|
|
16
|
-
def __str__(self) -> str:
|
17
|
-
msg = super().__str__()
|
18
|
-
if self.stderr:
|
19
|
-
msg += f"\n{self.stderr}"
|
20
|
-
return msg
|
21
22
|
|
23
|
+
class _AwaitableAsyncContextManager(Generic[R]):
|
24
|
+
def __init__(self, cm: "AsyncContextManager[R]"):
|
25
|
+
self._cm = cm
|
22
26
|
|
23
|
-
|
24
|
-
|
27
|
+
def __await__(self) -> Generator[Any, Any, R]:
|
28
|
+
return self._cm.__aenter__().__await__()
|
25
29
|
|
30
|
+
async def __aenter__(self) -> R:
|
31
|
+
return await self._cm.__aenter__()
|
26
32
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
async def __aexit__(
|
34
|
+
self,
|
35
|
+
exc_type: Optional[Type[BaseException]],
|
36
|
+
exc_val: Optional[BaseException],
|
37
|
+
exc_tb: Optional[TracebackType],
|
38
|
+
) -> Optional[bool]:
|
39
|
+
return await self._cm.__aexit__(exc_type, exc_val, exc_tb)
|
40
|
+
|
41
|
+
|
42
|
+
def awaitableasynccontextmanager(
|
43
|
+
cm: Callable[..., "AsyncContextManager[R]"],
|
44
|
+
) -> Callable[..., _AwaitableAsyncContextManager[R]]:
|
45
|
+
@functools.wraps(cm)
|
46
|
+
def f(*args: Any, **kwargs: Any) -> _AwaitableAsyncContextManager[R]:
|
47
|
+
return _AwaitableAsyncContextManager(cm(*args, **kwargs))
|
48
|
+
|
49
|
+
return f
|