From 5aff05aa82c0356b701a2e71827817f4e5f81a86 Mon Sep 17 00:00:00 2001 From: Tung Lam Date: Fri, 21 Aug 2026 10:21:25 +0000 Subject: [PATCH 1/2] fix: avoid eagerly opening lazy FIFO reads --- CHANGES.md | 3 +++ src/click/utils.py | 7 +++++-- tests/test_utils/test_LazyFile.py | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 42ca48b58..d3be2a94c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ Unreleased prompt when the output stream does not support them, matching `echo()`. This stripping was lost in `8.4.0` when {pr}`2969` began writing the prompt with `input()` directly. {issue}`3572` {pr}`3653` +- Lazy read-mode {class}`File` parameters no longer eagerly open FIFO paths, + which could consume a writer's data before the command accesses the file. + {issue}`2645` - Fix test failures when using pytest >= 9.1. {pr}`3656` - {class}`Path` with `allow_dash=True` no longer triggers a `BytesWarning`, an error under `python -bb`, when checking a value against the `-` diff --git a/src/click/utils.py b/src/click/utils.py index b529eb04b..eb3f2f380 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -3,6 +3,7 @@ import collections.abc as cabc import os import re +import stat import sys import typing as t from functools import update_wrapper @@ -146,10 +147,12 @@ def __init__( if self.name == "-": self._f, self.should_close = open_stream(filename, mode, encoding, errors) else: - if "r" in mode: + if "r" in mode and not stat.S_ISFIFO(os.stat(filename).st_mode): # Open and close the file in case we're opening it for # reading so that we can catch at least some errors in - # some cases early. + # some cases early. Do not do this for FIFOs because an + # eager read can consume the writer's data before the lazy + # file is accessed. open(filename, mode).close() self._f = None self.should_close = True diff --git a/tests/test_utils/test_LazyFile.py b/tests/test_utils/test_LazyFile.py index 8d0775aa8..dfe5e82e6 100644 --- a/tests/test_utils/test_LazyFile.py +++ b/tests/test_utils/test_LazyFile.py @@ -1,3 +1,8 @@ +import builtins +import os + +import pytest + import click @@ -9,3 +14,18 @@ def test_iter_lazyfile(tmpdir): with click.utils._LazyFile(f.name) as lf: for e_line, a_line in zip(expected, lf, strict=False): assert e_line == a_line.strip() + + +@pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="FIFOs are not supported.") +def test_lazyfile_does_not_eagerly_open_fifo(tmp_path, monkeypatch): + """Issue #2645: lazy read-mode files must not consume FIFO input early.""" + path = tmp_path / "input" + os.mkfifo(path) + + def unexpected_open(*args, **kwargs): + raise AssertionError("lazy FIFO setup should not open the file") + + monkeypatch.setattr(builtins, "open", unexpected_open) + lazy_file = click.utils._LazyFile(path, "rb") + + assert lazy_file._f is None From 75f80e5843fbe217ee709a5e8b411930b9f63062 Mon Sep 17 00:00:00 2001 From: Tung Lam Date: Fri, 21 Aug 2026 10:24:56 +0000 Subject: [PATCH 2/2] fix: preserve lazy file errors after FIFO check --- src/click/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/click/utils.py b/src/click/utils.py index eb3f2f380..9fea2ae97 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -147,7 +147,15 @@ def __init__( if self.name == "-": self._f, self.should_close = open_stream(filename, mode, encoding, errors) else: - if "r" in mode and not stat.S_ISFIFO(os.stat(filename).st_mode): + is_fifo = False + + if "r" in mode: + try: + is_fifo = stat.S_ISFIFO(os.stat(filename).st_mode) + except OSError: + pass + + if "r" in mode and not is_fifo: # Open and close the file in case we're opening it for # reading so that we can catch at least some errors in # some cases early. Do not do this for FIFOs because an