Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/advanced/pycpp/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ Using Python's print function in C++
The usual way to write output in C++ is using ``std::cout`` while in Python one
would use ``print``. Since these methods use different buffers, mixing them can
lead to output order issues. To resolve this, pybind11 modules can use the
:func:`py::print` function which writes to Python's ``sys.stdout`` for consistency.
:func:`py::print` function, which writes through Python's printing machinery.

Python's ``print`` function is replicated in the C++ API including optional
keyword arguments ``sep``, ``end``, ``file``, ``flush``. Everything works as
expected in Python:
:func:`py::print` delegates each call to the ``print`` entry in the current
execution frame's built-ins (normally ``builtins.print``). When no Python frame
is executing, it uses the active interpreter's built-ins. With the standard
built-in, optional keyword arguments ``sep``, ``end``, ``file``, and ``flush``
work as they do in Python:

.. code-block:: cpp

Expand All @@ -21,6 +23,10 @@ expected in Python:
auto args = py::make_tuple("unpacked", true);
py::print("->", *args, "end"_a="<-"); // -> unpacked True <-

With the standard built-in, omitting ``file`` or passing
``"file"_a = py::none()`` uses the current ``sys.stdout``. Other output and
error behavior is likewise supplied by the active Python runtime.

.. _ostream_redirect:

Capturing standard output from ostream
Expand Down
40 changes: 13 additions & 27 deletions include/pybind11/pybind11.h
Original file line number Diff line number Diff line change
Expand Up @@ -3764,34 +3764,20 @@ register_local_exception(handle scope, const char *name, handle base = PyExc_Exc

PYBIND11_NAMESPACE_BEGIN(detail)
PYBIND11_NOINLINE void print(const tuple &args, const dict &kwargs) {
auto strings = tuple(args.size());
for (size_t i = 0; i < args.size(); ++i) {
strings[i] = str(args[i]);
}
auto sep = kwargs.contains("sep") ? kwargs["sep"] : str(" ");
auto line = sep.attr("join")(std::move(strings));

object file;
if (kwargs.contains("file")) {
file = kwargs["file"].cast<object>();
} else {
try {
file = module_::import("sys").attr("stdout");
} catch (const error_already_set &) {
/* If print() is called from code that is executed as
part of garbage collection during interpreter shutdown,
importing 'sys' can fail. Give up rather than crashing the
interpreter in this case. */
return;
}
#if PY_VERSION_HEX >= 0x030D0000
auto builtins = reinterpret_steal<dict>(PyEval_GetFrameBuiltins());
#else
auto builtins = reinterpret_borrow<dict>(PyEval_GetBuiltins());
#endif
// The builtins dictionary may already be partially cleared during interpreter shutdown.
auto native_print = reinterpret_steal<object>(dict_getitemstringref(builtins.ptr(), "print"));
if (!native_print) {
return;
}

auto write = file.attr("write");
write(std::move(line));
write(kwargs.contains("end") ? kwargs["end"] : str("\n"));

if (kwargs.contains("flush") && kwargs["flush"].cast<bool>()) {
file.attr("flush")();
auto result
= reinterpret_steal<object>(PyObject_Call(native_print.ptr(), args.ptr(), kwargs.ptr()));
if (!result) {
throw error_already_set();
}
}
PYBIND11_NAMESPACE_END(detail)
Expand Down
21 changes: 2 additions & 19 deletions tests/test_pytypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -625,25 +625,8 @@ TEST_SUBMODULE(pytypes, m) {
return py::dict("d"_a = d, "l"_a = l);
});

// test_print
m.def("print_function", []() {
py::print("Hello, World!");
py::print(1, 2.0, "three", true, std::string("-- multiple args"));
auto args = py::make_tuple("and", "a", "custom", "separator");
py::print("*args", *args, "sep"_a = "-");
py::print("no new line here", "end"_a = " -- ");
py::print("next print");

auto py_stderr = py::module_::import("sys").attr("stderr");
py::print("this goes to stderr", "file"_a = py_stderr);

py::print("flush", "flush"_a = true);

py::print(
"{a} + {b} = {c}"_s.format("a"_a = "py::print", "b"_a = "str.format", "c"_a = "this"));
});

m.def("print_failure", []() { py::print(42, UnregisteredType()); });
m.def("print_args",
[](const py::args &args, const py::kwargs &kwargs) { py::print(*args, **kwargs); });

m.def("hash_function", [](py::object obj) { return py::hash(std::move(obj)); });

Expand Down
92 changes: 68 additions & 24 deletions tests/test_pytypes.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from __future__ import annotations

import builtins
import contextlib
import sys
import types

import pytest

import env
from pybind11_tests import detailed_error_messages_enabled
from pybind11_tests import pytypes as m


Expand Down Expand Up @@ -545,29 +545,73 @@ def test_implicit_casting():
assert z["l"] == [3, 6, 9, 12, 15]


def test_print(capture):
with capture:
m.print_function()
assert (
capture
== """
Hello, World!
1 2.0 three True -- multiple args
*args-and-a-custom-separator
no new line here -- next print
flush
py::print + str.format = this
"""
)
assert capture.stderr == "this goes to stderr"

with pytest.raises(RuntimeError) as excinfo:
m.print_failure()
assert str(excinfo.value) == "Unable to convert call argument " + (
"'1' of type 'UnregisteredType' to Python object"
if detailed_error_messages_enabled
else "'1' to Python object (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)"
)
def test_print_delegates_to_current_builtin(monkeypatch):
calls = []

def first_print(*args, **kwargs):
calls.append(("first", args, kwargs))
return object()

def second_print(*args, **kwargs):
calls.append(("second", args, kwargs))
return object()

positional = [object(), object()]
keywords = {"first": object(), "second": object()}

monkeypatch.setattr(builtins, "print", first_print)
assert m.print_args(*positional, **keywords) is None

monkeypatch.setattr(builtins, "print", second_print)
assert m.print_args() is None

assert calls[0][0] == "first"
assert len(calls[0][1]) == len(positional)
assert all(actual is expected for actual, expected in zip(calls[0][1], positional))
assert list(calls[0][2]) == list(keywords)
assert all(calls[0][2][key] is value for key, value in keywords.items())
assert calls[1] == ("second", (), {})


def test_print_missing_from_current_builtins_is_silent(monkeypatch):
# The builtins dictionary may have lost its print entry before C++ destructors
# run during interpreter shutdown.
with monkeypatch.context() as context:
context.delattr(builtins, "print")
result = m.print_args("ignored")
assert result is None


def test_print_stdout_none_matches_current_builtin(monkeypatch):
def exception_type(func):
try:
func("text")
except Exception as exc:
return type(exc)
return None

# Python runtimes differ here; py::print should follow the active runtime.
with monkeypatch.context() as context:
context.setattr(sys, "stdout", None)
native_exception_type = exception_type(builtins.print)
pybind_exception_type = exception_type(m.print_args)

assert pybind_exception_type is native_exception_type


def test_print_propagates_current_builtin_exception(monkeypatch):
class MarkerError(Exception):
pass

error = MarkerError()

def failing_print():
raise error

monkeypatch.setattr(builtins, "print", failing_print)
with pytest.raises(MarkerError) as exc_info:
m.print_args()
assert exc_info.value is error


def test_hash():
Expand Down
Loading