From 5484a12de84f422ae12768fec50ef1e350d54ae4 Mon Sep 17 00:00:00 2001 From: Alexander Reinhold Date: Mon, 2 Mar 2026 16:36:17 +0100 Subject: [PATCH 1/3] added possibilty to create custom named logfiles --- src/foamlib/_cases/_run.py | 37 +++++++++++++++++++++--------------- src/foamlib/_cases/async_.py | 10 +++++----- src/foamlib/_cases/slurm.py | 2 +- src/foamlib/_cases/sync.py | 10 +++++----- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/foamlib/_cases/_run.py b/src/foamlib/_cases/_run.py index fa21c487..c63ea738 100644 --- a/src/foamlib/_cases/_run.py +++ b/src/foamlib/_cases/_run.py @@ -112,7 +112,7 @@ def clone(self, dst: os.PathLike[str] | str | None = None) -> object: @abstractmethod def _prepare( - self, *, check: bool = True, log: bool = True + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True ) -> None | Coroutine[None, None, None]: raise NotImplementedError @@ -124,25 +124,25 @@ def run( parallel: bool | None = None, cpus: int | None = None, check: bool = True, - log: bool = True, + log: bool | str | os.PathLike[str] = True, ) -> None | Coroutine[None, None, None]: raise NotImplementedError @abstractmethod def block_mesh( - self, *, check: bool = True, log: bool = True + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True ) -> None | Coroutine[None, None, None]: raise NotImplementedError @abstractmethod def decompose_par( - self, *, check: bool = True, log: bool = True + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True ) -> None | Coroutine[None, None, None]: raise NotImplementedError @abstractmethod def reconstruct_par( - self, *, check: bool = True, log: bool = True + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True ) -> None | Coroutine[None, None, None]: raise NotImplementedError @@ -262,13 +262,20 @@ def __cmd_name(cmd: Sequence[str | os.PathLike[str]] | str) -> str: @contextmanager def __output( - self, cmd: Sequence[str | os.PathLike[str]] | str, *, log: bool + self, + cmd: Sequence[str | os.PathLike[str]] | str, + *, + log: bool | str | os.PathLike[str], ) -> Generator[tuple[int | TextIOBase, int | TextIOBase], None, None]: - if log: - with (self.path / f"log.{self.__cmd_name(cmd)}").open("a") as stdout: - yield stdout, STDOUT - else: + if log is False: yield DEVNULL, DEVNULL + return + + if log is True: + log = f"log.{self.__cmd_name(cmd)}" + + with (self.path / log).open("a") as stdout: + yield stdout, STDOUT @contextmanager def __process_stdout( @@ -364,22 +371,22 @@ def _restore_0_dir_calls( yield self._copytree(self.path / "0.orig", self.path / "0", symlinks=True) def _block_mesh_calls( - self, *, check: bool, log: bool + self, *, check: bool, log: bool | str | os.PathLike[str] ) -> Generator[object | Coroutine[None, None, object], None, None]: yield self.run(["blockMesh"], cpus=0, check=check, log=log) def _decompose_par_calls( - self, *, check: bool, log: bool + self, *, check: bool, log: bool | str | os.PathLike[str] ) -> Generator[object | Coroutine[None, None, object], None, None]: yield self.run(["decomposePar"], cpus=0, check=check, log=log) def _reconstruct_par_calls( - self, *, check: bool, log: bool + self, *, check: bool, log: bool | str | os.PathLike[str] ) -> Generator[object | Coroutine[None, None, object], None, None]: yield self.run(["reconstructPar"], cpus=0, check=check, log=log) def _prepare_calls( - self, *, check: bool, log: bool + self, *, check: bool, log: bool | str | os.PathLike[str] ) -> Generator[object | Coroutine[None, None, object], None, None]: if (script_path := self.__prepare_script()) is not None: yield self.run([script_path], log=log, check=check) @@ -394,7 +401,7 @@ def _run_calls( cpus: int | None = None, parallel: bool | None, check: bool, - log: bool, + log: bool | str | os.PathLike[str], **kwargs: Any, ) -> Generator[object | Coroutine[None, None, object], None, None]: if cmd is not None: diff --git a/src/foamlib/_cases/async_.py b/src/foamlib/_cases/async_.py index b7aca9fb..a9f0d992 100644 --- a/src/foamlib/_cases/async_.py +++ b/src/foamlib/_cases/async_.py @@ -203,7 +203,7 @@ def __getitem__( return [AsyncFoamCase.TimeDirectory(r) for r in ret] @override - async def _prepare(self, *, check: bool = True, log: bool = True) -> None: + async def _prepare(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: for coro in self._prepare_calls(check=check, log=log): assert asyncio.iscoroutine(coro) await coro @@ -216,7 +216,7 @@ async def run( parallel: bool | None = None, cpus: int | None = None, check: bool = True, - log: bool = True, + log: bool | str | os.PathLike[str] = True, ) -> None: """ Run this case, or a specified command in the context of this case. @@ -263,21 +263,21 @@ async def run( await coro @override - async def block_mesh(self, *, check: bool = True, log: bool = True) -> None: + async def block_mesh(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: """Run blockMesh on this case.""" for coro in self._block_mesh_calls(check=check, log=log): assert asyncio.iscoroutine(coro) await coro @override - async def decompose_par(self, *, check: bool = True, log: bool = True) -> None: + async def decompose_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: """Decompose this case for parallel running.""" for coro in self._decompose_par_calls(check=check, log=log): assert asyncio.iscoroutine(coro) await coro @override - async def reconstruct_par(self, *, check: bool = True, log: bool = True) -> None: + async def reconstruct_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: """Reconstruct this case after parallel running.""" for coro in self._reconstruct_par_calls(check=check, log=log): assert asyncio.iscoroutine(coro) diff --git a/src/foamlib/_cases/slurm.py b/src/foamlib/_cases/slurm.py index 4673a0a9..b1d32b83 100644 --- a/src/foamlib/_cases/slurm.py +++ b/src/foamlib/_cases/slurm.py @@ -78,7 +78,7 @@ async def run( parallel: bool | None = None, cpus: int | None = None, check: bool = True, - log: bool = True, + log: bool | str | os.PathLike[str] = True, fallback: bool = False, ) -> None: """ diff --git a/src/foamlib/_cases/sync.py b/src/foamlib/_cases/sync.py index a23d0b54..71ec33d3 100644 --- a/src/foamlib/_cases/sync.py +++ b/src/foamlib/_cases/sync.py @@ -165,7 +165,7 @@ def clean(self, *, check: bool = False) -> None: pass @override - def _prepare(self, *, check: bool = True, log: bool = True) -> None: + def _prepare(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: for _ in self._prepare_calls(check=check, log=log): pass @@ -177,7 +177,7 @@ def run( parallel: bool | None = None, cpus: int | None = None, check: bool = True, - log: bool = True, + log: bool | str | os.PathLike[str] = True, ) -> None: """ Run this case, or a specified command in the context of this case. @@ -223,19 +223,19 @@ def run( pass @override - def block_mesh(self, *, check: bool = True, log: bool = True) -> None: + def block_mesh(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: """Run blockMesh on this case.""" for _ in self._block_mesh_calls(check=check, log=log): pass @override - def decompose_par(self, *, check: bool = True, log: bool = True) -> None: + def decompose_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: """Decompose this case for parallel running.""" for _ in self._decompose_par_calls(check=check, log=log): pass @override - def reconstruct_par(self, *, check: bool = True, log: bool = True) -> None: + def reconstruct_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: """Reconstruct this case after parallel running.""" for _ in self._reconstruct_par_calls(check=check, log=log): pass From a6ac3e78e7efdf9da5c4d36ee1d909daaa126926 Mon Sep 17 00:00:00 2001 From: Alexander Reinhold Date: Mon, 2 Mar 2026 16:52:21 +0100 Subject: [PATCH 2/3] added tests for custom log file names --- tests/test_cases/test_flange.py | 29 ++++++++++++++++++++ tests/test_cases/test_flange_async.py | 39 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/test_cases/test_flange.py b/tests/test_cases/test_flange.py index 712ec3ed..405fb2c2 100644 --- a/tests/test_cases/test_flange.py +++ b/tests/test_cases/test_flange.py @@ -71,3 +71,32 @@ def test_run_cmd_shell(flange: FoamCase) -> None: def test_path(flange: FoamCase) -> None: assert Path(flange) == flange.path + + +def test_run_cmd_log_false(flange: FoamCase) -> None: + if not flange: + flange.restore_0_dir() + + ans_path = ( + Path(os.environ["FOAM_TUTORIALS"]) / "resources" / "geometry" / "flange.ans" + ) + if not ans_path.exists(): + ans_path = Path("flange.ans") + + flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log=False) + assert not (flange.path / "log.ansysToFoam").exists() + + +def test_run_cmd_custom_log(flange: FoamCase) -> None: + if not flange: + flange.restore_0_dir() + + ans_path = ( + Path(os.environ["FOAM_TUTORIALS"]) / "resources" / "geometry" / "flange.ans" + ) + if not ans_path.exists(): + ans_path = Path("flange.ans") + + flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log="custom_ansys.log") + assert not (flange.path / "log.ansysToFoam").exists() + assert (flange.path / "custom_ansys.log").exists() diff --git a/tests/test_cases/test_flange_async.py b/tests/test_cases/test_flange_async.py index e82f6056..284bdbe1 100644 --- a/tests/test_cases/test_flange_async.py +++ b/tests/test_cases/test_flange_async.py @@ -94,3 +94,42 @@ async def test_run_cmd_shell(flange: AsyncFoamCase) -> None: def test_path(flange: AsyncFoamCase) -> None: assert Path(flange) == flange.path + + +@pytest.mark.asyncio +async def test_run_cmd_log_false(flange: AsyncFoamCase) -> None: + if not flange: + await flange.restore_0_dir() + + ans_path = ( + Path(os.environ["FOAM_TUTORIALS"]) / "resources" / "geometry" / "flange.ans" + ) + if not ans_path.exists(): + ans_path = Path("flange.ans") + + if isinstance(flange, AsyncSlurmFoamCase): + await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log=False, fallback=True) + else: + await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log=False) + + assert not (flange.path / "log.ansysToFoam").exists() + + +@pytest.mark.asyncio +async def test_run_cmd_custom_log(flange: AsyncFoamCase) -> None: + if not flange: + await flange.restore_0_dir() + + ans_path = ( + Path(os.environ["FOAM_TUTORIALS"]) / "resources" / "geometry" / "flange.ans" + ) + if not ans_path.exists(): + ans_path = Path("flange.ans") + + if isinstance(flange, AsyncSlurmFoamCase): + await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log="custom_ansys.log", fallback=True) + else: + await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log="custom_ansys.log") + + assert not (flange.path / "log.ansysToFoam").exists() + assert (flange.path / "custom_ansys.log").exists() From 988f94d313a6d136e63d5078741f068aeb50f796 Mon Sep 17 00:00:00 2001 From: Gabriel Gerlero Date: Mon, 2 Mar 2026 13:38:54 -0300 Subject: [PATCH 3/3] Format with Ruff --- src/foamlib/_cases/async_.py | 16 ++++++++++++---- src/foamlib/_cases/sync.py | 16 ++++++++++++---- tests/test_cases/test_flange_async.py | 14 +++++++++++--- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/foamlib/_cases/async_.py b/src/foamlib/_cases/async_.py index 5d17af42..5ddb6ac4 100644 --- a/src/foamlib/_cases/async_.py +++ b/src/foamlib/_cases/async_.py @@ -205,7 +205,9 @@ def __getitem__( return [AsyncFoamCase.TimeDirectory(r) for r in ret] @override - async def _prepare(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + async def _prepare( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: for coro in self._prepare_calls(check=check, log=log): assert isinstance(coro, Awaitable) await coro @@ -265,21 +267,27 @@ async def run( await coro @override - async def block_mesh(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + async def block_mesh( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: """Run blockMesh on this case.""" for coro in self._block_mesh_calls(check=check, log=log): assert isinstance(coro, Awaitable) await coro @override - async def decompose_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + async def decompose_par( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: """Decompose this case for parallel running.""" for coro in self._decompose_par_calls(check=check, log=log): assert isinstance(coro, Awaitable) await coro @override - async def reconstruct_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + async def reconstruct_par( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: """Reconstruct this case after parallel running.""" for coro in self._reconstruct_par_calls(check=check, log=log): assert isinstance(coro, Awaitable) diff --git a/src/foamlib/_cases/sync.py b/src/foamlib/_cases/sync.py index 71ec33d3..a9c780ad 100644 --- a/src/foamlib/_cases/sync.py +++ b/src/foamlib/_cases/sync.py @@ -165,7 +165,9 @@ def clean(self, *, check: bool = False) -> None: pass @override - def _prepare(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + def _prepare( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: for _ in self._prepare_calls(check=check, log=log): pass @@ -223,19 +225,25 @@ def run( pass @override - def block_mesh(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + def block_mesh( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: """Run blockMesh on this case.""" for _ in self._block_mesh_calls(check=check, log=log): pass @override - def decompose_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + def decompose_par( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: """Decompose this case for parallel running.""" for _ in self._decompose_par_calls(check=check, log=log): pass @override - def reconstruct_par(self, *, check: bool = True, log: bool | str | os.PathLike[str] = True) -> None: + def reconstruct_par( + self, *, check: bool = True, log: bool | str | os.PathLike[str] = True + ) -> None: """Reconstruct this case after parallel running.""" for _ in self._reconstruct_par_calls(check=check, log=log): pass diff --git a/tests/test_cases/test_flange_async.py b/tests/test_cases/test_flange_async.py index 284bdbe1..563242d1 100644 --- a/tests/test_cases/test_flange_async.py +++ b/tests/test_cases/test_flange_async.py @@ -108,7 +108,9 @@ async def test_run_cmd_log_false(flange: AsyncFoamCase) -> None: ans_path = Path("flange.ans") if isinstance(flange, AsyncSlurmFoamCase): - await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log=False, fallback=True) + await flange.run( + ["ansysToFoam", ans_path, "-scale", "0.001"], log=False, fallback=True + ) else: await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log=False) @@ -127,9 +129,15 @@ async def test_run_cmd_custom_log(flange: AsyncFoamCase) -> None: ans_path = Path("flange.ans") if isinstance(flange, AsyncSlurmFoamCase): - await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log="custom_ansys.log", fallback=True) + await flange.run( + ["ansysToFoam", ans_path, "-scale", "0.001"], + log="custom_ansys.log", + fallback=True, + ) else: - await flange.run(["ansysToFoam", ans_path, "-scale", "0.001"], log="custom_ansys.log") + await flange.run( + ["ansysToFoam", ans_path, "-scale", "0.001"], log="custom_ansys.log" + ) assert not (flange.path / "log.ansysToFoam").exists() assert (flange.path / "custom_ansys.log").exists()