From bb690da56848bb40ce0c19132da66f7a4c04e5d0 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 28 Aug 2026 22:25:07 +0200 Subject: [PATCH 1/2] perf: skip reads for empty fd captures --- CHANGELOG.md | 2 ++ src/_pytask/capture.py | 6 ++++++ tests/test_capture.py | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4937678..f74d7305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and ## Unreleased +- Improves the performance of file descriptor capture for tasks that produce no + output. - Documents the pytest provenance of the adapted debugging, marker, and warning modules. - [#889](https://github.com/pytask-dev/pytask/pull/889) improves typing for tree diff --git a/src/_pytask/capture.py b/src/_pytask/capture.py index 7ea3546b..300ada5a 100644 --- a/src/_pytask/capture.py +++ b/src/_pytask/capture.py @@ -549,6 +549,9 @@ class FDCaptureBinary(FDCaptureBase[bytes]): def snap(self) -> bytes: self._assert_state("snap", ("started", "suspended")) + # Avoid seek/read/truncate in the common case of no output. + if os.fstat(self.tmpfile.fileno()).st_size == 0: + return self.EMPTY_BUFFER self.tmpfile.seek(0) res = self.tmpfile.buffer.read() self.tmpfile.seek(0) @@ -572,6 +575,9 @@ class FDCapture(FDCaptureBase[str]): def snap(self) -> str: self._assert_state("snap", ("started", "suspended")) + # Avoid seek/read/truncate in the common case of no output. + if os.fstat(self.tmpfile.fileno()).st_size == 0: + return self.EMPTY_BUFFER self.tmpfile.seek(0) res = self.tmpfile.read() self.tmpfile.seek(0) diff --git a/tests/test_capture.py b/tests/test_capture.py index fcbc319d..35277372 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -11,6 +11,7 @@ from typing import Any from typing import BinaryIO from typing import cast +from unittest.mock import Mock import pytest @@ -491,6 +492,25 @@ def lsof_check(): class TestFDCapture: + @pytest.mark.parametrize( + ("capture_class", "empty_buffer"), + [(capture.FDCapture, ""), (capture.FDCaptureBinary, b"")], + ) + def test_snap_empty_file_avoids_file_io( + self, monkeypatch, capture_class, empty_buffer + ): + cap = object.__new__(capture_class) + cap._state = "started" + cap.tmpfile = Mock() + cap.tmpfile.fileno.return_value = 42 + monkeypatch.setattr(capture.os, "fstat", lambda _fd: Mock(st_size=0)) + + assert cap.snap() == empty_buffer + cap.tmpfile.seek.assert_not_called() + cap.tmpfile.read.assert_not_called() + cap.tmpfile.buffer.read.assert_not_called() + cap.tmpfile.truncate.assert_not_called() + def test_simple(self, tmpfile): fd = tmpfile.fileno() cap = capture.FDCapture(fd) From 466f753e751c3766b1dbb61f5c5e9b60bbe1713e Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 28 Aug 2026 22:30:13 +0200 Subject: [PATCH 2/2] docs: link performance changelog entry --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f74d7305..a49fd87d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and ## Unreleased -- Improves the performance of file descriptor capture for tasks that produce no - output. +- [#968](https://github.com/pytask-dev/pytask/pull/968) improves the performance of file + descriptor capture for tasks that produce no output. - Documents the pytest provenance of the adapted debugging, marker, and warning modules. - [#889](https://github.com/pytask-dev/pytask/pull/889) improves typing for tree