Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

functools.partial(print, file=sys.stderr) stderr not captured via capsys/capfd #8900

Closed
hroncok opened this issue Jul 12, 2021 · 6 comments
Closed
Labels
plugin: capture related to the capture builtin plugin type: question general question, might be closed after 2 weeks of inactivity

Comments

@hroncok
Copy link
Member

hroncok commented Jul 12, 2021

I've been wrapping my head around this for a while and I was unable to figure out what's going on and why my capsys (or capfd) usage does not capture stderr at all. I've narrowed the case down to using functools.partial(print, file=sys.stderr). When that function is used, its output is not captured in capsys.readouterr().err (or capsys.readouterr().err).

Here is a reproducer adapted from the docs:

import functools
import sys

def test_myoutput(capsys):
    print("hello")
    sys.stderr.write("world\n")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    assert captured.err == "world\n"


def test_myoutput_all_prints(capsys):
    print("hello")
    print("world", file=sys.stderr)
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    assert captured.err == "world\n"


print_err = functools.partial(print, file=sys.stderr)


def test_myoutput_with_partial(capsys):
    print("hello")
    print_err("world")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    assert captured.err == "world\n"


def test_myoutput_with_partial_capfd(capfd):
    print("hello")
    print_err("world")
    captured = capfd.readouterr()
    assert captured.out == "hello\n"
    assert captured.err == "world\n"

I'd expect all tests here to pass. The ones using print_err don't pass.

============================= test session starts ==============================
platform linux -- Python 3.9.6, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/churchyard/tmp
collected 4 items

test_capsys.py ..FF                                                      [100%]

=================================== FAILURES ===================================
__________________________ test_myoutput_with_partial __________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7f75539b6dc0>

    def test_myoutput_with_partial(capsys):
        print("hello")
        print_err("world")
        captured = capsys.readouterr()
        assert captured.out == "hello\n"
>       assert captured.err == "world\n"
E       AssertionError: assert '' == 'world\n'
E         - world

test_capsys.py:28: AssertionError
----------------------------- Captured stderr call -----------------------------
world
_______________________ test_myoutput_with_partial_capfd _______________________

capfd = <_pytest.capture.CaptureFixture object at 0x7f75538e0c40>

    def test_myoutput_with_partial_capfd(capfd):
        print("hello")
        print_err("world")
        captured = capfd.readouterr()
        assert captured.out == "hello\n"
>       assert captured.err == "world\n"
E       AssertionError: assert '' == 'world\n'
E         - world

test_capsys.py:36: AssertionError
----------------------------- Captured stderr call -----------------------------
world
=========================== short test summary info ============================
FAILED test_capsys.py::test_myoutput_with_partial - AssertionError: assert ''...
FAILED test_capsys.py::test_myoutput_with_partial_capfd - AssertionError: ass...
========================= 2 failed, 2 passed in 0.03s ==========================

The capsys/capfd captured standard error is empty. The printed "Captured stderr call" is populated instead.

My operating system is Fedora Linux. This is pip list:

Package    Version
---------- -------
attrs      21.2.0
iniconfig  1.1.1
packaging  21.0
pip        20.2.2
pluggy     0.13.1
py         1.10.0
pyparsing  2.4.7
pytest     6.2.4
setuptools 49.1.3
toml       0.10.2

But this also happens at least with pytest 6.0, 5.4 and 4.6.

Possibly related to #5997 but not sure.

@hroncok
Copy link
Member Author

hroncok commented Jul 12, 2021

When the function is defined without partial, it works.

I.e. replacing print_err = functools.partial(print, file=sys.stderr) with:

def print_err(*args, **kwargs):
    kwargs.setdefault("file", sys.stderr)
    print(*args, **kwargs)

Makes the tests pass.

@Zac-HD Zac-HD added plugin: capture related to the capture builtin plugin type: question general question, might be closed after 2 weeks of inactivity labels Jul 13, 2021
@Zac-HD
Copy link
Member

Zac-HD commented Jul 13, 2021

This is "just" a matter of when the lookup is resolved:

  • print_err = functools.partial(print, file=sys.stderr) looks up the stderr attribute of the sys module when your file is imported and binds it to the file argument before pytest's capture machinery monkeypatches the sys module.
  • The function definition looks up the attribute each time you call print_err, i.e. after the monkeypatch

So I think your print_err function is probably the best way to go.

@Zac-HD Zac-HD closed this as completed Jul 13, 2021
@hroncok
Copy link
Member Author

hroncok commented Jul 13, 2021

Thanks for the explanation. This makes sense when explained, but I would probably never figured that out. Is this worth documenting somehow?

@hroncok
Copy link
Member Author

hroncok commented Jul 13, 2021

print_err = functools.partial(print, file=sys.stderr) looks up the stderr attribute of the sys module when your file is imported and binds it to the file argument before pytest's capture machinery monkeypatches the sys module.

I was thinking about this more and don't understand why does this also affect capfd. Does functools.partial also bind the file descriptor somehow?

@nicoddemus
Copy link
Member

It is that capfd also captures sys:

self.syscapture = SysCapture(targetfd)

@Glutexo
Copy link

Glutexo commented Nov 7, 2021

This also happens if you from sys import stderr instead of import sys. stderr then remains unpatched and thus the error output is not captured.

from sys import stderr

def test_err(capsys):
    print("stuff", file=stderr)
    out, err = capsys.readouterr()
    assert "err" == "stuff"  # Fails, err is "".
import sys

def test_err(capsys):
    print("stuff", file=sys.stderr)
    out, err = capsys.readouterr()
    assert "err" == "stuff"  # Passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plugin: capture related to the capture builtin plugin type: question general question, might be closed after 2 weeks of inactivity
Projects
None yet
Development

No branches or pull requests

4 participants