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.
Files changed (52) hide show
  1. {foamlib-0.8.6 → foamlib-0.8.8}/PKG-INFO +5 -4
  2. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/__init__.py +1 -1
  3. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_async.py +3 -0
  4. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_run.py +57 -29
  5. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_slurm.py +3 -3
  6. foamlib-0.8.8/foamlib/_cases/_subprocess.py +188 -0
  7. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_sync.py +3 -0
  8. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_util.py +41 -1
  9. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_parsing.py +10 -6
  10. {foamlib-0.8.6 → foamlib-0.8.8}/pyproject.toml +2 -1
  11. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/test_parsing.py +1 -0
  12. foamlib-0.8.6/foamlib/_cases/_subprocess.py +0 -152
  13. {foamlib-0.8.6 → foamlib-0.8.8}/.devcontainer.json +0 -0
  14. {foamlib-0.8.6 → foamlib-0.8.8}/.dockerignore +0 -0
  15. {foamlib-0.8.6 → foamlib-0.8.8}/.git-blame-ignore-revs +0 -0
  16. {foamlib-0.8.6 → foamlib-0.8.8}/.github/dependabot.yml +0 -0
  17. {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/ci.yml +0 -0
  18. {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/docker.yml +0 -0
  19. {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/dockerhub-description.yml +0 -0
  20. {foamlib-0.8.6 → foamlib-0.8.8}/.github/workflows/pypi-publish.yml +0 -0
  21. {foamlib-0.8.6 → foamlib-0.8.8}/.gitignore +0 -0
  22. {foamlib-0.8.6 → foamlib-0.8.8}/.readthedocs.yaml +0 -0
  23. {foamlib-0.8.6 → foamlib-0.8.8}/Dockerfile +0 -0
  24. {foamlib-0.8.6 → foamlib-0.8.8}/LICENSE.txt +0 -0
  25. {foamlib-0.8.6 → foamlib-0.8.8}/README.md +0 -0
  26. {foamlib-0.8.6 → foamlib-0.8.8}/benchmark.png +0 -0
  27. {foamlib-0.8.6 → foamlib-0.8.8}/docs/Makefile +0 -0
  28. {foamlib-0.8.6 → foamlib-0.8.8}/docs/cases.rst +0 -0
  29. {foamlib-0.8.6 → foamlib-0.8.8}/docs/conf.py +0 -0
  30. {foamlib-0.8.6 → foamlib-0.8.8}/docs/files.rst +0 -0
  31. {foamlib-0.8.6 → foamlib-0.8.8}/docs/index.rst +0 -0
  32. {foamlib-0.8.6 → foamlib-0.8.8}/docs/make.bat +0 -0
  33. {foamlib-0.8.6 → foamlib-0.8.8}/docs/ruff.toml +0 -0
  34. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/__init__.py +0 -0
  35. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_cases/_base.py +0 -0
  36. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/__init__.py +0 -0
  37. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_files.py +0 -0
  38. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_io.py +0 -0
  39. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_serialization.py +0 -0
  40. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/_files/_types.py +0 -0
  41. {foamlib-0.8.6 → foamlib-0.8.8}/foamlib/py.typed +0 -0
  42. {foamlib-0.8.6 → foamlib-0.8.8}/logo.png +0 -0
  43. {foamlib-0.8.6 → foamlib-0.8.8}/tests/__init__.py +0 -0
  44. {foamlib-0.8.6 → foamlib-0.8.8}/tests/ruff.toml +0 -0
  45. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/__init__.py +0 -0
  46. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_cavity.py +0 -0
  47. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_cavity_async.py +0 -0
  48. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_flange.py +0 -0
  49. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_cases/test_flange_async.py +0 -0
  50. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/__init__.py +0 -0
  51. {foamlib-0.8.6 → foamlib-0.8.8}/tests/test_files/test_dumps.py +0 -0
  52. {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.6
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.26,>=0.21; extra == 'dev'
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.26,>=0.21; extra == 'test'
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.26,>=0.21; extra == 'typing'
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
@@ -1,6 +1,6 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.8.6"
3
+ __version__ = "0.8.8"
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,59 @@ 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"]
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
- return env
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[bytes], int | IO[bytes]], None, None]:
274
+ ) -> Generator[tuple[int | IO[str], int | IO[str]], None, None]:
279
275
  if log:
280
- if isinstance(cmd, str):
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
- "/bin/sh",
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 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
@@ -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() # type: ignore [no-untyped-call]
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() # type: ignore [no-untyped-call]
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
- Word(_IDENTCHARS, _IDENTBODYCHARS, exclude_chars="()") + Opt(_PARENTHESIZED)
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 | _IDENTIFIER | _DIRECTIVE
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.26",
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