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.
foamlib/__init__.py CHANGED
@@ -1,11 +1,10 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.4.3"
3
+ __version__ = "0.5.0"
4
4
 
5
5
  from ._cases import (
6
6
  AsyncFoamCase,
7
7
  CalledProcessError,
8
- CalledProcessWarning,
9
8
  FoamCase,
10
9
  FoamCaseBase,
11
10
  )
@@ -19,5 +18,4 @@ __all__ = [
19
18
  "FoamFieldFile",
20
19
  "FoamFileBase",
21
20
  "CalledProcessError",
22
- "CalledProcessWarning",
23
21
  ]
@@ -1,12 +1,11 @@
1
1
  from ._async import AsyncFoamCase
2
2
  from ._base import FoamCaseBase
3
+ from ._subprocess import CalledProcessError
3
4
  from ._sync import FoamCase
4
- from ._util import CalledProcessError, CalledProcessWarning
5
5
 
6
6
  __all__ = [
7
7
  "FoamCaseBase",
8
8
  "FoamCase",
9
9
  "AsyncFoamCase",
10
10
  "CalledProcessError",
11
- "CalledProcessWarning",
12
11
  ]
foamlib/_cases/_async.py CHANGED
@@ -1,26 +1,34 @@
1
1
  import asyncio
2
2
  import multiprocessing
3
+ import os
3
4
  import sys
5
+ import tempfile
4
6
  from contextlib import asynccontextmanager
5
7
  from pathlib import Path
6
8
  from typing import (
9
+ Callable,
7
10
  Optional,
8
11
  Union,
9
12
  )
10
13
 
11
14
  if sys.version_info >= (3, 9):
12
- from collections.abc import AsyncGenerator, Sequence
15
+ from collections.abc import AsyncGenerator, Collection, Sequence
13
16
  else:
14
- from typing import AsyncGenerator, Sequence
17
+ from typing import AsyncGenerator, Collection, Sequence
18
+
19
+ if sys.version_info >= (3, 11):
20
+ from typing import Self
21
+ else:
22
+ from typing_extensions import Self
15
23
 
16
24
  import aioshutil
17
25
 
18
- from .._util import is_sequence
19
- from ._base import FoamCaseBase
20
- from ._util import check_returncode
26
+ from ._recipes import _FoamCaseRecipes
27
+ from ._subprocess import run_async
28
+ from ._util import awaitableasynccontextmanager
21
29
 
22
30
 
23
- class AsyncFoamCase(FoamCaseBase):
31
+ class AsyncFoamCase(_FoamCaseRecipes):
24
32
  """
25
33
  An OpenFOAM case with asynchronous support.
26
34
 
@@ -61,6 +69,24 @@ class AsyncFoamCase(FoamCaseBase):
61
69
  AsyncFoamCase._reserved_cpus -= cpus
62
70
  AsyncFoamCase._cpus_cond.notify(cpus)
63
71
 
72
+ @staticmethod
73
+ async def _rmtree(
74
+ path: Union["os.PathLike[str]", str], ignore_errors: bool = False
75
+ ) -> None:
76
+ await aioshutil.rmtree(path, ignore_errors=ignore_errors) # type: ignore [call-arg]
77
+
78
+ @staticmethod
79
+ async def _copytree(
80
+ src: Union["os.PathLike[str]", str],
81
+ dest: Union["os.PathLike[str]", str],
82
+ *,
83
+ symlinks: bool = False,
84
+ ignore: Optional[
85
+ Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
86
+ ] = None,
87
+ ) -> None:
88
+ await aioshutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
89
+
64
90
  async def clean(
65
91
  self,
66
92
  *,
@@ -73,74 +99,63 @@ class AsyncFoamCase(FoamCaseBase):
73
99
  :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
74
100
  :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
75
101
  """
76
- script_path = self._clean_script() if script else None
77
-
78
- if script_path is not None:
79
- await self.run([script_path], check=check)
80
- else:
81
- for p in self._clean_paths():
82
- if p.is_dir():
83
- await aioshutil.rmtree(p) # type: ignore [call-arg]
84
- else:
85
- p.unlink()
102
+ for name, args, kwargs in self._clean_cmds(script=script, check=check):
103
+ await getattr(self, name)(*args, **kwargs)
86
104
 
87
105
  async def _run(
88
106
  self,
89
- cmd: Union[Sequence[Union[str, Path]], str, Path],
107
+ cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
90
108
  *,
109
+ parallel: bool = False,
110
+ cpus: int = 1,
91
111
  check: bool = True,
112
+ log: bool = True,
92
113
  ) -> None:
93
- if not is_sequence(cmd):
94
- proc = await asyncio.create_subprocess_shell(
95
- str(cmd),
96
- cwd=self.path,
97
- env=self._env(shell=True),
98
- stdout=asyncio.subprocess.DEVNULL,
99
- stderr=asyncio.subprocess.PIPE if check else asyncio.subprocess.DEVNULL,
100
- )
101
-
102
- else:
103
- if sys.version_info < (3, 8):
104
- cmd = (str(arg) for arg in cmd)
105
- proc = await asyncio.create_subprocess_exec(
106
- *cmd,
107
- cwd=self.path,
108
- env=self._env(shell=False),
109
- stdout=asyncio.subprocess.DEVNULL,
110
- stderr=asyncio.subprocess.PIPE if check else asyncio.subprocess.DEVNULL,
111
- )
112
-
113
- stdout, stderr = await proc.communicate()
114
-
115
- assert stdout is None
116
- assert proc.returncode is not None
114
+ with self._output(cmd, log=log) as (stdout, stderr):
115
+ if parallel:
116
+ if isinstance(cmd, str):
117
+ cmd = [
118
+ "mpiexec",
119
+ "-n",
120
+ str(cpus),
121
+ "/bin/sh",
122
+ "-c",
123
+ f"{cmd} -parallel",
124
+ ]
125
+ else:
126
+ cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
117
127
 
118
- if check:
119
- check_returncode(proc.returncode, cmd, stderr.decode())
128
+ async with self._cpus(cpus):
129
+ await run_async(
130
+ cmd,
131
+ check=check,
132
+ cwd=self.path,
133
+ env=self._env(shell=isinstance(cmd, str)),
134
+ stdout=stdout,
135
+ stderr=stderr,
136
+ )
120
137
 
121
138
  async def run(
122
139
  self,
123
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
140
+ cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
124
141
  *,
125
142
  script: bool = True,
126
143
  parallel: Optional[bool] = None,
127
144
  cpus: Optional[int] = None,
128
145
  check: bool = True,
129
146
  ) -> None:
147
+ """
148
+ Run this case, or a specified command in the context of this case.
149
+
150
+ :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.
151
+ :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.
152
+ :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
153
+ :param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
154
+ """
130
155
  for name, args, kwargs in self._run_cmds(
131
- cmd=cmd, script=script, parallel=parallel, check=check
156
+ cmd=cmd, script=script, parallel=parallel, cpus=cpus, check=check
132
157
  ):
133
- if cpus is None:
134
- if name == "run":
135
- if kwargs.get("parallel", False):
136
- cpus = max(self._nprocessors, 1)
137
- else:
138
- cpus = 1
139
- else:
140
- cpus = 0
141
-
142
- async with self._cpus(cpus):
143
- await getattr(self, name)(*args, **kwargs)
158
+ await getattr(self, name)(*args, **kwargs)
144
159
 
145
160
  async def block_mesh(self, *, check: bool = True) -> None:
146
161
  """Run blockMesh on this case."""
@@ -156,32 +171,63 @@ class AsyncFoamCase(FoamCaseBase):
156
171
 
157
172
  async def restore_0_dir(self) -> None:
158
173
  """Restore the 0 directory from the 0.orig directory."""
159
- await aioshutil.rmtree(self.path / "0", ignore_errors=True) # type: ignore [call-arg]
160
- await aioshutil.copytree(self.path / "0.orig", self.path / "0")
174
+ for name, args, kwargs in self._restore_0_dir_cmds():
175
+ await getattr(self, name)(*args, **kwargs)
161
176
 
162
- async def copy(self, dest: Union[Path, str]) -> "AsyncFoamCase":
177
+ @awaitableasynccontextmanager
178
+ @asynccontextmanager
179
+ async def copy(
180
+ self, dst: Optional[Union["os.PathLike[str]", str]] = None
181
+ ) -> "AsyncGenerator[Self]":
163
182
  """
164
183
  Make a copy of this case.
165
184
 
166
- :param dest: The destination path.
185
+ Use as an async context manager to automatically delete the copy when done.
186
+
187
+ :param dst: The destination path. If None, copy to a temporary directory.
167
188
  """
168
- return AsyncFoamCase(await aioshutil.copytree(self.path, dest, symlinks=True))
189
+ if dst is None:
190
+ dst = Path(tempfile.mkdtemp(), self.name)
191
+ tmp = True
192
+ else:
193
+ tmp = False
194
+
195
+ for name, args, kwargs in self._copy_cmds(dst):
196
+ await getattr(self, name)(*args, **kwargs)
197
+
198
+ yield type(self)(dst)
199
+
200
+ if tmp:
201
+ assert isinstance(dst, Path)
202
+ await self._rmtree(dst.parent)
203
+ else:
204
+ await self._rmtree(dst)
169
205
 
170
- async def clone(self, dest: Union[Path, str]) -> "AsyncFoamCase":
206
+ @awaitableasynccontextmanager
207
+ @asynccontextmanager
208
+ async def clone(
209
+ self, dst: Optional[Union["os.PathLike[str]", str]] = None
210
+ ) -> "AsyncGenerator[Self]":
171
211
  """
172
212
  Clone this case (make a clean copy).
173
213
 
174
- :param dest: The destination path.
214
+ Use as an async context manager to automatically delete the clone when done.
215
+
216
+ :param dst: The destination path. If None, clone to a temporary directory.
175
217
  """
176
- if self._clean_script() is not None:
177
- copy = await self.copy(dest)
178
- await copy.clean()
179
- return copy
218
+ if dst is None:
219
+ dst = Path(tempfile.mkdtemp(), self.name)
220
+ tmp = True
221
+ else:
222
+ tmp = False
180
223
 
181
- dest = Path(dest)
224
+ for name, args, kwargs in self._clone_cmds(dst):
225
+ await getattr(self, name)(*args, **kwargs)
182
226
 
183
- await aioshutil.copytree(
184
- self.path, dest, symlinks=True, ignore=self._clone_ignore()
185
- )
227
+ yield type(self)(dst)
186
228
 
187
- return AsyncFoamCase(dest)
229
+ if tmp:
230
+ assert isinstance(dst, Path)
231
+ await self._rmtree(dst.parent)
232
+ else:
233
+ await self._rmtree(dst)
foamlib/_cases/_base.py CHANGED
@@ -3,41 +3,29 @@ import shutil
3
3
  import sys
4
4
  from pathlib import Path
5
5
  from typing import (
6
- Any,
7
6
  Optional,
8
- Tuple,
9
7
  Union,
10
8
  overload,
11
9
  )
12
10
 
13
11
  if sys.version_info >= (3, 9):
14
12
  from collections.abc import (
15
- Callable,
16
- Collection,
17
- Generator,
18
13
  Iterator,
19
- Mapping,
20
14
  Sequence,
21
15
  Set,
22
16
  )
23
17
  else:
24
18
  from typing import AbstractSet as Set
25
19
  from typing import (
26
- Callable,
27
- Collection,
28
- Generator,
29
20
  Iterator,
30
- Mapping,
31
21
  Sequence,
32
22
  )
33
23
 
34
-
35
24
  from .._files import FoamFieldFile, FoamFile
36
- from .._util import is_sequence
37
25
 
38
26
 
39
27
  class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
40
- def __init__(self, path: Union[Path, str] = Path()):
28
+ def __init__(self, path: Union["os.PathLike[str]", str] = Path()):
41
29
  self.path = Path(path).absolute()
42
30
 
43
31
  class TimeDirectory(Set[FoamFieldFile]):
@@ -49,7 +37,7 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
49
37
  :param path: The path to the time directory.
50
38
  """
51
39
 
52
- def __init__(self, path: Union[Path, str]):
40
+ def __init__(self, path: Union["os.PathLike[str]", str]):
53
41
  self.path = Path(path).absolute()
54
42
 
55
43
  @property
@@ -142,184 +130,15 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
142
130
  def __len__(self) -> int:
143
131
  return len(self._times)
144
132
 
145
- def _clean_paths(self) -> Set[Path]:
146
- has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
147
- has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
148
-
149
- paths = set()
150
-
151
- for p in self.path.iterdir():
152
- if p.is_dir():
153
- try:
154
- t = float(p.name)
155
- except ValueError:
156
- pass
157
- else:
158
- if t != 0:
159
- paths.add(p)
160
-
161
- if has_decompose_par_dict and p.name.startswith("processor"):
162
- paths.add(p)
163
-
164
- if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
165
- paths.add(self.path / "0")
166
-
167
- if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
168
- paths.add(self.path / "constant" / "polyMesh")
169
-
170
- if self._run_script() is not None:
171
- paths.update(self.path.glob("log.*"))
172
-
173
- return paths
174
-
175
133
  def __delitem__(self, key: Union[int, float, str]) -> None:
176
134
  shutil.rmtree(self[key].path)
177
135
 
178
- def _clone_ignore(
179
- self,
180
- ) -> Callable[[Union[Path, str], Collection[str]], Collection[str]]:
181
- clean_paths = self._clean_paths()
182
-
183
- def ignore(path: Union[Path, str], names: Collection[str]) -> Collection[str]:
184
- paths = {Path(path) / name for name in names}
185
- return {p.name for p in paths.intersection(clean_paths)}
186
-
187
- return ignore
188
-
189
- def _clean_script(self) -> Optional[Path]:
190
- """Return the path to the (All)clean script, or None if no clean script is found."""
191
- clean = self.path / "clean"
192
- all_clean = self.path / "Allclean"
193
-
194
- if clean.is_file():
195
- script = clean
196
- elif all_clean.is_file():
197
- script = all_clean
198
- else:
199
- return None
200
-
201
- if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
202
- return None
203
-
204
- return script if Path(sys.argv[0]).absolute() != script.absolute() else None
205
-
206
- def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
207
- """Return the path to the (All)run script, or None if no run script is found."""
208
- run = self.path / "run"
209
- run_parallel = self.path / "run-parallel"
210
- all_run = self.path / "Allrun"
211
- all_run_parallel = self.path / "Allrun-parallel"
212
-
213
- if run.is_file() or all_run.is_file():
214
- if run_parallel.is_file() or all_run_parallel.is_file():
215
- if parallel:
216
- script = (
217
- run_parallel if run_parallel.is_file() else all_run_parallel
218
- )
219
- elif parallel is False:
220
- script = run if run.is_file() else all_run
221
- else:
222
- raise ValueError(
223
- "Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
224
- )
225
- else:
226
- script = run if run.is_file() else all_run
227
- elif parallel is not False and (
228
- run_parallel.is_file() or all_run_parallel.is_file()
229
- ):
230
- script = run_parallel if run_parallel.is_file() else all_run_parallel
231
- else:
232
- return None
233
-
234
- if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
235
- return None
236
-
237
- return script
238
-
239
- def _env(self, *, shell: bool) -> Optional[Mapping[str, str]]:
240
- sip_workaround = os.environ.get(
241
- "FOAM_LD_LIBRARY_PATH", ""
242
- ) and not os.environ.get("DYLD_LIBRARY_PATH", "")
243
-
244
- if not shell or sip_workaround:
245
- env = os.environ.copy()
246
-
247
- if not shell:
248
- env["PWD"] = str(self.path)
249
-
250
- if sip_workaround:
251
- env["DYLD_LIBRARY_PATH"] = env["FOAM_LD_LIBRARY_PATH"]
252
-
253
- return env
254
- else:
255
- return None
256
-
257
- def _run_cmds(
258
- self,
259
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
260
- *,
261
- script: bool = True,
262
- parallel: Optional[bool] = None,
263
- check: bool = True,
264
- ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
265
- if cmd is not None:
266
- if parallel:
267
- cmd = self._parallel_cmd(cmd)
268
-
269
- yield ("_run", (cmd,), {"check": check})
270
- else:
271
- script_path = self._run_script(parallel=parallel) if script else None
272
-
273
- if script_path is not None:
274
- yield ("_run", ([script_path],), {"check": check})
275
-
276
- else:
277
- if not self and (self.path / "0.orig").is_dir():
278
- yield ("restore_0_dir", (), {})
279
-
280
- if (self.path / "system" / "blockMeshDict").is_file():
281
- yield ("block_mesh", (), {"check": check})
282
-
283
- if parallel is None:
284
- parallel = (
285
- self._nprocessors > 0
286
- or (self.path / "system" / "decomposeParDict").is_file()
287
- )
288
-
289
- if parallel:
290
- if (
291
- self._nprocessors == 0
292
- and (self.path / "system" / "decomposeParDict").is_file()
293
- ):
294
- yield ("decompose_par", (), {"check": check})
295
-
296
- yield (
297
- "run",
298
- ([self.application],),
299
- {"parallel": parallel, "check": check},
300
- )
301
-
302
- def _parallel_cmd(
303
- self, cmd: Union[Sequence[Union[str, Path]], str, Path]
304
- ) -> Union[Sequence[Union[str, Path]], str]:
305
- if not is_sequence(cmd):
306
- return f"mpiexec -np {self._nprocessors} {cmd} -parallel"
307
- else:
308
- return [
309
- "mpiexec",
310
- "-np",
311
- str(self._nprocessors),
312
- cmd[0],
313
- "-parallel",
314
- *cmd[1:],
315
- ]
316
-
317
136
  @property
318
137
  def name(self) -> str:
319
138
  """The name of the case."""
320
139
  return self.path.name
321
140
 
322
- def file(self, path: Union[Path, str]) -> FoamFile:
141
+ def file(self, path: Union["os.PathLike[str]", str]) -> FoamFile:
323
142
  """Return a FoamFile object for the given path in the case."""
324
143
  return FoamFile(self.path / path)
325
144