foamlib 0.8.5__tar.gz → 0.8.7__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.
Files changed (52) hide show
  1. {foamlib-0.8.5 → foamlib-0.8.7}/.github/workflows/ci.yml +2 -2
  2. {foamlib-0.8.5 → foamlib-0.8.7}/.github/workflows/docker.yml +1 -1
  3. {foamlib-0.8.5 → foamlib-0.8.7}/Dockerfile +1 -1
  4. {foamlib-0.8.5 → foamlib-0.8.7}/PKG-INFO +2 -1
  5. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/__init__.py +1 -1
  6. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/_async.py +3 -0
  7. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/_run.py +55 -26
  8. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/_slurm.py +3 -3
  9. foamlib-0.8.7/foamlib/_cases/_subprocess.py +188 -0
  10. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/_sync.py +3 -0
  11. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/_util.py +41 -1
  12. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_files/_files.py +5 -5
  13. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_files/_parsing.py +27 -7
  14. {foamlib-0.8.5 → foamlib-0.8.7}/pyproject.toml +1 -0
  15. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_files/test_files.py +2 -2
  16. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_files/test_parsing.py +4 -1
  17. foamlib-0.8.5/foamlib/_cases/_subprocess.py +0 -152
  18. {foamlib-0.8.5 → foamlib-0.8.7}/.devcontainer.json +0 -0
  19. {foamlib-0.8.5 → foamlib-0.8.7}/.dockerignore +0 -0
  20. {foamlib-0.8.5 → foamlib-0.8.7}/.git-blame-ignore-revs +0 -0
  21. {foamlib-0.8.5 → foamlib-0.8.7}/.github/dependabot.yml +0 -0
  22. {foamlib-0.8.5 → foamlib-0.8.7}/.github/workflows/dockerhub-description.yml +0 -0
  23. {foamlib-0.8.5 → foamlib-0.8.7}/.github/workflows/pypi-publish.yml +0 -0
  24. {foamlib-0.8.5 → foamlib-0.8.7}/.gitignore +0 -0
  25. {foamlib-0.8.5 → foamlib-0.8.7}/.readthedocs.yaml +0 -0
  26. {foamlib-0.8.5 → foamlib-0.8.7}/LICENSE.txt +0 -0
  27. {foamlib-0.8.5 → foamlib-0.8.7}/README.md +0 -0
  28. {foamlib-0.8.5 → foamlib-0.8.7}/benchmark.png +0 -0
  29. {foamlib-0.8.5 → foamlib-0.8.7}/docs/Makefile +0 -0
  30. {foamlib-0.8.5 → foamlib-0.8.7}/docs/cases.rst +0 -0
  31. {foamlib-0.8.5 → foamlib-0.8.7}/docs/conf.py +0 -0
  32. {foamlib-0.8.5 → foamlib-0.8.7}/docs/files.rst +0 -0
  33. {foamlib-0.8.5 → foamlib-0.8.7}/docs/index.rst +0 -0
  34. {foamlib-0.8.5 → foamlib-0.8.7}/docs/make.bat +0 -0
  35. {foamlib-0.8.5 → foamlib-0.8.7}/docs/ruff.toml +0 -0
  36. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/__init__.py +0 -0
  37. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_cases/_base.py +0 -0
  38. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_files/__init__.py +0 -0
  39. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_files/_io.py +0 -0
  40. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_files/_serialization.py +0 -0
  41. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/_files/_types.py +0 -0
  42. {foamlib-0.8.5 → foamlib-0.8.7}/foamlib/py.typed +0 -0
  43. {foamlib-0.8.5 → foamlib-0.8.7}/logo.png +0 -0
  44. {foamlib-0.8.5 → foamlib-0.8.7}/tests/__init__.py +0 -0
  45. {foamlib-0.8.5 → foamlib-0.8.7}/tests/ruff.toml +0 -0
  46. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_cases/__init__.py +0 -0
  47. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_cases/test_cavity.py +0 -0
  48. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_cases/test_cavity_async.py +0 -0
  49. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_cases/test_flange.py +0 -0
  50. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_cases/test_flange_async.py +0 -0
  51. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_files/__init__.py +0 -0
  52. {foamlib-0.8.5 → foamlib-0.8.7}/tests/test_files/test_dumps.py +0 -0
@@ -49,11 +49,11 @@ jobs:
49
49
  options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
50
50
  strategy:
51
51
  matrix:
52
- openfoam-version: [2406, 2006, 12, 9]
52
+ openfoam-version: [2412, 2006, 12, 9]
53
53
  python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
54
54
  slurm: [false]
55
55
  include:
56
- - openfoam-version: 2406
56
+ - openfoam-version: 2412
57
57
  python-version: '3.13'
58
58
  slurm: true
59
59
  - openfoam-version: 12
@@ -80,7 +80,7 @@ jobs:
80
80
  packages: write
81
81
  strategy:
82
82
  matrix:
83
- openfoam-version: [2406, 2312, 12, 11]
83
+ openfoam-version: [2412, 2406, 12, 11]
84
84
  fail-fast: false
85
85
  steps:
86
86
  - name: Set up QEMU
@@ -1,7 +1,7 @@
1
1
 
2
2
  ARG BASE=python:3.12-slim
3
3
 
4
- ARG OPENFOAM_VERSION=2406
4
+ ARG OPENFOAM_VERSION=2412
5
5
  FROM microfluidica/openfoam:${OPENFOAM_VERSION} AS openfoam
6
6
 
7
7
  ARG VIRTUAL_ENV=/opt/venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foamlib
3
- Version: 0.8.5
3
+ Version: 0.8.7
4
4
  Summary: A Python interface for interacting with OpenFOAM
5
5
  Project-URL: Homepage, https://github.com/gerlero/foamlib
6
6
  Project-URL: Repository, https://github.com/gerlero/foamlib
@@ -29,6 +29,7 @@ Requires-Dist: aioshutil<2,>=1
29
29
  Requires-Dist: numpy<3,>=1
30
30
  Requires-Dist: numpy<3,>=1.25.0; python_version >= '3.10'
31
31
  Requires-Dist: pyparsing<4,>=3.1.2
32
+ Requires-Dist: rich<14,>=13
32
33
  Requires-Dist: typing-extensions<5,>=4; python_version < '3.11'
33
34
  Provides-Extra: dev
34
35
  Requires-Dist: mypy<2,>=1; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.8.5"
3
+ __version__ = "0.8.7"
4
4
 
5
5
  from ._cases import (
6
6
  AsyncFoamCase,
@@ -99,6 +99,9 @@ class AsyncFoamCase(FoamCaseRunBase):
99
99
  cpus: int,
100
100
  **kwargs: Any,
101
101
  ) -> None:
102
+ if isinstance(cmd, str):
103
+ cmd = [*AsyncFoamCase._SHELL, cmd]
104
+
102
105
  async with AsyncFoamCase._cpus(cpus):
103
106
  await run_async(cmd, **kwargs)
104
107
 
@@ -10,13 +10,16 @@ from contextlib import contextmanager
10
10
  from pathlib import Path
11
11
  from typing import IO, TYPE_CHECKING, Any
12
12
 
13
+ from rich.progress import Progress
14
+
15
+ from ._util import SingletonContextManager
16
+
13
17
  if sys.version_info >= (3, 9):
14
18
  from collections.abc import (
15
19
  Callable,
16
20
  Collection,
17
21
  Coroutine,
18
22
  Generator,
19
- Mapping,
20
23
  Sequence,
21
24
  )
22
25
  from collections.abc import Set as AbstractSet
@@ -27,7 +30,6 @@ else:
27
30
  Collection,
28
31
  Coroutine,
29
32
  Generator,
30
- Mapping,
31
33
  Sequence,
32
34
  )
33
35
 
@@ -68,6 +70,10 @@ class FoamCaseRunBase(FoamCaseBase):
68
70
 
69
71
  return ret
70
72
 
73
+ _SHELL = ("bash", "-c")
74
+
75
+ __progress = SingletonContextManager(Progress)
76
+
71
77
  def __delitem__(self, key: int | float | str) -> None:
72
78
  shutil.rmtree(self[key].path)
73
79
 
@@ -255,38 +261,60 @@ class FoamCaseRunBase(FoamCaseBase):
255
261
 
256
262
  return script
257
263
 
258
- def __env(self, *, shell: bool) -> Mapping[str, str] | None:
259
- sip_workaround = os.environ.get(
260
- "FOAM_LD_LIBRARY_PATH", ""
261
- ) and not os.environ.get("DYLD_LIBRARY_PATH", "")
262
-
263
- if not shell or sip_workaround:
264
- env = os.environ.copy()
265
-
266
- if not shell:
267
- env["PWD"] = str(self.path)
268
-
269
- if sip_workaround:
270
- env["DYLD_LIBRARY_PATH"] = env["FOAM_LD_LIBRARY_PATH"]
271
-
272
- return env
273
- return None
274
-
275
264
  @contextmanager
276
265
  def __output(
277
266
  self, cmd: Sequence[str | os.PathLike[str]] | str, *, log: bool
278
- ) -> Generator[tuple[int | IO[bytes], int | IO[bytes]], None, None]:
267
+ ) -> Generator[tuple[int | IO[str], int | IO[str]], None, None]:
279
268
  if log:
280
269
  if isinstance(cmd, str):
281
270
  name = shlex.split(cmd)[0]
282
271
  else:
283
272
  name = Path(cmd[0]).name if isinstance(cmd[0], os.PathLike) else cmd[0]
284
273
 
285
- with (self.path / f"log.{name}").open("ab") as stdout:
274
+ with (self.path / f"log.{name}").open("a") as stdout:
286
275
  yield stdout, STDOUT
287
276
  else:
288
277
  yield DEVNULL, DEVNULL
289
278
 
279
+ @contextmanager
280
+ def __process_stdout(
281
+ self, cmd: Sequence[str | os.PathLike[str]] | str
282
+ ) -> Generator[Callable[[str], None], None, None]:
283
+ if isinstance(cmd, str):
284
+ name = shlex.split(cmd)[0]
285
+ else:
286
+ name = Path(cmd[0]).name if isinstance(cmd[0], os.PathLike) else cmd[0]
287
+
288
+ try:
289
+ with self.control_dict as control_dict:
290
+ if control_dict["stopAt"] == "endTime":
291
+ control_dict_end_time = control_dict["endTime"]
292
+ if isinstance(control_dict_end_time, (int, float)):
293
+ end_time = control_dict_end_time
294
+ else:
295
+ end_time = None
296
+ else:
297
+ end_time = None
298
+ except (KeyError, FileNotFoundError):
299
+ end_time = None
300
+
301
+ with self.__progress as progress:
302
+ task = progress.add_task(f"({self.name}) Running {name}...", total=None)
303
+
304
+ def process_stdout(line: str) -> None:
305
+ if line.startswith("Time = "):
306
+ try:
307
+ time = float(line.split()[2])
308
+ except ValueError:
309
+ progress.update(task)
310
+ else:
311
+ progress.update(task, completed=time, total=end_time)
312
+ else:
313
+ progress.update(task)
314
+
315
+ yield process_stdout
316
+ progress.update(task, completed=1, total=1)
317
+
290
318
  def __mkrundir(self) -> Path:
291
319
  d = Path(os.environ["FOAM_RUN"], "foamlib")
292
320
  d.mkdir(parents=True, exist_ok=True)
@@ -379,15 +407,16 @@ class FoamCaseRunBase(FoamCaseBase):
379
407
  if cpus is None:
380
408
  cpus = 1
381
409
 
382
- with self.__output(cmd, log=log) as (stdout, stderr):
410
+ with self.__output(cmd, log=log) as (stdout, stderr), self.__process_stdout(
411
+ cmd
412
+ ) as process_stdout:
383
413
  if parallel:
384
414
  if isinstance(cmd, str):
385
415
  cmd = [
386
416
  "mpiexec",
387
417
  "-n",
388
418
  str(cpus),
389
- "/bin/sh",
390
- "-c",
419
+ *FoamCaseRunBase._SHELL,
391
420
  f"{cmd} -parallel",
392
421
  ]
393
422
  else:
@@ -396,11 +425,11 @@ class FoamCaseRunBase(FoamCaseBase):
396
425
  yield self._run(
397
426
  cmd,
398
427
  cpus=cpus,
428
+ case=self,
399
429
  check=check,
400
- cwd=self.path,
401
- env=self.__env(shell=isinstance(cmd, str)),
402
430
  stdout=stdout,
403
431
  stderr=stderr,
432
+ process_stdout=process_stdout,
404
433
  **kwargs,
405
434
  )
406
435
 
@@ -31,10 +31,10 @@ class AsyncSlurmFoamCase(AsyncFoamCase):
31
31
  await AsyncFoamCase._run(cmd, cpus=cpus, **kwargs)
32
32
  return
33
33
 
34
- if cpus >= 1:
35
- if isinstance(cmd, str):
36
- cmd = ["/bin/sh", "-c", cmd]
34
+ if isinstance(cmd, str):
35
+ cmd = [*AsyncSlurmFoamCase._SHELL, cmd]
37
36
 
37
+ if cpus >= 1:
38
38
  if cpus == 1:
39
39
  cmd = ["srun", *cmd]
40
40
 
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import selectors
6
+ import subprocess
7
+ import sys
8
+ from io import StringIO
9
+ from pathlib import Path
10
+ from typing import IO
11
+
12
+ if sys.version_info >= (3, 9):
13
+ from collections.abc import Callable, Mapping, Sequence
14
+ else:
15
+ from typing import Callable, Mapping, Sequence
16
+
17
+ CompletedProcess = subprocess.CompletedProcess
18
+
19
+
20
+ class CalledProcessError(subprocess.CalledProcessError):
21
+ def __str__(self) -> str:
22
+ if self.stderr:
23
+ if isinstance(self.stderr, bytes):
24
+ return super().__str__() + "\n" + self.stderr.decode()
25
+ if isinstance(self.stderr, str):
26
+ return super().__str__() + "\n" + self.stderr
27
+ return super().__str__()
28
+
29
+
30
+ DEVNULL = subprocess.DEVNULL
31
+ PIPE = subprocess.PIPE
32
+ STDOUT = subprocess.STDOUT
33
+
34
+
35
+ def _env(case: os.PathLike[str]) -> Mapping[str, str]:
36
+ env = os.environ.copy()
37
+
38
+ env["PWD"] = str(Path(case))
39
+
40
+ if os.environ.get("FOAM_LD_LIBRARY_PATH", "") and not os.environ.get(
41
+ "DYLD_LIBRARY_PATH", ""
42
+ ):
43
+ env["DYLD_LIBRARY_PATH"] = env["FOAM_LD_LIBRARY_PATH"]
44
+
45
+ return env
46
+
47
+
48
+ def run_sync(
49
+ cmd: Sequence[str | os.PathLike[str]],
50
+ *,
51
+ case: os.PathLike[str],
52
+ check: bool = True,
53
+ stdout: int | IO[str] = DEVNULL,
54
+ stderr: int | IO[str] = STDOUT,
55
+ process_stdout: Callable[[str], None] = lambda _: None,
56
+ ) -> CompletedProcess[str]:
57
+ if sys.version_info < (3, 8):
58
+ cmd = [str(arg) for arg in cmd]
59
+
60
+ with subprocess.Popen(
61
+ cmd,
62
+ cwd=case,
63
+ env=_env(case),
64
+ stdout=PIPE,
65
+ stderr=PIPE,
66
+ text=True,
67
+ ) as proc:
68
+ assert proc.stdout is not None
69
+ assert proc.stderr is not None
70
+
71
+ output = StringIO() if stdout is PIPE else None
72
+ error = StringIO()
73
+
74
+ if stderr is STDOUT:
75
+ stderr = stdout
76
+
77
+ with selectors.DefaultSelector() as selector:
78
+ selector.register(proc.stdout, selectors.EVENT_READ)
79
+ selector.register(proc.stderr, selectors.EVENT_READ)
80
+ open_streams = {proc.stdout, proc.stderr}
81
+ while open_streams:
82
+ for key, _ in selector.select():
83
+ assert key.fileobj in open_streams
84
+ line = key.fileobj.readline() # type: ignore [union-attr]
85
+ if not line:
86
+ selector.unregister(key.fileobj)
87
+ open_streams.remove(key.fileobj) # type: ignore [arg-type]
88
+ elif key.fileobj is proc.stdout:
89
+ process_stdout(line)
90
+ if output is not None:
91
+ output.write(line)
92
+ if stdout not in (DEVNULL, PIPE):
93
+ assert not isinstance(stdout, int)
94
+ stdout.write(line)
95
+ else:
96
+ assert key.fileobj is proc.stderr
97
+ error.write(line)
98
+ if stderr not in (DEVNULL, PIPE):
99
+ assert not isinstance(stderr, int)
100
+ stderr.write(line)
101
+
102
+ assert proc.returncode is not None
103
+
104
+ if check and proc.returncode != 0:
105
+ raise CalledProcessError(
106
+ returncode=proc.returncode,
107
+ cmd=cmd,
108
+ output=output.getvalue() if output is not None else None,
109
+ stderr=error.getvalue(),
110
+ )
111
+
112
+ return CompletedProcess(
113
+ cmd,
114
+ returncode=proc.returncode,
115
+ stdout=output.getvalue() if output is not None else None,
116
+ stderr=error.getvalue(),
117
+ )
118
+
119
+
120
+ async def run_async(
121
+ cmd: Sequence[str | os.PathLike[str]],
122
+ *,
123
+ case: os.PathLike[str],
124
+ check: bool = True,
125
+ stdout: int | IO[str] = DEVNULL,
126
+ stderr: int | IO[str] = STDOUT,
127
+ process_stdout: Callable[[str], None] = lambda _: None,
128
+ ) -> CompletedProcess[str]:
129
+ if sys.version_info < (3, 8):
130
+ cmd = [str(arg) for arg in cmd]
131
+
132
+ proc = await asyncio.create_subprocess_exec(
133
+ *cmd,
134
+ cwd=case,
135
+ env=_env(case),
136
+ stdout=PIPE,
137
+ stderr=PIPE,
138
+ )
139
+
140
+ if stderr is STDOUT:
141
+ stderr = stdout
142
+
143
+ output = StringIO() if stdout is PIPE else None
144
+ error = StringIO()
145
+
146
+ async def tee_stdout() -> None:
147
+ while True:
148
+ assert proc.stdout is not None
149
+ line = (await proc.stdout.readline()).decode()
150
+ if not line:
151
+ break
152
+ process_stdout(line)
153
+ if output is not None:
154
+ output.write(line)
155
+ if stdout not in (DEVNULL, PIPE):
156
+ assert not isinstance(stdout, int)
157
+ stdout.write(line)
158
+
159
+ async def tee_stderr() -> None:
160
+ while True:
161
+ assert proc.stderr is not None
162
+ line = (await proc.stderr.readline()).decode()
163
+ if not line:
164
+ break
165
+ error.write(line)
166
+ if stderr not in (DEVNULL, PIPE):
167
+ assert not isinstance(stderr, int)
168
+ stderr.write(line)
169
+
170
+ await asyncio.gather(tee_stdout(), tee_stderr())
171
+
172
+ await proc.wait()
173
+ assert proc.returncode is not None
174
+
175
+ if check and proc.returncode != 0:
176
+ raise CalledProcessError(
177
+ returncode=proc.returncode,
178
+ cmd=cmd,
179
+ output=output.getvalue() if output is not None else None,
180
+ stderr=error.getvalue(),
181
+ )
182
+
183
+ return CompletedProcess(
184
+ cmd,
185
+ returncode=proc.returncode,
186
+ stdout=output.getvalue() if output is not None else None,
187
+ stderr=error.getvalue(),
188
+ )
@@ -58,6 +58,9 @@ class FoamCase(FoamCaseRunBase):
58
58
  cpus: int,
59
59
  **kwargs: Any,
60
60
  ) -> None:
61
+ if isinstance(cmd, str):
62
+ cmd = [*FoamCase._SHELL, cmd]
63
+
61
64
  run_sync(cmd, **kwargs)
62
65
 
63
66
  @staticmethod
@@ -2,7 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import functools
4
4
  import sys
5
- from typing import TYPE_CHECKING, Any, AsyncContextManager, Callable, Generic, TypeVar
5
+ import threading
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ AsyncContextManager,
10
+ Callable,
11
+ ContextManager,
12
+ Generic,
13
+ TypeVar,
14
+ cast,
15
+ )
6
16
 
7
17
  if TYPE_CHECKING:
8
18
  from types import TracebackType
@@ -54,3 +64,33 @@ def awaitableasynccontextmanager(
54
64
  return _AwaitableAsyncContextManager(cm(*args, **kwargs))
55
65
 
56
66
  return f
67
+
68
+
69
+ class SingletonContextManager(Generic[R]):
70
+ def __init__(self, factory: Callable[[], ContextManager[R]]) -> None:
71
+ self._factory = factory
72
+ self._users = 0
73
+ self._cm: ContextManager[R] | None = None
74
+ self._ret: R | None = None
75
+ self._lock = threading.Lock()
76
+
77
+ def __enter__(self) -> R:
78
+ with self._lock:
79
+ if self._users == 0:
80
+ self._cm = self._factory()
81
+ self._ret = self._cm.__enter__()
82
+ self._users += 1
83
+ return cast("R", self._ret)
84
+
85
+ def __exit__(
86
+ self,
87
+ exc_type: type[BaseException] | None,
88
+ exc_val: BaseException | None,
89
+ exc_tb: TracebackType | None,
90
+ ) -> bool | None:
91
+ with self._lock:
92
+ self._users -= 1
93
+ if self._users == 0:
94
+ assert self._cm is not None
95
+ return self._cm.__exit__(exc_type, exc_val, exc_tb)
96
+ return False
@@ -100,9 +100,9 @@ class FoamFile(
100
100
  assert isinstance(ret, dict)
101
101
  v = ret[k]
102
102
  assert isinstance(v, dict)
103
- ret = cast(File, v)
103
+ ret = cast("File", v)
104
104
 
105
- return cast(Dict_, ret)
105
+ return cast("Dict_", ret)
106
106
 
107
107
  @property
108
108
  def version(self) -> float:
@@ -127,7 +127,7 @@ class FoamFile(
127
127
  if ret not in ("ascii", "binary"):
128
128
  msg = "format is not 'ascii' or 'binary'"
129
129
  raise ValueError(msg)
130
- return cast(Literal["ascii", "binary"], ret)
130
+ return cast("Literal['ascii', 'binary']", ret)
131
131
 
132
132
  @format.setter
133
133
  def format(self, value: Literal["ascii", "binary"]) -> None:
@@ -412,7 +412,7 @@ class FoamFieldFile(FoamFile):
412
412
  ) -> Field:
413
413
  """Alias of `self["value"]`."""
414
414
  return cast(
415
- Field,
415
+ "Field",
416
416
  self["value"],
417
417
  )
418
418
 
@@ -461,7 +461,7 @@ class FoamFieldFile(FoamFile):
461
461
  self,
462
462
  ) -> Field:
463
463
  """Alias of `self["internalField"]`."""
464
- return cast(Field, self["internalField"])
464
+ return cast("Field", self["internalField"])
465
465
 
466
466
  @internal_field.setter
467
467
  def internal_field(
@@ -16,6 +16,7 @@ else:
16
16
 
17
17
  import numpy as np
18
18
  from pyparsing import (
19
+ CaselessKeyword,
19
20
  Combine,
20
21
  Dict,
21
22
  Forward,
@@ -157,6 +158,7 @@ def _dict_of(
157
158
  data: ParserElement,
158
159
  *,
159
160
  directive: ParserElement | None = None,
161
+ data_entry: ParserElement | None = None,
160
162
  located: bool = False,
161
163
  ) -> ParserElement:
162
164
  dict_ = Forward()
@@ -164,7 +166,8 @@ def _dict_of(
164
166
  keyword_entry = keyword + (dict_ | (data + Literal(";").suppress()))
165
167
 
166
168
  if directive is not None:
167
- keyword_entry |= directive + data + LineEnd().suppress() # type: ignore [no-untyped-call]
169
+ assert data_entry is not None
170
+ keyword_entry |= directive + data_entry + LineEnd().suppress()
168
171
 
169
172
  if located:
170
173
  keyword_entry = Located(keyword_entry)
@@ -183,15 +186,19 @@ def _keyword_entry_of(
183
186
  data: ParserElement,
184
187
  *,
185
188
  directive: ParserElement | None = None,
189
+ data_entry: ParserElement | None = None,
186
190
  located: bool = False,
187
191
  ) -> ParserElement:
188
192
  keyword_entry = keyword + (
189
- _dict_of(keyword, data, directive=directive, located=located)
193
+ _dict_of(
194
+ keyword, data, directive=directive, data_entry=data_entry, located=located
195
+ )
190
196
  | (data + Literal(";").suppress())
191
197
  )
192
198
 
193
199
  if directive is not None:
194
- keyword_entry |= directive + data + LineEnd().suppress() # type: ignore [no-untyped-call]
200
+ assert data_entry is not None
201
+ keyword_entry |= directive + data_entry + LineEnd().suppress()
195
202
 
196
203
  if located:
197
204
  keyword_entry = Located(keyword_entry)
@@ -261,7 +268,16 @@ _DICT = _dict_of(_TOKEN, _DATA)
261
268
  _DATA_ENTRY = Forward()
262
269
  _LIST_ENTRY = _DICT | _KEYWORD_ENTRY | _DATA_ENTRY
263
270
  _LIST = _list_of(_LIST_ENTRY)
264
- _NUMBER = common.signed_integer ^ common.ieee_float
271
+ _NUMBER = (
272
+ common.number
273
+ | CaselessKeyword("nan").set_parse_action(lambda: np.nan)
274
+ | (CaselessKeyword("inf") | CaselessKeyword("infinity")).set_parse_action(
275
+ lambda: np.inf
276
+ )
277
+ | (CaselessKeyword("-inf") | CaselessKeyword("-infinity")).set_parse_action(
278
+ lambda: -np.inf
279
+ )
280
+ )
265
281
  _DATA_ENTRY <<= _FIELD | _LIST | _DIMENSIONED | _DIMENSIONS | _NUMBER | _SWITCH | _TOKEN
266
282
 
267
283
  _DATA <<= (
@@ -275,12 +291,16 @@ _DATA <<= (
275
291
  def parse_data(s: str) -> Data:
276
292
  if not s.strip():
277
293
  return ""
278
- return cast(Data, _DATA.parse_string(s, parse_all=True)[0])
294
+ return cast("Data", _DATA.parse_string(s, parse_all=True)[0])
279
295
 
280
296
 
281
297
  _LOCATED_DICTIONARY = Group(
282
298
  _keyword_entry_of(
283
- _TOKEN, Opt(_DATA, default=""), directive=_DIRECTIVE, located=True
299
+ _TOKEN,
300
+ Opt(_DATA, default=""),
301
+ directive=_DIRECTIVE,
302
+ data_entry=_DATA_ENTRY,
303
+ located=True,
284
304
  )
285
305
  )[...]
286
306
  _LOCATED_DATA = Group(Located(_DATA.copy().add_parse_action(lambda tks: ["", tks[0]])))
@@ -421,7 +441,7 @@ class Parsed(Mapping[Tuple[str, ...], Union[Data, EllipsisType]]):
421
441
  for k in keywords[:-1]:
422
442
  v = r[k]
423
443
  assert isinstance(v, dict)
424
- r = cast(File, v)
444
+ r = cast("File", v)
425
445
 
426
446
  assert isinstance(r, dict)
427
447
  if keywords:
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "numpy>=1,<3",
35
35
  "pyparsing>=3.1.2,<4",
36
36
  "typing-extensions>=4,<5; python_version<'3.11'",
37
+ "rich>=13,<14",
37
38
  ]
38
39
 
39
40
  dynamic = ["version"]
@@ -163,7 +163,7 @@ def test_internal_field(cavity: FoamCase) -> None:
163
163
  p_arr = np.zeros(size)
164
164
  U_arr = np.zeros((size, 3))
165
165
 
166
- cavity[0]["p"].internal_field = p_arr # type: ignore [assignment]
166
+ cavity[0]["p"].internal_field = p_arr
167
167
  cavity[0]["U"].internal_field = U_arr # type: ignore [assignment]
168
168
 
169
169
  assert cavity[0]["p"].internal_field == pytest.approx(p_arr)
@@ -174,7 +174,7 @@ def test_internal_field(cavity: FoamCase) -> None:
174
174
  p_arr = np.arange(size) * 1e-6 # type: ignore [assignment]
175
175
  U_arr = np.full((size, 3), [-1e-6, 1e-6, 0]) * np.arange(size)[:, np.newaxis]
176
176
 
177
- cavity[0]["p"].internal_field = p_arr # type: ignore [assignment]
177
+ cavity[0]["p"].internal_field = p_arr
178
178
  cavity[0]["U"].internal_field = U_arr # type: ignore [assignment]
179
179
 
180
180
  assert cavity[0]["p"].internal_field == pytest.approx(p_arr)
@@ -10,6 +10,7 @@ def test_parse_value() -> None:
10
10
  assert Parsed(b"yes")[()] is True
11
11
  assert Parsed(b"no")[()] is False
12
12
  assert Parsed(b"word")[()] == "word"
13
+ assert Parsed(b"inference")[()] == "inference"
13
14
  assert Parsed(b"word word")[()] == ("word", "word")
14
15
  assert Parsed(b'"a string"')[()] == '"a string"'
15
16
  assert Parsed(b"uniform 1")[()] == 1
@@ -104,6 +105,8 @@ def test_parse_value() -> None:
104
105
  def test_parse_directive() -> None:
105
106
  assert Parsed(b'#include "filename"')[("#include",)] == '"filename"'
106
107
  assert (
107
- Parsed(b"functions\n{\n#includeFunc funcName\n}")[("functions", "#includeFunc")]
108
+ Parsed(b"functions\n{\n#includeFunc funcName\nsubdict{}}")[
109
+ ("functions", "#includeFunc")
110
+ ]
108
111
  == "funcName"
109
112
  )
@@ -1,152 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import subprocess
5
- import sys
6
- from io import BytesIO
7
- from typing import IO, TYPE_CHECKING
8
-
9
- if TYPE_CHECKING:
10
- import os
11
-
12
- if sys.version_info >= (3, 9):
13
- from collections.abc import Mapping, Sequence
14
- else:
15
- from typing import Mapping, Sequence
16
-
17
- CompletedProcess = subprocess.CompletedProcess
18
-
19
-
20
- class CalledProcessError(subprocess.CalledProcessError):
21
- def __str__(self) -> str:
22
- if self.stderr:
23
- if isinstance(self.stderr, bytes):
24
- return super().__str__() + "\n" + self.stderr.decode()
25
- if isinstance(self.stderr, str):
26
- return super().__str__() + "\n" + self.stderr
27
- return super().__str__()
28
-
29
-
30
- DEVNULL = subprocess.DEVNULL
31
- PIPE = subprocess.PIPE
32
- STDOUT = subprocess.STDOUT
33
-
34
-
35
- def run_sync(
36
- cmd: Sequence[str | os.PathLike[str]] | str,
37
- *,
38
- check: bool = True,
39
- cwd: os.PathLike[str] | None = None,
40
- env: Mapping[str, str] | None = None,
41
- stdout: int | IO[bytes] | None = None,
42
- stderr: int | IO[bytes] | None = None,
43
- ) -> CompletedProcess[bytes]:
44
- if not isinstance(cmd, str) and sys.version_info < (3, 8):
45
- cmd = [str(arg) for arg in cmd]
46
-
47
- proc = subprocess.Popen(
48
- cmd,
49
- cwd=cwd,
50
- env=env,
51
- stdout=stdout,
52
- stderr=PIPE,
53
- shell=isinstance(cmd, str),
54
- )
55
-
56
- if stderr == STDOUT:
57
- stderr = stdout
58
- if stderr not in (PIPE, DEVNULL):
59
- stderr_copy = BytesIO()
60
-
61
- assert not isinstance(stderr, int)
62
- if stderr is None:
63
- stderr = sys.stderr.buffer
64
-
65
- assert proc.stderr is not None
66
- for line in proc.stderr:
67
- stderr.write(line)
68
- stderr_copy.write(line)
69
-
70
- output, _ = proc.communicate()
71
- assert not _
72
- error = stderr_copy.getvalue()
73
- else:
74
- output, error = proc.communicate()
75
-
76
- assert proc.returncode is not None
77
-
78
- if check and proc.returncode != 0:
79
- raise CalledProcessError(
80
- returncode=proc.returncode,
81
- cmd=cmd,
82
- output=output,
83
- stderr=error,
84
- )
85
-
86
- return CompletedProcess(
87
- cmd, returncode=proc.returncode, stdout=output, stderr=error
88
- )
89
-
90
-
91
- async def run_async(
92
- cmd: Sequence[str | os.PathLike[str]] | str,
93
- *,
94
- check: bool = True,
95
- cwd: os.PathLike[str] | None = None,
96
- env: Mapping[str, str] | None = None,
97
- stdout: int | IO[bytes] | None = None,
98
- stderr: int | IO[bytes] | None = None,
99
- ) -> CompletedProcess[bytes]:
100
- if isinstance(cmd, str):
101
- proc = await asyncio.create_subprocess_shell(
102
- cmd,
103
- cwd=cwd,
104
- env=env,
105
- stdout=stdout,
106
- stderr=PIPE,
107
- )
108
-
109
- else:
110
- if sys.version_info < (3, 8):
111
- cmd = [str(arg) for arg in cmd]
112
- proc = await asyncio.create_subprocess_exec(
113
- *cmd,
114
- cwd=cwd,
115
- env=env,
116
- stdout=stdout,
117
- stderr=PIPE,
118
- )
119
-
120
- if stderr == STDOUT:
121
- stderr = stdout
122
- if stderr not in (PIPE, DEVNULL):
123
- stderr_copy = BytesIO()
124
-
125
- assert not isinstance(stderr, int)
126
- if stderr is None:
127
- stderr = sys.stderr.buffer
128
-
129
- assert proc.stderr is not None
130
- async for line in proc.stderr:
131
- stderr.write(line)
132
- stderr_copy.write(line)
133
-
134
- output, _ = await proc.communicate()
135
- assert not _
136
- error = stderr_copy.getvalue()
137
- else:
138
- output, error = await proc.communicate()
139
-
140
- assert proc.returncode is not None
141
-
142
- if check and proc.returncode != 0:
143
- raise CalledProcessError(
144
- returncode=proc.returncode,
145
- cmd=cmd,
146
- output=output,
147
- stderr=error,
148
- )
149
-
150
- return CompletedProcess(
151
- cmd, returncode=proc.returncode, stdout=output, stderr=error
152
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes