Skip to content

Click 8.3.3 breaks pytest when stream fd is duplicated #3384

@ryanking13

Description

@ryanking13

Starting from Click 8.3.3, I've noticed that it breaks pytest when the standard stream is duplicated with os.dup2.

I guess the related change is: #3244

I am not sure if this is an expected behaviro of that change, but for me it was a bit surprising behavior, hence reporting.

Reproducer

"""
Passes on click <=8.3.2, crashes pytest on click 8.3.3.

    pip install click==8.3.3 pytest
    pytest test_click_fileno_regression.py  # OSError: Illegal seek

    pip install click==8.3.2
    pytest test_click_fileno_regression.py   # passes
"""

import io
import os
import sys

import click
from click.testing import CliRunner


@click.command()
def cmd():
    # Many libraries (build tools, logging tees, subprocess wrappers)
    # use os.dup2 on sys.stdout.fileno() to redirect output at the
    # fd level.
    #
    # On click <=8.3.2, sys.stdout.fileno() inside CliRunner raises
    # UnsupportedOperation.
    #
    # On click 8.3.3, sys.stdout.fileno() returns the original
    # stream's fd
    try:
        stdout_fd = sys.stdout.fileno()
    except io.UnsupportedOperation:
        click.echo("fileno() not available (click <=8.3.2)")
        return

    r, w = os.pipe()
    os.dup2(w, stdout_fd)
    os.close(w)
    os.close(r)
    click.echo("hello")


def test_invoke():
    runner = CliRunner()
    result = runner.invoke(cmd)
    assert result.exit_code == 0
Traceback (most recent call last):
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/bin/pytest", line 10, in <module>
    sys.exit(console_main())
             ^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py", line 223, in console_main
    code = main()
           ^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py", line 199, in main
    ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/main.py", line 365, in pytest_cmdline_main
    return wrap_session(config, _main)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/main.py", line 360, in wrap_session
    config._ensure_unconfigure()
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/config/__init__.py", line 1171, in _ensure_unconfigure
    self._cleanup_stack.close()
  File "/Users/gyeongjae/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/python3.12/contextlib.py", line 618, in close
    self.__exit__(None, None, None)
  File "/Users/gyeongjae/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/python3.12/contextlib.py", line 610, in __exit__
    raise exc_details[1]
  File "/Users/gyeongjae/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/python3.12/contextlib.py", line 595, in __exit__
    if cb(*exc_details):
       ^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/python3.12/contextlib.py", line 478, in _exit_wrapper
    callback(*args, **kwds)
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/capture.py", line 778, in stop_global_capturing
    self._global_capturing.pop_outerr_to_orig()
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/capture.py", line 659, in pop_outerr_to_orig
    out, err = self.readouterr()
               ^^^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/capture.py", line 706, in readouterr
    out = self.out.snap() if self.out else ""
          ^^^^^^^^^^^^^^^
  File "/Users/gyeongjae/Desktop/github/pyodide/fork/pyodide-build/.venv/lib/python3.12/site-packages/_pytest/capture.py", line 591, in snap
    self.tmpfile.seek(0)
OSError: [Errno 29] Illegal seek

Environment:

  • Python version: 3.12.3
  • Click version: 8.3.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions