foamlib 0.8.6__tar.gz → 0.8.8__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.
- {foamlib-0.8.6 → foamlib-0.8.8}/PKG-INFO +5 -4
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/__init__.py +1 -1
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_async.py +3 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_run.py +57 -29
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_slurm.py +3 -3
- foamlib-0.8.8/foamlib/_cases/_subprocess.py +188 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_sync.py +3 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_util.py +41 -1
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_parsing.py +10 -6
- {foamlib-0.8.6 → foamlib-0.8.8}/pyproject.toml +2 -1
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/test_parsing.py +1 -0
- foamlib-0.8.6/foamlib/_cases/_subprocess.py +0 -152
- {foamlib-0.8.6 → foamlib-0.8.8}/.devcontainer.json +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.dockerignore +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.git-blame-ignore-revs +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.github/dependabot.yml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/ci.yml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/docker.yml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/dockerhub-description.yml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/pypi-publish.yml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.gitignore +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/.readthedocs.yaml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/Dockerfile +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/LICENSE.txt +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/README.md +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/benchmark.png +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/Makefile +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/cases.rst +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/conf.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/files.rst +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/index.rst +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/make.bat +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/docs/ruff.toml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/__init__.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_base.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/__init__.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_files.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_io.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_serialization.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_types.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/py.typed +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/logo.png +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/__init__.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/ruff.toml +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/__init__.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_cavity.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_cavity_async.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_flange.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_flange_async.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/__init__.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/test_dumps.py +0 -0
- {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/test_files.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: foamlib
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.8
|
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,10 +29,11 @@ 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<15,>=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'
|
35
|
-
Requires-Dist: pytest-asyncio<0.
|
36
|
+
Requires-Dist: pytest-asyncio<0.27,>=0.21; extra == 'dev'
|
36
37
|
Requires-Dist: pytest-cov; extra == 'dev'
|
37
38
|
Requires-Dist: pytest<9,>=7; extra == 'dev'
|
38
39
|
Requires-Dist: ruff; extra == 'dev'
|
@@ -44,12 +45,12 @@ Requires-Dist: sphinx<9,>=5; extra == 'docs'
|
|
44
45
|
Provides-Extra: lint
|
45
46
|
Requires-Dist: ruff; extra == 'lint'
|
46
47
|
Provides-Extra: test
|
47
|
-
Requires-Dist: pytest-asyncio<0.
|
48
|
+
Requires-Dist: pytest-asyncio<0.27,>=0.21; extra == 'test'
|
48
49
|
Requires-Dist: pytest-cov; extra == 'test'
|
49
50
|
Requires-Dist: pytest<9,>=7; extra == 'test'
|
50
51
|
Provides-Extra: typing
|
51
52
|
Requires-Dist: mypy<2,>=1; extra == 'typing'
|
52
|
-
Requires-Dist: pytest-asyncio<0.
|
53
|
+
Requires-Dist: pytest-asyncio<0.27,>=0.21; extra == 'typing'
|
53
54
|
Requires-Dist: pytest-cov; extra == 'typing'
|
54
55
|
Requires-Dist: pytest<9,>=7; extra == 'typing'
|
55
56
|
Description-Content-Type: text/markdown
|
@@ -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,59 @@ class FoamCaseRunBase(FoamCaseBase):
|
|
255
261
|
|
256
262
|
return script
|
257
263
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
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"]
|
264
|
+
@staticmethod
|
265
|
+
def __cmd_name(cmd: Sequence[str | os.PathLike[str]] | str) -> str:
|
266
|
+
if isinstance(cmd, str):
|
267
|
+
cmd = shlex.split(cmd)
|
271
268
|
|
272
|
-
|
273
|
-
return None
|
269
|
+
return Path(cmd[0]).name
|
274
270
|
|
275
271
|
@contextmanager
|
276
272
|
def __output(
|
277
273
|
self, cmd: Sequence[str | os.PathLike[str]] | str, *, log: bool
|
278
|
-
) -> Generator[tuple[int | IO[
|
274
|
+
) -> Generator[tuple[int | IO[str], int | IO[str]], None, None]:
|
279
275
|
if log:
|
280
|
-
|
281
|
-
name = shlex.split(cmd)[0]
|
282
|
-
else:
|
283
|
-
name = Path(cmd[0]).name if isinstance(cmd[0], os.PathLike) else cmd[0]
|
284
|
-
|
285
|
-
with (self.path / f"log.{name}").open("ab") as stdout:
|
276
|
+
with (self.path / f"log.{self.__cmd_name(cmd)}").open("a") as stdout:
|
286
277
|
yield stdout, STDOUT
|
287
278
|
else:
|
288
279
|
yield DEVNULL, DEVNULL
|
289
280
|
|
281
|
+
@contextmanager
|
282
|
+
def __process_stdout(
|
283
|
+
self, cmd: Sequence[str | os.PathLike[str]] | str
|
284
|
+
) -> Generator[Callable[[str], None], None, None]:
|
285
|
+
try:
|
286
|
+
with self.control_dict as control_dict:
|
287
|
+
if control_dict["stopAt"] == "endTime":
|
288
|
+
control_dict_end_time = control_dict["endTime"]
|
289
|
+
if isinstance(control_dict_end_time, (int, float)):
|
290
|
+
end_time = control_dict_end_time
|
291
|
+
else:
|
292
|
+
end_time = None
|
293
|
+
else:
|
294
|
+
end_time = None
|
295
|
+
except (KeyError, FileNotFoundError):
|
296
|
+
end_time = None
|
297
|
+
|
298
|
+
with self.__progress as progress:
|
299
|
+
task = progress.add_task(
|
300
|
+
f"({self.name}) Running {self.__cmd_name(cmd)}...", total=None
|
301
|
+
)
|
302
|
+
|
303
|
+
def process_stdout(line: str) -> None:
|
304
|
+
if line.startswith("Time = "):
|
305
|
+
try:
|
306
|
+
time = float(line.split()[2])
|
307
|
+
except ValueError:
|
308
|
+
progress.update(task)
|
309
|
+
else:
|
310
|
+
progress.update(task, completed=time, total=end_time)
|
311
|
+
else:
|
312
|
+
progress.update(task)
|
313
|
+
|
314
|
+
yield process_stdout
|
315
|
+
progress.update(task, completed=1, total=1)
|
316
|
+
|
290
317
|
def __mkrundir(self) -> Path:
|
291
318
|
d = Path(os.environ["FOAM_RUN"], "foamlib")
|
292
319
|
d.mkdir(parents=True, exist_ok=True)
|
@@ -379,15 +406,16 @@ class FoamCaseRunBase(FoamCaseBase):
|
|
379
406
|
if cpus is None:
|
380
407
|
cpus = 1
|
381
408
|
|
382
|
-
with self.__output(cmd, log=log) as (stdout, stderr)
|
409
|
+
with self.__output(cmd, log=log) as (stdout, stderr), self.__process_stdout(
|
410
|
+
cmd
|
411
|
+
) as process_stdout:
|
383
412
|
if parallel:
|
384
413
|
if isinstance(cmd, str):
|
385
414
|
cmd = [
|
386
415
|
"mpiexec",
|
387
416
|
"-n",
|
388
417
|
str(cpus),
|
389
|
-
|
390
|
-
"-c",
|
418
|
+
*FoamCaseRunBase._SHELL,
|
391
419
|
f"{cmd} -parallel",
|
392
420
|
]
|
393
421
|
else:
|
@@ -396,11 +424,11 @@ class FoamCaseRunBase(FoamCaseBase):
|
|
396
424
|
yield self._run(
|
397
425
|
cmd,
|
398
426
|
cpus=cpus,
|
427
|
+
case=self,
|
399
428
|
check=check,
|
400
|
-
cwd=self.path,
|
401
|
-
env=self.__env(shell=isinstance(cmd, str)),
|
402
429
|
stdout=stdout,
|
403
430
|
stderr=stderr,
|
431
|
+
process_stdout=process_stdout,
|
404
432
|
**kwargs,
|
405
433
|
)
|
406
434
|
|
@@ -31,10 +31,10 @@ class AsyncSlurmFoamCase(AsyncFoamCase):
|
|
31
31
|
await AsyncFoamCase._run(cmd, cpus=cpus, **kwargs)
|
32
32
|
return
|
33
33
|
|
34
|
-
if
|
35
|
-
|
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
|
+
)
|
@@ -2,7 +2,17 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import functools
|
4
4
|
import sys
|
5
|
-
|
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
|
@@ -167,7 +167,7 @@ def _dict_of(
|
|
167
167
|
|
168
168
|
if directive is not None:
|
169
169
|
assert data_entry is not None
|
170
|
-
keyword_entry |= directive + data_entry + LineEnd().suppress()
|
170
|
+
keyword_entry |= directive + data_entry + LineEnd().suppress()
|
171
171
|
|
172
172
|
if located:
|
173
173
|
keyword_entry = Located(keyword_entry)
|
@@ -198,7 +198,7 @@ def _keyword_entry_of(
|
|
198
198
|
|
199
199
|
if directive is not None:
|
200
200
|
assert data_entry is not None
|
201
|
-
keyword_entry |= directive + data_entry + LineEnd().suppress()
|
201
|
+
keyword_entry |= directive + data_entry + LineEnd().suppress()
|
202
202
|
|
203
203
|
if located:
|
204
204
|
keyword_entry = Located(keyword_entry)
|
@@ -214,6 +214,8 @@ _COMMENT = Regex(r"(?:/\*(?:[^*]|\*(?!/))*\*/)|(?://(?:\\\n|[^\n])*)")
|
|
214
214
|
_IDENTCHARS = identchars + "$"
|
215
215
|
_IDENTBODYCHARS = (
|
216
216
|
printables.replace(";", "")
|
217
|
+
.replace("(", "")
|
218
|
+
.replace(")", "")
|
217
219
|
.replace("{", "")
|
218
220
|
.replace("}", "")
|
219
221
|
.replace("[", "")
|
@@ -243,10 +245,12 @@ _TENSOR = (
|
|
243
245
|
| _tensor(TensorKind.TENSOR)
|
244
246
|
)
|
245
247
|
_PARENTHESIZED = Forward()
|
246
|
-
_IDENTIFIER = Combine(
|
247
|
-
|
248
|
+
_IDENTIFIER = Combine(Word(_IDENTCHARS, _IDENTBODYCHARS) + Opt(_PARENTHESIZED))
|
249
|
+
_PARENTHESIZED <<= Combine(
|
250
|
+
Literal("(")
|
251
|
+
+ (_PARENTHESIZED | Word(_IDENTBODYCHARS) + Opt(_PARENTHESIZED))
|
252
|
+
+ Literal(")")
|
248
253
|
)
|
249
|
-
_PARENTHESIZED <<= Combine(Literal("(") + (_PARENTHESIZED | _IDENTIFIER) + Literal(")"))
|
250
254
|
|
251
255
|
_DIMENSIONED = (Opt(_IDENTIFIER) + _DIMENSIONS + _TENSOR).set_parse_action(
|
252
256
|
lambda tks: Dimensioned(*reversed(tks.as_list()))
|
@@ -261,7 +265,7 @@ _FIELD = (Keyword("uniform", _IDENTBODYCHARS).suppress() + _TENSOR) | (
|
|
261
265
|
)
|
262
266
|
)
|
263
267
|
_DIRECTIVE = Word("#", _IDENTBODYCHARS)
|
264
|
-
_TOKEN = dbl_quoted_string |
|
268
|
+
_TOKEN = dbl_quoted_string | _DIRECTIVE | _IDENTIFIER
|
265
269
|
_DATA = Forward()
|
266
270
|
_KEYWORD_ENTRY = _keyword_entry_of(_TOKEN | _list_of(_IDENTIFIER), _DATA)
|
267
271
|
_DICT = _dict_of(_TOKEN, _DATA)
|
@@ -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,<15",
|
37
38
|
]
|
38
39
|
|
39
40
|
dynamic = ["version"]
|
@@ -42,7 +43,7 @@ dynamic = ["version"]
|
|
42
43
|
lint = ["ruff"]
|
43
44
|
test = [
|
44
45
|
"pytest>=7,<9",
|
45
|
-
"pytest-asyncio>=0.21,<0.
|
46
|
+
"pytest-asyncio>=0.21,<0.27",
|
46
47
|
"pytest-cov",
|
47
48
|
]
|
48
49
|
typing = [
|
@@ -93,6 +93,7 @@ def test_parse_value() -> None:
|
|
93
93
|
assert Parsed(b"({a b; c d;} {e g;})")[()] == [{"a": "b", "c": "d"}, {"e": "g"}]
|
94
94
|
assert Parsed(b"(water oil mercury air)")[()] == ["water", "oil", "mercury", "air"]
|
95
95
|
assert Parsed(b"div(phi,U)")[()] == "div(phi,U)"
|
96
|
+
assert Parsed(b"U.component(1)")[()] == "U.component(1)"
|
96
97
|
assert Parsed(b"div(nuEff*dev(T(grad(U))))")[()] == "div(nuEff*dev(T(grad(U))))"
|
97
98
|
assert Parsed(b"div((nuEff*dev(T(grad(U)))))")[()] == "div((nuEff*dev(T(grad(U)))))"
|
98
99
|
assert Parsed(b"((air and water) { type constant; sigma 0.07; })")[()] == [
|
@@ -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
|
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
|