diff --git a/include/pybind11/iostream.h b/include/pybind11/iostream.h index 44261e881e..df7fa3c381 100644 --- a/include/pybind11/iostream.h +++ b/include/pybind11/iostream.h @@ -131,7 +131,22 @@ class pythonbuf : public std::streambuf { setp(d_buffer.get(), d_buffer.get() + buf_size - 1); } - pythonbuf(pythonbuf &&) = default; + pythonbuf(pythonbuf &&other) noexcept + : buf_size(other.buf_size), d_buffer(std::move(other.d_buffer)), + pywrite(std::move(other.pywrite)), pyflush(std::move(other.pyflush)) { + const auto pending = (other.pbase() != nullptr && other.pptr() != nullptr) + ? static_cast(other.pptr() - other.pbase()) + : 0; + if (d_buffer != nullptr) { + // Rebuild the put area from the transferred storage. + setp(d_buffer.get(), d_buffer.get() + buf_size - 1); + pbump(pending); + } else { + setp(nullptr, nullptr); + } + // Prevent the moved-from destructor from flushing through moved-out handles. + other.setp(nullptr, nullptr); + } /// Sync before destroy ~pythonbuf() override { _sync(); } @@ -169,6 +184,7 @@ class scoped_ostream_redirect { std::streambuf *old; std::ostream &costream; detail::pythonbuf buffer; + bool active = true; public: explicit scoped_ostream_redirect(std::ostream &costream = std::cout, @@ -178,10 +194,22 @@ class scoped_ostream_redirect { old = costream.rdbuf(&buffer); } - ~scoped_ostream_redirect() { costream.rdbuf(old); } + ~scoped_ostream_redirect() { + if (active) { + costream.rdbuf(old); + } + } scoped_ostream_redirect(const scoped_ostream_redirect &) = delete; - scoped_ostream_redirect(scoped_ostream_redirect &&other) = default; + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + scoped_ostream_redirect(scoped_ostream_redirect &&other) + : old(other.old), costream(other.costream), buffer(std::move(other.buffer)), + active(other.active) { + if (active) { + costream.rdbuf(&buffer); // Re-point stream to our buffer + other.active = false; + } + } scoped_ostream_redirect &operator=(const scoped_ostream_redirect &) = delete; scoped_ostream_redirect &operator=(scoped_ostream_redirect &&) = delete; }; diff --git a/tests/test_iostream.cpp b/tests/test_iostream.cpp index 421eaa2dd8..7484e734be 100644 --- a/tests/test_iostream.cpp +++ b/tests/test_iostream.cpp @@ -123,4 +123,40 @@ TEST_SUBMODULE(iostream, m) { .def("stop", &TestThread::stop) .def("join", &TestThread::join) .def("sleep", &TestThread::sleep); + + m.def("move_redirect_output", [](const std::string &msg_before, const std::string &msg_after) { + py::scoped_ostream_redirect redir1(std::cout, py::module_::import("sys").attr("stdout")); + std::cout << msg_before << std::flush; + py::scoped_ostream_redirect redir2(std::move(redir1)); + std::cout << msg_after << std::flush; + }); + + m.def("move_redirect_output_unflushed", + [](const std::string &msg_before, const std::string &msg_after) { + py::scoped_ostream_redirect redir1(std::cout, + py::module_::import("sys").attr("stdout")); + std::cout << msg_before; + py::scoped_ostream_redirect redir2(std::move(redir1)); + std::cout << msg_after << std::flush; + }); + + // Redirect a stream whose original rdbuf is nullptr, then move the redirect. + // Verifies that nullptr is correctly restored (not confused with a moved-from sentinel). + m.def("move_redirect_null_rdbuf", [](const std::string &msg) { + std::ostream os(nullptr); + py::scoped_ostream_redirect redir1(os, py::module_::import("sys").attr("stdout")); + os << msg << std::flush; + py::scoped_ostream_redirect redir2(std::move(redir1)); + os << msg << std::flush; + // After redir2 goes out of scope, os.rdbuf() should be restored to nullptr. + }); + + m.def("get_null_rdbuf_restored", [](const std::string &msg) -> bool { + std::ostream os(nullptr); + { + py::scoped_ostream_redirect redir(os, py::module_::import("sys").attr("stdout")); + os << msg << std::flush; + } + return os.rdbuf() == nullptr; + }); } diff --git a/tests/test_iostream.py b/tests/test_iostream.py index 791b9e0483..857e0b5f73 100644 --- a/tests/test_iostream.py +++ b/tests/test_iostream.py @@ -284,6 +284,31 @@ def test_redirect_both(capfd): assert stream2.getvalue() == msg2 +def test_move_redirect(capsys): + m.move_redirect_output("before_move", "after_move") + stdout, stderr = capsys.readouterr() + assert stdout == "before_moveafter_move" + assert not stderr + + +def test_move_redirect_unflushed(capsys): + m.move_redirect_output_unflushed("before_move", "after_move") + stdout, stderr = capsys.readouterr() + assert stdout == "before_moveafter_move" + assert not stderr + + +def test_move_redirect_null_rdbuf(capsys): + m.move_redirect_null_rdbuf("hello") + stdout, stderr = capsys.readouterr() + assert stdout == "hellohello" + assert not stderr + + +def test_null_rdbuf_restored(): + assert m.get_null_rdbuf_restored("test") + + @pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_threading(): with m.ostream_redirect(stdout=True, stderr=False):