foamlib 0.4.3__py3-none-any.whl → 0.5.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.
@@ -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,23 +1,31 @@
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 (
8
+ Callable,
6
9
  Optional,
10
+ Type,
7
11
  Union,
8
12
  )
9
13
 
10
14
  if sys.version_info >= (3, 9):
11
- from collections.abc import Sequence
15
+ from collections.abc import Collection, Sequence
12
16
  else:
13
- from typing import Sequence
17
+ from typing import Collection, Sequence
14
18
 
15
- from .._util import is_sequence
16
- from ._base import FoamCaseBase
17
- from ._util import check_returncode
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
18
26
 
19
27
 
20
- class FoamCase(FoamCaseBase):
28
+ class FoamCase(_FoamCaseRecipes):
21
29
  """
22
30
  An OpenFOAM case.
23
31
 
@@ -28,6 +36,35 @@ class FoamCase(FoamCaseBase):
28
36
  :param path: The path to the case directory.
29
37
  """
30
38
 
39
+ @staticmethod
40
+ def _rmtree(
41
+ path: Union["os.PathLike[str]", str], *, ignore_errors: bool = False
42
+ ) -> None:
43
+ shutil.rmtree(path, ignore_errors=ignore_errors)
44
+
45
+ @staticmethod
46
+ def _copytree(
47
+ src: Union["os.PathLike[str]", str],
48
+ dest: Union["os.PathLike[str]", str],
49
+ *,
50
+ symlinks: bool = False,
51
+ ignore: Optional[
52
+ Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
53
+ ] = None,
54
+ ) -> None:
55
+ shutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
56
+
57
+ def __enter__(self) -> "FoamCase":
58
+ return self
59
+
60
+ def __exit__(
61
+ self,
62
+ exc_type: Optional[Type[BaseException]],
63
+ exc_val: Optional[BaseException],
64
+ exc_tb: Optional[TracebackType],
65
+ ) -> None:
66
+ self._rmtree(self.path)
67
+
31
68
  def clean(
32
69
  self,
33
70
  *,
@@ -40,47 +77,44 @@ class FoamCase(FoamCaseBase):
40
77
  :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
41
78
  :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
42
79
  """
43
- script_path = self._clean_script() if script else None
44
-
45
- if script_path is not None:
46
- self.run([script_path], check=check)
47
- else:
48
- for p in self._clean_paths():
49
- if p.is_dir():
50
- shutil.rmtree(p)
51
- else:
52
- p.unlink()
80
+ for name, args, kwargs in self._clean_cmds(script=script, check=check):
81
+ getattr(self, name)(*args, **kwargs)
53
82
 
54
83
  def _run(
55
84
  self,
56
- cmd: Union[Sequence[Union[str, Path]], str, Path],
85
+ cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
57
86
  *,
87
+ parallel: bool = False,
88
+ cpus: int = 1,
58
89
  check: bool = True,
90
+ log: bool = True,
59
91
  ) -> None:
60
- shell = not is_sequence(cmd)
61
-
62
- if sys.version_info < (3, 8):
63
- if shell:
64
- cmd = str(cmd)
65
- else:
66
- cmd = (str(arg) for arg in cmd)
67
-
68
- proc = subprocess.run(
69
- cmd,
70
- cwd=self.path,
71
- env=self._env(shell=shell),
72
- stdout=subprocess.DEVNULL,
73
- stderr=subprocess.PIPE if check else subprocess.DEVNULL,
74
- text=True,
75
- shell=shell,
76
- )
77
-
78
- if check:
79
- check_returncode(proc.returncode, cmd, proc.stderr)
92
+ with self._output(cmd, log=log) as (stdout, stderr):
93
+ if parallel:
94
+ if isinstance(cmd, str):
95
+ cmd = [
96
+ "mpiexec",
97
+ "-n",
98
+ str(cpus),
99
+ "/bin/sh",
100
+ "-c",
101
+ f"{cmd} -parallel",
102
+ ]
103
+ else:
104
+ cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
105
+
106
+ run_sync(
107
+ cmd,
108
+ check=check,
109
+ cwd=self.path,
110
+ env=self._env(shell=isinstance(cmd, str)),
111
+ stdout=stdout,
112
+ stderr=stderr,
113
+ )
80
114
 
81
115
  def run(
82
116
  self,
83
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
117
+ cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
84
118
  *,
85
119
  script: bool = True,
86
120
  parallel: Optional[bool] = None,
@@ -113,30 +147,37 @@ class FoamCase(FoamCaseBase):
113
147
 
114
148
  def restore_0_dir(self) -> None:
115
149
  """Restore the 0 directory from the 0.orig directory."""
116
- shutil.rmtree(self.path / "0", ignore_errors=True)
117
- shutil.copytree(self.path / "0.orig", self.path / "0")
150
+ for name, args, kwargs in self._restore_0_dir_cmds():
151
+ getattr(self, name)(*args, **kwargs)
118
152
 
119
- def copy(self, dest: Union[Path, str]) -> "FoamCase":
153
+ def copy(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
120
154
  """
121
155
  Make a copy of this case.
122
156
 
123
- :param dest: The destination path.
157
+ Use as a context manager to automatically delete the copy when done.
158
+
159
+ :param dst: The destination path. If None, copy to a temporary directory.
124
160
  """
125
- return FoamCase(shutil.copytree(self.path, dest, symlinks=True))
161
+ if dst is None:
162
+ dst = Path(tempfile.mkdtemp(), self.name)
126
163
 
127
- def clone(self, dest: Union[Path, str]) -> "FoamCase":
164
+ for name, args, kwargs in self._copy_cmds(dst):
165
+ getattr(self, name)(*args, **kwargs)
166
+
167
+ return type(self)(dst)
168
+
169
+ def clone(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
128
170
  """
129
171
  Clone this case (make a clean copy).
130
172
 
131
- :param dest: The destination path.
132
- """
133
- if self._clean_script() is not None:
134
- copy = self.copy(dest)
135
- copy.clean()
136
- return copy
173
+ Use as a context manager to automatically delete the clone when done.
137
174
 
138
- dest = Path(dest)
175
+ :param dst: The destination path. If None, clone to a temporary directory.
176
+ """
177
+ if dst is None:
178
+ dst = Path(tempfile.mkdtemp(), self.name)
139
179
 
140
- shutil.copytree(self.path, dest, symlinks=True, ignore=self._clone_ignore())
180
+ for name, args, kwargs in self._clone_cmds(dst):
181
+ getattr(self, name)(*args, **kwargs)
141
182
 
142
- return FoamCase(dest)
183
+ return type(self)(dst)
foamlib/_cases/_util.py CHANGED
@@ -1,35 +1,27 @@
1
- import subprocess
2
- import sys
3
- from pathlib import Path
4
- from typing import Optional, Union
5
- from warnings import warn
1
+ from types import TracebackType
2
+ from typing import Any, AsyncContextManager, Callable, Optional, Type
6
3
 
7
- if sys.version_info >= (3, 9):
8
- from collections.abc import Sequence
9
- else:
10
- from typing import Sequence
11
4
 
5
+ class _AwaitableAsyncContextManager:
6
+ def __init__(self, cm: "AsyncContextManager[Any]"):
7
+ self._cm = cm
12
8
 
13
- class CalledProcessError(subprocess.CalledProcessError):
14
- """Exception raised when a process fails and `check=True`."""
9
+ def __await__(self) -> Any:
10
+ return self._cm.__aenter__().__await__()
15
11
 
16
- def __str__(self) -> str:
17
- msg = super().__str__()
18
- if self.stderr:
19
- msg += f"\n{self.stderr}"
20
- return msg
12
+ async def __aenter__(self) -> Any:
13
+ return await self._cm.__aenter__()
21
14
 
15
+ async def __aexit__(
16
+ self,
17
+ exc_type: Optional[Type[BaseException]],
18
+ exc_val: Optional[BaseException],
19
+ exc_tb: Optional[TracebackType],
20
+ ) -> Any:
21
+ return await self._cm.__aexit__(exc_type, exc_val, exc_tb)
22
22
 
23
- class CalledProcessWarning(Warning):
24
- """Warning raised when a process prints to stderr and `check=True`."""
25
23
 
26
-
27
- def check_returncode(
28
- retcode: int,
29
- cmd: Union[Sequence[Union[str, Path]], str, Path],
30
- stderr: Optional[str],
31
- ) -> None:
32
- if retcode != 0:
33
- raise CalledProcessError(retcode, cmd, None, stderr)
34
- elif stderr:
35
- warn(f"Command {cmd} printed to stderr.\n{stderr}", CalledProcessWarning)
24
+ def awaitableasynccontextmanager(
25
+ cm: Callable[..., "AsyncContextManager[Any]"],
26
+ ) -> Callable[..., _AwaitableAsyncContextManager]:
27
+ return lambda *args, **kwargs: _AwaitableAsyncContextManager(cm(*args, **kwargs))
foamlib/_files/_files.py CHANGED
@@ -16,10 +16,10 @@ if sys.version_info >= (3, 11):
16
16
  else:
17
17
  from typing_extensions import Self
18
18
 
19
- from .._util import is_sequence
20
19
  from ._base import FoamFileBase
21
20
  from ._io import _FoamFileIO
22
21
  from ._serialization import Kind, dumpb
22
+ from ._util import is_sequence
23
23
 
24
24
  try:
25
25
  import numpy as np
@@ -105,13 +105,9 @@ class FoamFile(
105
105
  """
106
106
  Create the file.
107
107
 
108
- Parameters
109
- ----------
110
- exist_ok : bool, optional
111
- If False (the default), raise a FileExistsError if the file already exists.
112
- If True, do nothing if the file already exists.
113
- parents : bool, optional
114
- If True, also create parent directories as needed.
108
+ :param exist_ok: If False, raise a FileExistsError if the file already exists.
109
+
110
+ :param parents: If True, also create parent directories as needed.
115
111
  """
116
112
  if self.path.exists():
117
113
  if not exist_ok: